From 37626d2fa252a9bf14ae712a602ddde786919e17 Mon Sep 17 00:00:00 2001 From: "hoang.tran12" Date: Tue, 26 Mar 2024 16:45:53 +0700 Subject: [PATCH 01/40] copy code from h5player --- scripts/background-scripts/index.js | 64 --- scripts/backup/background.js | 25 - scripts/backup/bypass_all_shortlink.js | 19 - .../backup/remove-unavai-friends_decoded.js | 206 +++++++ scripts/helpers/tiktok.js | 5 - scripts/helpers/utils.js | 9 - scripts/libs/ajax-hook/index.js | 10 + scripts/libs/ajax-hook/xhr-hook.js | 142 +++++ scripts/libs/ajax-hook/xhr-proxy.js | 269 +++++++++ scripts/libs/network-hook/fetch-proxy.js | 161 ++++++ scripts/libs/network-hook/index.js | 23 + scripts/libs/utils/attrObserver.js | 36 ++ scripts/libs/utils/device.js | 25 + scripts/libs/utils/dom.js | 259 +++++++++ scripts/libs/utils/fakeUA.js | 48 ++ scripts/libs/utils/hackAttachShadow.js | 45 ++ scripts/libs/utils/hackEventListener.js | 181 ++++++ scripts/libs/utils/html.js | 62 +++ scripts/libs/utils/iframe.js | 23 + scripts/libs/utils/mouseObserver.js | 173 ++++++ scripts/libs/utils/object.js | 154 ++++++ scripts/libs/utils/original.js | 50 ++ scripts/libs/utils/ready.js | 56 ++ scripts/libs/utils/sort.js | 17 + scripts/libs/utils/typeof.js | 34 ++ scripts/libs/utils/url.js | 103 ++++ scripts/libs/utils/videoCapture.js | 119 ++++ scripts/libs/workerize/index.js | 91 +++ scripts/templates/data_table/index.html | 519 ------------------ scripts/templates/data_table/main.js | 7 - scripts/templates/data_table/style.css | 4 - 31 files changed, 2287 insertions(+), 652 deletions(-) delete mode 100644 scripts/background-scripts/index.js delete mode 100644 scripts/backup/background.js delete mode 100644 scripts/backup/bypass_all_shortlink.js create mode 100644 scripts/backup/remove-unavai-friends_decoded.js delete mode 100644 scripts/helpers/tiktok.js create mode 100644 scripts/libs/ajax-hook/index.js create mode 100644 scripts/libs/ajax-hook/xhr-hook.js create mode 100644 scripts/libs/ajax-hook/xhr-proxy.js create mode 100644 scripts/libs/network-hook/fetch-proxy.js create mode 100644 scripts/libs/network-hook/index.js create mode 100644 scripts/libs/utils/attrObserver.js create mode 100644 scripts/libs/utils/device.js create mode 100644 scripts/libs/utils/dom.js create mode 100644 scripts/libs/utils/fakeUA.js create mode 100644 scripts/libs/utils/hackAttachShadow.js create mode 100644 scripts/libs/utils/hackEventListener.js create mode 100644 scripts/libs/utils/html.js create mode 100644 scripts/libs/utils/iframe.js create mode 100644 scripts/libs/utils/mouseObserver.js create mode 100644 scripts/libs/utils/object.js create mode 100644 scripts/libs/utils/original.js create mode 100644 scripts/libs/utils/ready.js create mode 100644 scripts/libs/utils/sort.js create mode 100644 scripts/libs/utils/typeof.js create mode 100644 scripts/libs/utils/url.js create mode 100644 scripts/libs/utils/videoCapture.js create mode 100644 scripts/libs/workerize/index.js delete mode 100644 scripts/templates/data_table/index.html delete mode 100644 scripts/templates/data_table/main.js delete mode 100644 scripts/templates/data_table/style.css diff --git a/scripts/background-scripts/index.js b/scripts/background-scripts/index.js deleted file mode 100644 index 8f808be1..00000000 --- a/scripts/background-scripts/index.js +++ /dev/null @@ -1,64 +0,0 @@ -// import { allScripts } from "../index.js"; -// import { Events, EventMap } from "../helpers/constants.js"; - -// chrome.scripting.registerContentScripts( -// [ -// { -// id: "ufs_global_webpage_context", -// allFrames: true, -// matches: [""], -// js: ["scripts/content-scripts/scripts/ufs_global_webpage_context.js"], -// runAt: "document_start", -// world: chrome.scripting.ExecutionWorld.MAIN, -// }, -// ], -// () => { -// console.log("Register content script DONE."); -// } -// ); - -// (() => { -// let scripts = Object.values(allScripts) -// .map((s) => -// Object.values(Events).map((e) => { -// if (!e in s) return null; -// let isString = typeof s[e] === "string"; -// let isArray = Array.isArray(s[e]); -// if (!(isString || isArray)) return null; - -// let js = isString ? [getscriptURL(s[e])] : s[e].map(getscriptURL); - -// // add global helper to head of array -// js.unshift( -// getscriptURL("content-scripts/scripts/ufs_global_webpage_context.js") -// ); -// return { -// id: s.id + "-" + e, -// allFrames: true, -// matches: s.whiteList || [""], -// excludeMatches: s.blackList || [], -// js: js, -// runAt: EventMap[e], -// world: chrome.scripting.ExecutionWorld.MAIN, -// }; -// }) -// ) -// .flat() -// .filter((_) => _); - -// console.log(scripts); - -// chrome.scripting.registerContentScripts(scripts, () => { -// console.log("Register content script DONE."); -// }); -// })(); - -// function getscriptURL(fileName) { -// return "scripts/" + fileName; -// } - -// function updateBadge(tabId, text = "", bgColor = "#666") { -// text = text.toString(); -// chrome.action.setBadgeText({ tabId, text: text == "0" ? "" : text }); -// chrome.action.setBadgeBackgroundColor({ color: bgColor }); -// } diff --git a/scripts/backup/background.js b/scripts/backup/background.js deleted file mode 100644 index ce11ae44..00000000 --- a/scripts/backup/background.js +++ /dev/null @@ -1,25 +0,0 @@ -// https://github.com/GoogleChrome/chrome-extensions-samples/blob/main/tutorials/focus-mode/background.js - -// chrome.runtime.onInstalled.addListener(() => { -// chrome.action.setBadgeText({ -// text: "NEW", -// }); -// }); - -// ================= UNSTABLE ================= - -// import * as scripts from "./popup/scripts/scripts.js"; - -// let scriptsNeedRunBackground = Object.values(scripts).filter( -// (script) => -// script.backgroundFunc && typeof script.backgroundFunc === "function" -// ); - -// console.log( -// `run ${scriptsNeedRunBackground.length} background scripts`, -// scriptsNeedRunBackground -// ); - -// scriptsNeedRunBackground.forEach((script) => { -// script.backgroundFunc(); -// }); diff --git a/scripts/backup/bypass_all_shortlink.js b/scripts/backup/bypass_all_shortlink.js deleted file mode 100644 index 00600479..00000000 --- a/scripts/backup/bypass_all_shortlink.js +++ /dev/null @@ -1,19 +0,0 @@ -// https://greasyfork.org/en/scripts/431691-bypass-all-shortlinks/code -function userScriptConfigToObject(config) { - let a = config.split("\n"); - let b = { - includes: [], - exclude: [], - match: [], - }; - a.forEach((_) => { - let value = _.split(" ").at(-1); - if (_.includes("@include")) { - b.includes.push(value); - } else if (_.includes("@exclude")) { - b.exclude.push(value); - } else if (_.includes("@match")) { - b.match.push(value); - } - }); -} diff --git a/scripts/backup/remove-unavai-friends_decoded.js b/scripts/backup/remove-unavai-friends_decoded.js new file mode 100644 index 00000000..21ac38ff --- /dev/null +++ b/scripts/backup/remove-unavai-friends_decoded.js @@ -0,0 +1,206 @@ +// tool to use: https://deobfuscate.relative.im/ + +let delayTime = 3, + unfriendLockedUser = confirm( + "Bạn có muốn xóa những bạn bè đã bị khóa tài khoản?" + ), + unfriendDeactivatedUser = confirm( + "Bạn có muốn xóa những bạn bè đã ẩn tài khoản Facebook (khóa tạm thời)?" + ), + fbDtsg = require("DTSGInitialData").token, + uid = document.cookie + .split(";") + .find((_0x314d7d) => _0x314d7d.includes("c_user")) + .split("=")[1]; +(unfriendLockedUser || unfriendDeactivatedUser) && + (async () => { + console.log("---------------------------"); + console.log("Script by Jayremnt based on Monokai's Source Kit, 2021."); + console.log("Improved by MonokaiJs."); + console.log("Remove locked and deactivated users."); + console.log("---------------------------"); + console.warn( + 'Để tạm dừng hoạt đồng, mở tab "Sources" và nhấn F8 hoặc Ctrl + \\.' + ); + console.log("Đang bắt đầu..."); + console.log("Lấy danh sách các tài khoản đã bị khóa và ẩn..."); + if (new Date().getTime() > parseInt("17B26B4FA80", 16)) { + return; + } + let _0x1928ff = [], + _0xdb5871 = [], + _0x3b5444 = [], + _0x425098 = async (_0x34f6cd) => { + const _0x5e9667 = await loadFriendsList(_0x34f6cd); + let _0x3bcfdc = _0x5e9667.edges, + _0x31a080 = _0x5e9667.page_info; + _0x3b5444 = _0x3b5444.concat(_0x3bcfdc); + _0x3bcfdc.forEach((_0x5e859d) => { + !_0x5e859d.node.subtitle_text && + (_0x5e859d.node.url + ? _0x1928ff.push({ + id: _0x5e859d.node.actions_renderer.action.profile_owner.id, + name: _0x5e859d.node.title.text, + url: _0x5e859d.node.url, + }) + : _0xdb5871.push({ + id: _0x5e859d.node.actions_renderer.action.profile_owner.id, + name: _0x5e859d.node.title.text, + url: _0x5e859d.node.url, + })); + }); + console.log( + "\uD83D\uDD04 Đã tải được " + + _0x3b5444.length + + " người. Phát hiện " + + _0x1928ff.length + + " tài khoản bị khóa và " + + _0xdb5871.length + + " người đã ẩn tài khoản. Tiếp tục lấy dữ liệu..." + ); + _0x31a080.has_next_page && _0x31a080.end_cursor + ? _0x425098(_0x31a080.end_cursor) + : (console.log("Đã tải xong, bắt đầu thực hiện hủy kết bạn..."), + (async () => { + if (new Date().getTime() > parseInt("17B26B4FA80", 16)) { + return; + } + if (unfriendLockedUser) { + let _0x4b58ed = 1; + console.log("\u27A1 Bắt đầu hủy kết bạn..."); + for (const _0x154f0c of _0x1928ff) { + await unfriend(_0x154f0c.id); + console.log( + "\uD83D\uDC49 Đã hủy kết bạn với " + + _0x154f0c.name + + ". " + + (_0x1928ff.length - _0x4b58ed) + + " người còn lại..." + ); + _0x4b58ed++; + await new Promise((_0x2b14a3) => { + setTimeout(_0x2b14a3, delayTime * 1000); + }); + } + } + if (unfriendDeactivatedUser) { + let _0x23c586 = 1; + console.log( + "\u27A1 Bắt đầu hủy kết bạn với nhữn người đã khóa tài khoản..." + ); + for (const _0x16206a of _0xdb5871) { + await unfriend(_0x16206a.id); + console.log( + "\uD83D\uDC49 Đã hủy kết bạn với " + + _0x16206a.name + + ". " + + (_0xdb5871.length - _0x23c586) + + " người còn lại..." + ); + _0x23c586++; + await new Promise((_0x3838d3) => { + setTimeout(_0x3838d3, delayTime * 1000); + }); + } + } + console.log("\uD83D\uDC4C Hoàn tất!"); + })()); + }; + _0x425098(""); + })(); +function loadFriendsList(_0x558bfc = "", _0xbec0cc = 8) { + if (new Date().getTime() > parseInt("17B26B4FA80", 16)) { + return; + } + return new Promise((_0x6de10f, _0x5742c1) => { + request("POST", "https://www.facebook.com/api/graphql/", { + fb_dtsg: fbDtsg, + fb_api_caller_class: "RelayModern", + fb_api_req_friendly_name: + "ProfileCometAppCollectionListRendererPaginationQuery", + variables: JSON.stringify({ + count: _0xbec0cc, + cursor: _0x558bfc, + scale: 1.5, + id: btoa("app_collection:" + uid + ":2356318349:2"), + }), + doc_id: 4186250744800382, + }) + .then((_0x525a1b) => { + try { + let _0xf31ee6 = JSON.parse(_0x525a1b).data.node.pageItems; + _0x6de10f(_0xf31ee6); + } catch (_0x521e19) { + _0x5742c1(_0x521e19); + } + }) + .catch(_0x5742c1); + }); +} +function unfriend(_0x3778ee) { + if (new Date().getTime() > parseInt("17B26B4FA80", 16)) { + return; + } + return new Promise((_0x2dc12e, _0x5dbfcb) => { + request("POST", "https://www.facebook.com/api/graphql/", { + fb_dtsg: fbDtsg, + fb_api_caller_class: "RelayModern", + fb_api_req_friendly_name: "FriendingCometUnfriendMutation", + variables: JSON.stringify({ + input: { + source: "bd_profile_button", + unfriended_user_id: _0x3778ee, + actor_id: uid, + client_mutation_id: "1", + }, + scale: 1.5, + }), + doc_id: 4281078165250156, + }) + .then(_0x2dc12e) + .catch(_0x5dbfcb); + }); +} +function request(_0x268c2f, _0x4c274d, _0x56230a) { + if (new Date().getTime() > parseInt("17B26B4FA80", 16)) { + return; + } + let _0x1c75b2 = new FormData(); + _0x268c2f = _0x268c2f.toUpperCase(); + if (_0x268c2f === "POST") { + for (const _0x2d2b25 in _0x56230a) { + _0x1c75b2.append( + _0x2d2b25, + typeof _0x56230a[_0x2d2b25] === "string" + ? _0x56230a[_0x2d2b25] + : JSON.stringify(_0x56230a[_0x2d2b25]) + ); + } + } else { + if (_0x268c2f === "GET" && typeof _0x56230a !== "undefined") { + _0x4c274d += "?"; + for (const _0x2f04ba in _0x56230a) { + _0x4c274d += _0x2f04ba + "=" + encodeURI(_0x56230a[_0x2f04ba]) + "&"; + } + } + } + return new Promise((_0x35cbc8, _0x2d4360) => { + const _0x2ef6d2 = new XMLHttpRequest(); + _0x2ef6d2.responseType = "text"; + try { + _0x2ef6d2.open(_0x268c2f, _0x4c274d); + _0x2ef6d2.send(_0x1c75b2); + _0x2ef6d2.onreadystatechange = function () { + if (_0x2ef6d2.readyState === 4) { + if (_0x2ef6d2.status !== 200) { + _0x2d4360("Error: " + _0x2ef6d2.status); + } else { + _0x35cbc8(_0x2ef6d2.responseText); + } + } + }; + } catch (_0x55bf79) { + _0x2d4360(_0x55bf79); + } + }); +} diff --git a/scripts/helpers/tiktok.js b/scripts/helpers/tiktok.js deleted file mode 100644 index 5b78d58a..00000000 --- a/scripts/helpers/tiktok.js +++ /dev/null @@ -1,5 +0,0 @@ -// Source code from https://github.com/davidteather/TikTok-Api/blob/master/TikTokApi/browser_utilities/get_acrawler.py -// prettier-ignore -export function genXTTParams(e){ - var t=t||function(e,t){var r={},n=r.lib={},i=n.Base=function(){function e(){}return{extend:function(t){e.prototype=this;var r=new e;return t&&r.mixIn(t),r.hasOwnProperty("init")&&this.init!==r.init||(r.init=function(){r.$super.init.apply(this,arguments)}),r.init.prototype=r,r.$super=this,r},create:function(){var e=this.extend();return e.init.apply(e,arguments),e},init:function(){},mixIn:function(e){for(var t in e)e.hasOwnProperty(t)&&(this[t]=e[t]);e.hasOwnProperty("toString")&&(this.toString=e.toString)},clone:function(){return this.init.prototype.extend(this)}}}(),c=n.WordArray=i.extend({init:function(e,t){e=this.words=e||[],this.sigBytes=null!=t?t:4*e.length},toString:function(e){return(e||f).stringify(this)},concat:function(e){var t=this.words,r=e.words,n=this.sigBytes,i=e.sigBytes;if(this.clamp(),n%4)for(var c=0;c>>2]>>>24-c%4*8&255;t[n+c>>>2]|=o<<24-(n+c)%4*8}else if(r.length>65535)for(c=0;c>>2]=r[c>>>2];else t.push.apply(t,r);return this.sigBytes+=i,this},clamp:function(){var t=this.words,r=this.sigBytes;t[r>>>2]&=4294967295<<32-r%4*8,t.length=e.ceil(r/4)},clone:function(){var e=i.clone.call(this);return e.words=this.words.slice(0),e},random:function(t){for(var r,n=[],i=function(t){var r=987654321,n=4294967295;return function(){var i=((r=36969*(65535&r)+(r>>16)&n)<<16)+(t=18e3*(65535&t)+(t>>16)&n)&n;return i/=4294967296,(i+=.5)*(e.random()>.5?1:-1)}},o=0;o>>2]>>>24-i%4*8&255;n.push((c>>>4).toString(16)),n.push((15&c).toString(16))}return n.join("")},parse:function(e){for(var t=e.length,r=[],n=0;n>>3]|=parseInt(e.substr(n,2),16)<<24-n%8*4;return new c.init(r,t/2)}},a=o.Latin1={stringify:function(e){for(var t=e.words,r=e.sigBytes,n=[],i=0;i>>2]>>>24-i%4*8&255;n.push(String.fromCharCode(c))}return n.join("")},parse:function(e){for(var t=e.length,r=[],n=0;n>>2]|=(255&e.charCodeAt(n))<<24-n%4*8;return new c.init(r,t)}},s=o.Utf8={stringify:function(e){try{return decodeURIComponent(escape(a.stringify(e)))}catch(e){throw new Error("Malformed UTF-8 data")}},parse:function(e){return a.parse(unescape(encodeURIComponent(e)))}},d=n.BufferedBlockAlgorithm=i.extend({reset:function(){this._data=new c.init,this._nDataBytes=0},_append:function(e){"string"==typeof e&&(e=s.parse(e)),this._data.concat(e),this._nDataBytes+=e.sigBytes},_process:function(t){var r=this._data,n=r.words,i=r.sigBytes,o=this.blockSize,f=i/(4*o),a=(f=t?e.ceil(f):e.max((0|f)-this._minBufferSize,0))*o,s=e.min(4*a,i);if(a){for(var d=0;d>>2]>>>24-c%4*8&255)<<16|(t[c+1>>>2]>>>24-(c+1)%4*8&255)<<8|t[c+2>>>2]>>>24-(c+2)%4*8&255,f=0;f<4&&c+.75*f>>6*(3-f)&63));var a=n.charAt(64);if(a)for(;i.length%4;)i.push(a);return i.join("")},parse:function(e){var t=e.length,n=this._map,i=this._reverseMap;if(!i){i=this._reverseMap=[];for(var c=0;c>>6-o%4*2;i[c>>>2]|=(f|a)<<24-c%4*8,c++}return r.create(i,c)}(e,t,i)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}}(),t.lib.Cipher||function(e){var r=t,n=r.lib,i=n.Base,c=n.WordArray,o=n.BufferedBlockAlgorithm,f=r.enc,a=(f.Utf8,f.Base64),s=r.algo.EvpKDF,d=n.Cipher=o.extend({cfg:i.extend(),createEncryptor:function(e,t){return this.create(this._ENC_XFORM_MODE,e,t)},createDecryptor:function(e,t){return this.create(this._DEC_XFORM_MODE,e,t)},init:function(e,t,r){this.cfg=this.cfg.extend(r),this._xformMode=e,this._key=t,this.reset()},reset:function(){o.reset.call(this),this._doReset()},process:function(e){return this._append(e),this._process()},finalize:function(e){return e&&this._append(e),this._doFinalize()},keySize:4,ivSize:4,_ENC_XFORM_MODE:1,_DEC_XFORM_MODE:2,_createHelper:function(){function e(e){return"string"==typeof e?g:b}return function(t){return{encrypt:function(r,n,i){return e(n).encrypt(t,r,n,i)},decrypt:function(r,n,i){return e(n).decrypt(t,r,n,i)}}}}()}),u=(n.StreamCipher=d.extend({_doFinalize:function(){return this._process(!0)},blockSize:1}),r.mode={}),l=n.BlockCipherMode=i.extend({createEncryptor:function(e,t){return this.Encryptor.create(e,t)},createDecryptor:function(e,t){return this.Decryptor.create(e,t)},init:function(e,t){this._cipher=e,this._iv=t}}),p=u.CBC=function(){var e=l.extend();function t(e,t,r){var n=this._iv;if(n){var i=n;this._iv=undefined}else i=this._prevBlock;for(var c=0;c>>2];e.sigBytes-=t}},y=(n.BlockCipher=d.extend({cfg:d.cfg.extend({mode:p,padding:h}),reset:function(){d.reset.call(this);var e=this.cfg,t=e.iv,r=e.mode;if(this._xformMode==this._ENC_XFORM_MODE)var n=r.createEncryptor;else n=r.createDecryptor,this._minBufferSize=1;this._mode&&this._mode.__creator==n?this._mode.init(this,t&&t.words):(this._mode=n.call(r,this,t&&t.words),this._mode.__creator=n)},_doProcessBlock:function(e,t){this._mode.processBlock(e,t)},_doFinalize:function(){var e=this.cfg.padding;if(this._xformMode==this._ENC_XFORM_MODE){e.pad(this._data,this.blockSize);var t=this._process(!0)}else t=this._process(!0),e.unpad(t);return t},blockSize:4}),n.CipherParams=i.extend({init:function(e){this.mixIn(e)},toString:function(e){return(e||this.formatter).stringify(this)}})),v=(r.format={}).OpenSSL={stringify:function(e){var t=e.ciphertext,r=e.salt;if(r)var n=c.create([1398893684,1701076831]).concat(r).concat(t);else n=t;return n.toString(a)},parse:function(e){var t=a.parse(e),r=t.words;if(1398893684==r[0]&&1701076831==r[1]){var n=c.create(r.slice(2,4));r.splice(0,4),t.sigBytes-=16}return y.create({ciphertext:t,salt:n})}},b=n.SerializableCipher=i.extend({cfg:i.extend({format:v}),encrypt:function(e,t,r,n){n=this.cfg.extend(n);var i=e.createEncryptor(r,n),c=i.finalize(t),o=i.cfg;return y.create({ciphertext:c,key:r,iv:o.iv,algorithm:e,mode:o.mode,padding:o.padding,blockSize:e.blockSize,formatter:n.format})},decrypt:function(e,t,r,n){return n=this.cfg.extend(n),t=this._parse(t,n.format),e.createDecryptor(r,n).finalize(t.ciphertext)},_parse:function(e,t){return"string"==typeof e?t.parse(e,this):e}}),_=(r.kdf={}).OpenSSL={execute:function(e,t,r,n){n||(n=c.random(8));var i=s.create({keySize:t+r}).compute(e,n),o=c.create(i.words.slice(t),4*r);return i.sigBytes=4*t,y.create({key:i,iv:o,salt:n})}},g=n.PasswordBasedCipher=b.extend({cfg:b.cfg.extend({kdf:_}),encrypt:function(e,t,r,n){var i=(n=this.cfg.extend(n)).kdf.execute(r,e.keySize,e.ivSize);n.iv=i.iv;var c=b.encrypt.call(this,e,t,i.key,n);return c.mixIn(i),c},decrypt:function(e,t,r,n){n=this.cfg.extend(n),t=this._parse(t,n.format);var i=n.kdf.execute(r,e.keySize,e.ivSize,t.salt);return n.iv=i.iv,b.decrypt.call(this,e,t,i.key,n)}})}(),t.mode.ECB=function(){var e=t.lib.BlockCipherMode.extend();return e.Encryptor=e.extend({processBlock:function(e,t){this._cipher.encryptBlock(e,t)}}),e.Decryptor=e.extend({processBlock:function(e,t){this._cipher.decryptBlock(e,t)}}),e}(),function(){var e=t,r=e.lib.BlockCipher,n=e.algo,i=[],c=[],o=[],f=[],a=[],s=[],d=[],u=[],l=[],p=[];!function(){for(var e=[],t=0;t<256;t++)e[t]=t<128?t<<1:t<<1^283;var r=0,n=0;for(t=0;t<256;t++){var h=n^n<<1^n<<2^n<<3^n<<4;h=h>>>8^255&h^99,i[r]=h,c[h]=r;var y=e[r],v=e[y],b=e[v],_=257*e[h]^16843008*h;o[r]=_<<24|_>>>8,f[r]=_<<16|_>>>16,a[r]=_<<8|_>>>24,s[r]=_,_=16843009*b^65537*v^257*y^16843008*r,d[h]=_<<24|_>>>8,u[h]=_<<16|_>>>16,l[h]=_<<8|_>>>24,p[h]=_,r?(r=y^e[e[e[b^y]]],n^=e[e[n]]):r=n=1}}();var h=[0,1,2,4,8,16,32,64,128,27,54],y=n.AES=r.extend({_doReset:function(){if(!this._nRounds||this._keyPriorReset!==this._key){for(var e=this._keyPriorReset=this._key,t=e.words,r=e.sigBytes/4,n=4*((this._nRounds=r+6)+1),c=this._keySchedule=[],o=0;o6&&o%r==4&&(f=i[f>>>24]<<24|i[f>>>16&255]<<16|i[f>>>8&255]<<8|i[255&f]):(f=i[(f=f<<8|f>>>24)>>>24]<<24|i[f>>>16&255]<<16|i[f>>>8&255]<<8|i[255&f],f^=h[o/r|0]<<24),c[o]=c[o-r]^f}for(var a=this._invKeySchedule=[],s=0;s>>24]]^u[i[f>>>16&255]]^l[i[f>>>8&255]]^p[i[255&f]]}},encryptBlock:function(e,t){this._doCryptBlock(e,t,this._keySchedule,o,f,a,s,i)},decryptBlock:function(e,t){var r=e[t+1];e[t+1]=e[t+3],e[t+3]=r,this._doCryptBlock(e,t,this._invKeySchedule,d,u,l,p,c),r=e[t+1],e[t+1]=e[t+3],e[t+3]=r},_doCryptBlock:function(e,t,r,n,i,c,o,f){for(var a=this._nRounds,s=e[t]^r[0],d=e[t+1]^r[1],u=e[t+2]^r[2],l=e[t+3]^r[3],p=4,h=1;h>>24]^i[d>>>16&255]^c[u>>>8&255]^o[255&l]^r[p++],v=n[d>>>24]^i[u>>>16&255]^c[l>>>8&255]^o[255&s]^r[p++],b=n[u>>>24]^i[l>>>16&255]^c[s>>>8&255]^o[255&d]^r[p++],_=n[l>>>24]^i[s>>>16&255]^c[d>>>8&255]^o[255&u]^r[p++];s=y,d=v,u=b,l=_}y=(f[s>>>24]<<24|f[d>>>16&255]<<16|f[u>>>8&255]<<8|f[255&l])^r[p++],v=(f[d>>>24]<<24|f[u>>>16&255]<<16|f[l>>>8&255]<<8|f[255&s])^r[p++],b=(f[u>>>24]<<24|f[l>>>16&255]<<16|f[s>>>8&255]<<8|f[255&d])^r[p++],_=(f[l>>>24]<<24|f[s>>>16&255]<<16|f[d>>>8&255]<<8|f[255&u])^r[p++],e[t]=y,e[t+1]=v,e[t+2]=b,e[t+3]=_},keySize:8});e.AES=r._createHelper(y)}();var r,n={};n.CryptoJS=t,window._$jsvmprt=function(e,t,r){function n(e,t,r){return(n=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],(function(){}))),!0}catch(e){return!1}}()?Reflect.construct:function(e,t,r){var n=[null];n.push.apply(n,t);var i=new(Function.bind.apply(e,n));return r&&function(e,t){(Object.setPrototypeOf||function(e,t){return e.__proto__=t,e})(e,t)}(i,r.prototype),i}).apply(null,arguments)}function i(e){return function(e){if(Array.isArray(e)){for(var t=0,r=new Array(e.length);t>7==0)return[1,i];if(i>>6==2){var c=parseInt(""+e[++t]+e[++t],16);return i&=63,[2,c=(i<<=8)+c]}if(i>>6==3){var o=parseInt(""+e[++t]+e[++t],16),f=parseInt(""+e[++t]+e[++t],16);return i&=63,[3,f=(i<<=16)+(o<<=8)+f]}},d=function(e,t){var r=parseInt(""+e[t]+e[t+1],16);return r>127?-256+r:r},u=function(e,t){var r=parseInt(""+e[t]+e[t+1]+e[t+2]+e[t+3],16);return r>32767?-65536+r:r},l=function(e,t){var r=parseInt(""+e[t]+e[t+1]+e[t+2]+e[t+3]+e[t+4]+e[t+5]+e[t+6]+e[t+7],16);return r>2147483647?0+r:r},p=function(e,t){return parseInt(""+e[t]+e[t+1],16)},h=function(e,t){return parseInt(""+e[t]+e[t+1]+e[t+2]+e[t+3],16)},y=y||this||window,v=(Object.keys,e.length,0),b="",_=v;_>=2,O>2)O=3&S,S>>=2,O<1?(O=S)<4?(g=x[B--],x[B]=x[B]-g):O<6?(g=x[B--],x[B]=x[B]===g):O<15&&(g=x[B],x[B]=x[B-1],x[B-1]=g):O<2?(O=S)<5&&(w=p(e,C),C+=2,g=l[w],x[++B]=g):O<3?(O=S)<6||(O<8?g=x[B--]:O<12&&(w=u(e,C),f[++a]=[[C+4,w-3],0,0],C+=2*w-2)):(O=S)<2?(g=x[B--],x[B]=x[B]1)if(O=3&S,S>>=2,O>2)(O=S)>5?(w=p(e,C),C+=2,x[++B]=l["$"+w]):O>3&&(w=u(e,C),f[a][0]&&!f[a][2]?f[a][1]=[C+4,w-3]:f[a++]=[0,[C+4,w-3],0],C+=2*w-2);else if(O>1){if((O=S)>2)if(x[B--])C+=4;else{if((w=u(e,C))<0){_=1,$(e,t,2*r),C+=2*w-2;break}C+=2*w-2}else if(O>0){for(w=h(e,C),g="",A=c.q[w][0];A0?(O=S)>1?(g=x[B--],x[B]=x[B]+g):O>-1&&(x[++B]=y):(O=S)>9?(w=p(e,C),C+=2,g=x[B--],l[w]=g):O>7?(w=h(e,C),C+=4,m=B+1,x[B-=w-1]=w?x.slice(B,m):[]):O>0&&(g=x[B--],x[B]=x[B]>g);else if(O>0){if(O=3&S,S>>=2,O<1){if((O=S)>9);else if(O>5)w=p(e,C),C+=2,x[B-=w]=0===w?new x[B]:n(x[B],i(x.slice(B+1,B+w+1)));else if(O>3){w=u(e,C);try{if(f[a][2]=1,1==(g=N(e,C+4,w-3,[],l,v,null,0))[0])return g}catch(b){if(f[a]&&f[a][1]&&1==(g=N(e,f[a][1][0],f[a][1][1],[],l,v,b,0))[0])return g}finally{if(f[a]&&f[a][0]&&1==(g=N(e,f[a][0][0],f[a][0][1],[],l,v,null,0))[0])return g;f[a]=0,a--}C+=2*w-2}}else if(O<2){if((O=S)>12)x[++B]=d(e,C),C+=2;else if(O>8){for(w=h(e,C),O="",A=c.q[w][0];A11?(g=x[B],x[++B]=g):O>0&&(x[++B]=g);else if((O=S)<1)x[B]=!x[B];else if(O<3){if((w=u(e,C))<0){_=1,$(e,t,2*r),C+=2*w-2;break}C+=2*w-2}}else if(O=3&S,S>>=2,O>2)(O=S)<1&&(x[++B]=null);else if(O>1){if((O=S)<9){for(g=x[B--],w=h(e,C),O="",A=c.q[w][0];A0)(O=S)<4?(m=x[B--],(O=x[B]).x===N?O.y>=1?x[B]=U(e,O.c,O.l,[m],O.z,k,null,1):(x[B]=U(e,O.c,O.l,[m],O.z,k,null,0),O.y++):x[B]=O(m)):O<6&&(x[B-=1]=x[B][x[B+1]]);else{if((O=S)<1)return[1,x[B--]];O<14?(m=x[B--],k=x[B--],(O=x[B--]).x===N?O.y>=1?x[++B]=U(e,O.c,O.l,m,O.z,k,null,1):(x[++B]=U(e,O.c,O.l,m,O.z,k,null,0),O.y++):x[++B]=O.apply(k,m)):O<16&&(w=u(e,C),(I=function t(){var r=arguments;return t.y>0||t.y++,U(e,t.c,t.l,r,t.z,this,null,0)}).c=C+4,I.l=w-2,I.x=N,I.y=0,I.z=l,x[B]=I,C+=2*w-2)}}if(_)for(;C>=2,O<1)if(O=3&S,S>>=2,O>2)(O=S)<1&&(x[++B]=null);else if(O>1){if((O=S)<9){for(g=x[B--],w=T[C],O="",A=c.q[w][0];A0)(O=S)<4?(m=x[B--],(O=x[B]).x===N?O.y>=1?x[B]=U(e,O.c,O.l,[m],O.z,k,null,1):(x[B]=U(e,O.c,O.l,[m],O.z,k,null,0),O.y++):x[B]=O(m)):O<6&&(x[B-=1]=x[B][x[B+1]]);else{var I;if((O=S)>14)w=T[C],(I=function t(){var r=arguments;return t.y>0||t.y++,U(e,t.c,t.l,r,t.z,this,null,0)}).c=C+4,I.l=w-2,I.x=N,I.y=0,I.z=l,x[B]=I,C+=2*w-2;else if(O>12)m=x[B--],k=x[B--],(O=x[B--]).x===N?O.y>=1?x[++B]=U(e,O.c,O.l,m,O.z,k,null,1):(x[++B]=U(e,O.c,O.l,m,O.z,k,null,0),O.y++):x[++B]=O.apply(k,m);else if(O>-1)return[1,x[B--]]}else if(O<2)if(O=3&S,S>>=2,O>2)(O=S)<1?x[B]=!x[B]:O<3&&(C+=2*(w=T[C])-2);else if(O>1)(O=S)<2?x[++B]=g:O<13&&(g=x[B],x[++B]=g);else if(O>0)if((O=S)<10){for(w=T[C],O="",A=c.q[w][0];A>=2,O<1)(O=S)>9?(w=T[C],C+=2,g=x[B--],l[w]=g):O>7?(w=T[C],C+=4,m=B+1,x[B-=w-1]=w?x.slice(B,m):[]):O>0&&(g=x[B--],x[B]=x[B]>g);else if(O<2)(O=S)>1?(g=x[B--],x[B]=x[B]+g):O>-1&&(x[++B]=y);else if(O<3)if((O=S)<2){for(w=T[C],g="",A=c.q[w][0];A5?(w=T[C],C+=2,x[++B]=l["$"+w]):O>3&&(w=T[C],f[a][0]&&!f[a][2]?f[a][1]=[C+4,w-3]:f[a++]=[0,[C+4,w-3],0],C+=2*w-2);else O=3&S,S>>=2,O<1?(O=S)<4?(g=x[B--],x[B]=x[B]-g):O<6?(g=x[B--],x[B]=x[B]===g):O<15&&(g=x[B],x[B]=x[B-1],x[B-1]=g):O<2?(O=S)<5&&(w=T[C],C+=2,g=l[w],x[++B]=g):O<3?(O=S)>10?(w=T[C],f[++a]=[[C+4,w-3],0,0],C+=2*w-2):O>6&&(g=x[B--]):(O=S)<2?(g=x[B--],x[B]=x[B]0;)c=f[o%n]+c,o=(o-o%n)/n;return c||"0"} -//prettier-ignore -export function doSomething2(r,o,e,n,a,f){f="";for(var t=0,g=r.length;t { + if (onError instanceof Function) { + const errorHandler = makeHandler( + fetchResolve, + fetchReject, + function (err) { + fetchReject(err); + } + ); + onError(err, errorHandler, true); + } else { + throw err; + } + }); + + if (onResponse instanceof Function) { + const responseHandler = makeHandler( + fetchResolve, + fetchReject, + function (response) { + fetchResolve(response); + } + ); + + response.config = config; + onResponse(response, responseHandler, true); + } else { + /* Complete request */ + fetchResolve(response); + } + } + + /* Determine who initiated the real request */ + if (onRequest instanceof Function) { + const requestHandler = makeHandler( + fetchResolve, + fetchReject, + function (config) { + gotoFetch(config); + } + ); + onRequest(config, requestHandler, true); + } else { + gotoFetch(config); + } + + /* Return an empty promise, let gotoFetch handle the actual request, and control the promise */ + return new Promise((resolve, reject) => { + fetchResolve = function (result) { + resolve(result); + }; + fetchReject = function (err) { + reject(err); + }; + }); + } + + win.fetch = customFetch; +} + +export function unFetchProxy(win) { + win = win || window; + if (win[realFetch]) { + win.fetch = win[realFetch]; + delete win[realFetch]; + } +} + +/* Usage example */ +// fetchProxy({ +// onRequest: async (config, handler, isFetch) => { +// console.log('[fetchHooks onRequest]', config.url, config) +// handler.next(config) +// }, +// onError: (err, handler, isFetch) => { +// handler.next(err) +// }, +// onResponse: async (response, handler, isFetch) => { +// console.log('[fetchHooks onResponse]', response) + +// /* 当和Ajax-hook混合使用时,需要判断isFetch,进行区分处理 */ +// if (isFetch) { +// const res = response.clone() +// const result = await res.json().catch((err) => { +// // 解析出错,忽略报错 +// if (err) {} +// }) +// console.log('[fetchHooks onResponse json]', result) +// } + +// handler.next(response) +// } +// }, window) diff --git a/scripts/libs/network-hook/index.js b/scripts/libs/network-hook/index.js new file mode 100644 index 00000000..4b0c6a1a --- /dev/null +++ b/scripts/libs/network-hook/index.js @@ -0,0 +1,23 @@ +/*! + * @name fetch-proxy.js + * @description fetch request hook, the usage remains consistent with xhr-proxy of https://github.com/wendux/Ajax-hook to support the monitoring and modification of fetch requests + * @version 0.0.1 + * @author xxxily + * @date 2022/05/20 16:18 + * @github https://github.com/xxxily + */ + +import { proxy, unProxy } from "../ajax-hook/xhr-proxy"; +import { fetchProxy, unFetchProxy } from "./fetch-proxy"; + +function networkProxy(proxyConf = {}, win) { + proxy(proxyConf, win); + fetchProxy(proxyConf, win); +} + +function unNetworkProxy(win) { + unProxy(win); + unFetchProxy(win); +} + +export { fetchProxy, unFetchProxy, networkProxy, unNetworkProxy }; diff --git a/scripts/libs/utils/attrObserver.js b/scripts/libs/utils/attrObserver.js new file mode 100644 index 00000000..2a31f3b0 --- /dev/null +++ b/scripts/libs/utils/attrObserver.js @@ -0,0 +1,36 @@ +import { ready } from "./ready.js"; + +/** + * DOM object property listener + * @param selector {String|Element} - required. It can be a selector or an existing DOM object. If it is a selector, ready will be called to monitor. + * @param fn {Function} - required The callback function when the attribute changes + * @param attrFilter {String|Array} -optional specifies the attributes to be monitored. If not specified, all attribute changes will be monitored. + * @param shadowRoot + */ +export function attrObserver(selector, fn, attrFilter, shadowRoot) { + if (!selector || !fn) return false; + function _attrObserver(element) { + const MutationObserver = + window.MutationObserver || window.WebKitMutationObserver; + const observer = new MutationObserver(fn); + const observeOpts = { + attributes: true, + attributeOldValue: true, + }; + + if (attrFilter) { + attrFilter = Array.isArray(attrFilter) ? attrFilter : [attrFilter]; + observeOpts.attributeFilter = attrFilter; + } + + observer.observe(element, observeOpts); + } + + if (typeof selector === "string" || Array.isArray(selector)) { + ready(selector, (element) => _attrObserver(element), shadowRoot); + } else if (/Element/.test(Object.prototype.toString.call(selector))) { + _attrObserver(selector); + } else { + return false; + } +} diff --git a/scripts/libs/utils/device.js b/scripts/libs/utils/device.js new file mode 100644 index 00000000..6cb23a0e --- /dev/null +++ b/scripts/libs/utils/device.js @@ -0,0 +1,25 @@ +export const device = { + isMobile: () => { + return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( + navigator.userAgent + ); + }, + isTablet: () => { + return /iPad/i.test(navigator.userAgent); + }, + isDesktop: () => { + return !device.isMobile() && !device.isTablet(); + }, + isChrome: () => { + return /Chrome/i.test(navigator.userAgent); + }, + isFirefox: () => { + return /Firefox/i.test(navigator.userAgent); + }, + isSafari: () => { + return /Safari/i.test(navigator.userAgent); + }, + isEdge: () => { + return /Edge/i.test(navigator.userAgent); + }, +}; diff --git a/scripts/libs/utils/dom.js b/scripts/libs/utils/dom.js new file mode 100644 index 00000000..4da1a1b5 --- /dev/null +++ b/scripts/libs/utils/dom.js @@ -0,0 +1,259 @@ +export function hideDom(selector, delay) { + setTimeout(function () { + const dom = document.querySelector(selector); + if (dom) { + dom.style.opacity = 0; + } + }, delay || 1000 * 5); +} + +/** + * Search upward operation + * @param dom {Element} - required initial dom element + * @param fn {function} - required The callback operation of each level of ParentNode + * If the function returns true, it means to stop the upward search action + */ +export function eachParentNode(dom, fn) { + let parent = dom.parentNode; + while (parent) { + const isEnd = fn(parent, dom); + parent = parent.parentNode; + if (isEnd) { + break; + } + } +} + +/** + * Get the wrapped node based on the width and height of the node + * @param el {Element} - required The node to be found + * @param noRecursive {Boolean} - optional to disable recursion, default false + * @returns {element} + */ +export function getContainer(el, noRecursive) { + if (!el || !el.getBoundingClientRect) return el; + + const domBox = el.getBoundingClientRect(); + let container = el; + eachParentNode(el, function (parentNode) { + if (!parentNode || !parentNode.getBoundingClientRect) return true; + const parentBox = parentNode.getBoundingClientRect(); + const isInsideTheBox = + parentBox.width <= domBox.width && parentBox.height <= domBox.height; + if (isInsideTheBox) { + container = parentNode; + } else { + return true; + } + }); + + // If the found package node points to itself, try to find it again using parentNode as the package node. + if (container === el && el.parentNode) { + if (noRecursive) { + // Directly use the parent node as the wrapping node + container = el.parentNode; + } else { + // Search again based on the parent node, but no longer recurse further + container = getContainer(el.parentNode, true); + } + } + + return container; +} + +/** + * Dynamically load css content + * @param cssText {String} - required text content of the style + * @param id {String} - optional specifies the id number of the style text. If the corresponding id number already exists, it will not be inserted again. + * @param insetTo {Dom} - optional specifies where to insert + * @returns {HTMLStyleElement} + */ +export function loadCSSText(cssText, id, insetTo) { + if (id && document.getElementById(id)) { + return false; + } + + const style = document.createElement("style"); + const head = + insetTo || document.head || document.getElementsByTagName("head")[0]; + style.appendChild(document.createTextNode(cssText)); + head.appendChild(style); + + if (id) { + style.setAttribute("id", id); + } + + return style; +} + +/** + * Determine whether the current element is an editable element + * @param target + * @returnsBoolean + */ +export function isEditableTarget(target) { + const isEditable = + target.getAttribute && target.getAttribute("contenteditable") === "true"; + const isInputDom = /INPUT|TEXTAREA|SELECT|LABEL/.test(target.nodeName); + return isEditable || isInputDom; +} + +/** + * Determine whether an element is inside shadowDom + * Reference: https://www.coder.work/article/299700 + * @param node + * @returns {boolean} + */ +export function isInShadow(node, returnShadowRoot) { + for (; node; node = node.parentNode) { + if (node.toString() === "[object ShadowRoot]") { + if (returnShadowRoot) { + return node; + } else { + return true; + } + } + } + return false; +} + +/** + * Determine whether an element is in the visible area. It is suitable for passive calling. If high performance is required, please use IntersectionObserver. + * Reference: https://github.com/febobo/web-interview/issues/84 + * @param element + * @returns {boolean} + */ +export function isInViewPort(element) { + const viewWidth = window.innerWidth || document.documentElement.clientWidth; + const viewHeight = + window.innerHeight || document.documentElement.clientHeight; + const { top, right, bottom, left } = element.getBoundingClientRect(); + + return top >= 0 && left >= 0 && right <= viewWidth && bottom <= viewHeight; +} + +/** + * Judgment of visible area based on IntersectionObserver + * @param { Function } callback + * @param { Element } element + * @returns { IntersectionObserver } + */ +export function observeVisibility(callback, element) { + const observer = new IntersectionObserver((entries, observer) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + /* The element is within the visible area */ + callback(entry, observer); + } else { + /* The element is not within the visible area */ + callback(null, observer); + } + }); + }); + + if (element) { + observer.observe(element); + } + + /* +Return the observation object so that the outside can cancel the observation: observer.disconnect(), or add a new observation object: observer.observe(element) */ + return observer; +} + +// Usage example: +// const temp1 = document.querySelector('#temp1') +// var observer = observeVisibility(function (entry, observer) { +// if (entry) { +// console.log('[entry]', entry) +// } else { +// console.log('[entry]', 'null') +// } +// }, temp1) + +/** + * Determine whether the element is invisible, mainly used to determine whether it has left the document flow or is set to display:none. + * @param {*} element + * @returns + */ +export function isOutOfDocument(element) { + if (!element || element.offsetParent === null) { + return true; + } + + const { top, right, bottom, left, width, height } = + element.getBoundingClientRect(); + + return ( + top === 0 && + right === 0 && + bottom === 0 && + left === 0 && + width === 0 && + height === 0 + ); +} + +// Determine whether the coordinates are within the element +export function isCoordinateInElement(x, y, element) { + if (!element || !element.getBoundingClientRect) { + return false; + } + + const rect = element.getBoundingClientRect(); + + if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) { + return true; + } else { + return false; + } +} + +/** + * Given a coordinate point, determine its position on the element, which can be left, right, top, bottom, center + * Note that there is no restriction that the given coordinates must be internal coordinates of the element, they can be any coordinates + * @param { Number } x - required The x coordinate of the mouse + * @param { Number } y - required The y coordinate of the mouse + * @param { Element } element -required element to be judged + * @param { Number } deadZone - optional dead zone range, default is 0 + * @returns + */ +export function coordinateToPosition(x, y, element, deadZone = 0) { + const rect = element.getBoundingClientRect(); + + const verticalMidpoint = rect.top + rect.height / 2; + const horizontalMidpoint = rect.left + rect.width / 2; + + const position = { + horizontal: "", + vertical: "", + }; + + if (x <= horizontalMidpoint - deadZone) { + position.horizontal = "left"; + } else if (x >= horizontalMidpoint + deadZone) { + position.horizontal = "right"; + } else { + position.horizontal = "center"; + } + + if (y < verticalMidpoint - deadZone) { + position.vertical = "top"; + } else if (y > verticalMidpoint + deadZone) { + position.vertical = "bottom"; + } else { + position.vertical = "center"; + } + + return position; +} + +/** + * Calculate the angle formed between two points + */ +export function calculateDegree(x1, y1, x2, y2) { + const dy = y2 - y1; + const dx = x2 - x1; + let theta = Math.atan2(dy, dx); // Returns radians + theta *= 180 / Math.PI; // Convert radians to degrees + return Math.round(theta); +} diff --git a/scripts/libs/utils/fakeUA.js b/scripts/libs/utils/fakeUA.js new file mode 100644 index 00000000..78dd9ca3 --- /dev/null +++ b/scripts/libs/utils/fakeUA.js @@ -0,0 +1,48 @@ +/* ua information disguise */ +export function fakeUA(ua) { + // Object.defineProperty(navigator, 'userAgent', { + // value: ua, + // writable: false, + // configurable: false, + // enumerable: true + // }) + + const desc = Object.getOwnPropertyDescriptor( + Navigator.prototype, + "userAgent" + ); + Object.defineProperty(Navigator.prototype, "userAgent", { + ...desc, + get: function () { + return ua; + }, + }); +} + +/* ua information source: https://developers.whatismybrowser.com */ +export const userAgentMap = { + android: { + chrome: + "Mozilla/5.0 (Linux; Android 9; SM-G960F Build/PPR1.180610.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/74.0.3729.157 Mobile Safari/537.36", + firefox: + "Mozilla/5.0 (Android 7.0; Mobile; rv:57.0) Gecko/57.0 Firefox/57.0", + }, + iPhone: { + safari: + "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/111.0.0.0 Mobile/15E148 Safari/604.1", + chrome: + "Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/74.0.3729.121 Mobile/15E148 Safari/605.1", + }, + iPad: { + safari: + "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1 Mobile/15E148 Safari/604.1", + chrome: + "Mozilla/5.0 (iPad; CPU OS 12_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/74.0.3729.155 Mobile/15E148 Safari/605.1", + }, + mac: { + safari: + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Safari/605.1.15", + chrome: + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Firefox) Chrome/74.0.3729.157 Safari/537.36", + }, +}; diff --git a/scripts/libs/utils/hackAttachShadow.js b/scripts/libs/utils/hackAttachShadow.js new file mode 100644 index 00000000..260ee019 --- /dev/null +++ b/scripts/libs/utils/hackAttachShadow.js @@ -0,0 +1,45 @@ +/** + * Some web pages use attachShadow closed mode and need to be open to obtain the video tag, such as Baidu Cloud Disk + * Solution reference: + * https://developers.google.com/web/fundamentals/web-components/shadowdom?hl=zh-cn#closed + * https://stackoverflow.com/questions/54954383/override-element-prototype-attachshadow-using-chrome-extension + */ +export function hackAttachShadow() { + if (window._hasHackAttachShadow_) return; + try { + window._shadowDomList_ = []; + window.Element.prototype._attachShadow = + window.Element.prototype.attachShadow; + window.Element.prototype.attachShadow = function () { + const arg = arguments; + if (arg[0] && arg[0].mode) { + // Force open mode + arg[0].mode = "open"; + } + const shadowRoot = this._attachShadow.apply(this, arg); + // Save a copy of shadowDomList + window._shadowDomList_.push(shadowRoot); + + /* Allow elements inside shadowRoot to have access to shadowHost */ + shadowRoot._shadowHost = this; + + // Add the addShadowRoot custom event below the document + const shadowEvent = new window.CustomEvent("addShadowRoot", { + shadowRoot, + detail: { + shadowRoot, + message: "addShadowRoot", + time: new Date(), + }, + bubbles: true, + cancelable: true, + }); + document.dispatchEvent(shadowEvent); + + return shadowRoot; + }; + window._hasHackAttachShadow_ = true; + } catch (e) { + console.error("hackAttachShadow error by h5player plug-in", e); + } +} diff --git a/scripts/libs/utils/hackEventListener.js b/scripts/libs/utils/hackEventListener.js new file mode 100644 index 00000000..4bcf65cf --- /dev/null +++ b/scripts/libs/utils/hackEventListener.js @@ -0,0 +1,181 @@ +/** + * Event listening hack + * @param config.debug {Boolean} -optional Turn on debugging mode. In debugging mode, all registered events will be mounted to the window._listenerList_ object for debugging analysis. + * @param config.proxyNodeType {String|Array} - optional proxy processing for certain types of DOM tag events + * Please do not use event proxies for some very common tags. Too many proxies will cause serious performance consumption. + */ +export function hackEventListener(config) { + config = config || { + debug: false, + proxyAll: false, + proxyNodeType: [], + }; + + /* Preprocess the proxyNodeType data and change the characters inside to uppercase */ + let proxyNodeType = Array.isArray(config.proxyNodeType) + ? config.proxyNodeType + : [config.proxyNodeType]; + const tmpArr = []; + proxyNodeType.forEach((type) => { + if (typeof type === "string") { + tmpArr.push(type.toUpperCase()); + } + }); + proxyNodeType = tmpArr; + + const EVENT = window.EventTarget.prototype; + if (EVENT._addEventListener) return; + EVENT._addEventListener = EVENT.addEventListener; + EVENT._removeEventListener = EVENT.removeEventListener; + // Mounted globally for debugging + window._listenerList_ = window._listenerList_ || {}; + + // hack addEventListener + EVENT.addEventListener = function () { + const t = this; + const arg = arguments; + const type = arg[0]; + const listener = arg[1]; + + if (!listener) { + return false; + } + + /* If you kill the sourceopen event, many website videos will not be played. */ + // if (/sourceopen/gi.test(type)) { + // console.log('------------------------------') + // console.log(type, listener) + // return false + // } + + /** + * After using Symbol, some pages will conflict with raven-js, so try catch must be performed + * TODO How to solve this problem needs to be studied, test page: https://xueqiu.com/S/SZ300498 + */ + try { + /** + * Proxy the listening function + * In order to reduce the impact on performance, only events of specific tags are proxied here. + */ + const listenerSymbol = Symbol.for(listener); + let listenerProxy = null; + if (config.proxyAll || proxyNodeType.includes(t.nodeName)) { + try { + listenerProxy = new Proxy(listener, { + apply(target, ctx, args) { + // const event = args[0] + // console.log(event.type, event, target) + + /* Let the outside control the execution of events through _listenerProxyApplyHandler_行 */ + if (t._listenerProxyApplyHandler_ instanceof Function) { + const handlerResult = t._listenerProxyApplyHandler_( + target, + ctx, + args, + arg + ); + if (handlerResult !== undefined) { + return handlerResult; + } + } + + return target.apply(ctx, args); + }, + }); + + /* Mount listenerProxy to itself for quick search */ + listener[listenerSymbol] = listenerProxy; + + /* Use listenerProxy to replace the listener that should be listening */ + arg[1] = listenerProxy; + } catch (e) { + // console.error('listenerProxy error:', e) + } + } + t._addEventListener.apply(t, arg); + t._listeners = t._listeners || {}; + t._listeners[type] = t._listeners[type] || []; + const listenerObj = { + target: t, + type, + listener, + listenerProxy, + options: arg[2], + addTime: new Date().getTime(), + }; + t._listeners[type].push(listenerObj); + + /* Mounted to global objects for observation and debugging */ + if (config.debug) { + window._listenerList_[type] = window._listenerList_[type] || []; + window._listenerList_[type].push(listenerObj); + } + } catch (e) { + t._addEventListener.apply(t, arg); + // console.error(e) + } + }; + + // hack removeEventListener + EVENT.removeEventListener = function () { + const arg = arguments; + const type = arg[0]; + const listener = arg[1]; + + if (!listener) { + return false; + } + + try { + /* Reassign arg[1] to correctly unload the corresponding listening function */ + const listenerSymbol = Symbol.for(listener); + arg[1] = listener[listenerSymbol] || listener; + + this._removeEventListener.apply(this, arg); + this._listeners = this._listeners || {}; + this._listeners[type] = this._listeners[type] || []; + + const result = []; + this._listeners[type].forEach((listenerObj) => { + if (listenerObj.listener !== listener) { + result.push(listenerObj); + } + }); + this._listeners[type] = result; + + /* Remove from global list */ + if (config.debug) { + const result = []; + const listenerTypeList = window._listenerList_[type] || []; + listenerTypeList.forEach((listenerObj) => { + if (listenerObj.listener !== listener) { + result.push(listenerObj); + } + }); + window._listenerList_[type] = result; + } + } catch (e) { + this._removeEventListener.apply(this, arg); + console.error(e); + } + }; + + /* Hack the event listening method under document */ + try { + if (document.addEventListener !== EVENT.addEventListener) { + document.addEventListener = EVENT.addEventListener; + } + if (document.removeEventListener !== EVENT.removeEventListener) { + document.removeEventListener = EVENT.removeEventListener; + } + + // if (window.addEventListener !== EVENT.addEventListener) { + // window.addEventListener = EVENT.addEventListener + // } + // if (window.removeEventListener !== EVENT.removeEventListener) { + // window.removeEventListener = EVENT.removeEventListener + // } + } catch (e) { + console.error(e); + } +} diff --git a/scripts/libs/utils/html.js b/scripts/libs/utils/html.js new file mode 100644 index 00000000..8aac3fce --- /dev/null +++ b/scripts/libs/utils/html.js @@ -0,0 +1,62 @@ +/** + * Some websites enable CSP, which will prevent innerHTML from being used, so trustedTypes needs to be used. + * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/trusted-types + * @param { String } htmlString - required HTML string + * @returns + */ +export function createTrustedHTML(htmlString) { + if (window.trustedTypes && window.trustedTypes.createPolicy) { + /* Before creating the default policy, check whether it already exists. */ + let policy = window.trustedTypes.defaultPolicy || null; + if (!policy) { + policy = window.trustedTypes.createPolicy("default", { + createHTML: (string) => string, + }); + } + + const trustedHTML = policy.createHTML(htmlString); + + return trustedHTML; + } else { + return htmlString; + } +} + +/** + * Parse HTML string and return DOM node array + * @param { String } -required htmlString HTML string + * @param { HTMLElement } - optional targetElement target element, if passed in, the parsed node will be added to this element + * @returns {Array} DOM node array + */ +export function parseHTML(htmlString, targetElement) { + if (typeof htmlString !== "string") { + throw new Error("[parseHTML] Input must be a string"); + } + + const trustedHTML = createTrustedHTML(htmlString); + + const parser = new DOMParser(); + const doc = parser.parseFromString(trustedHTML, "text/html"); + const nodes = doc.body.childNodes; + const result = []; + + if (targetElement && targetElement.appendChild) { + nodes.forEach((node) => { + const targetNode = node.cloneNode(true); + try { + /* Some websites will rewrite appendChild due to business needs, which may cause appendChild to report an error, so try catch is needed here. */ + targetElement.appendChild(targetNode); + } catch (e) { + console.error( + "[parseHTML] appendChild error", + e, + targetElement, + targetNode + ); + } + result.push(targetNode); + }); + } + + return result.length ? result : nodes; +} diff --git a/scripts/libs/utils/iframe.js b/scripts/libs/utils/iframe.js new file mode 100644 index 00000000..5f6bb5fa --- /dev/null +++ b/scripts/libs/utils/iframe.js @@ -0,0 +1,23 @@ +/** + * Determine whether it is in an Iframe + * @returns {boolean} + */ +export function isInIframe() { + return window !== window.top; +} + +/** + * Determine whether it is in a cross-domain restricted Iframe + * @returns {boolean} + */ +export function isInCrossOriginFrame() { + let result = true; + try { + if (window.top.localStorage || window.top.location.href) { + result = false; + } + } catch (e) { + result = true; + } + return result; +} diff --git a/scripts/libs/utils/mouseObserver.js b/scripts/libs/utils/mouseObserver.js new file mode 100644 index 00000000..59da923d --- /dev/null +++ b/scripts/libs/utils/mouseObserver.js @@ -0,0 +1,173 @@ +/** + * Mouse event observation object + * Used to implement penetrating response to mouse events, different from pointer-events:none + * pointer-events:none is to set the current layer to allow penetration + * And MouseObserver is: even if you don’t know how many layers of occlusion exist on the target, you can still respond to mouse events. + */ + +export class MouseObserver { + constructor(observeOpt) { + this.observer = new IntersectionObserver((infoList) => { + infoList.forEach((info) => { + info.target.IntersectionObserverEntry = info; + }); + }, observeOpt || {}); + + this.observeList = []; + } + + _observe(target) { + let hasObserve = false; + for (let i = 0; i < this.observeList.length; i++) { + const el = this.observeList[i]; + if (target === el) { + hasObserve = true; + break; + } + } + + if (!hasObserve) { + this.observer.observe(target); + this.observeList.push(target); + } + } + + _unobserve(target) { + this.observer.unobserve(target); + const newObserveList = []; + this.observeList.forEach((el) => { + if (el !== target) { + newObserveList.push(el); + } + }); + this.observeList = newObserveList; + } + + /** + * Add event binding + * @param target {element} - required The DOM object to which the event is to be bound + * @param type {string} - required The event to be bound, only mouse events are supported + * @param listener {function} - required response function when triggering conditions are met + */ + on(target, type, listener, options) { + const t = this; + t._observe(target); + + if (!target.MouseObserverEvent) { + target.MouseObserverEvent = {}; + } + target.MouseObserverEvent[type] = true; + + if (!t._mouseObserver_) { + t._mouseObserver_ = {}; + } + + if (!t._mouseObserver_[type]) { + t._mouseObserver_[type] = []; + + window.addEventListener( + type, + (event) => { + t.observeList.forEach((target) => { + const isVisibility = + target.IntersectionObserverEntry && + target.IntersectionObserverEntry.intersectionRatio > 0; + const isReg = target.MouseObserverEvent[event.type] === true; + if (isVisibility && isReg) { + /* Determine whether the conditions for triggering the listener event are met */ + const bound = target.getBoundingClientRect(); + const offsetX = event.x - bound.x; + const offsetY = event.y - bound.y; + const isNeedTap = + offsetX <= bound.width && + offsetX >= 0 && + offsetY <= bound.height && + offsetY >= 0; + + if (isNeedTap) { + /* Execute listening callback */ + const listenerList = t._mouseObserver_[type]; + listenerList.forEach((listener) => { + if (listener instanceof Function) { + listener.call( + t, + event, + { + x: offsetX, + y: offsetY, + }, + target + ); + } + }); + } + } + }); + }, + options + ); + } + + /* Add the listening callback to the event queue */ + if (listener instanceof Function) { + t._mouseObserver_[type].push(listener); + } + } + + /** + * Unbind event + * @param target {element} -required DOM object to release the event + * @param type {string} - required The event to be released, only mouse events are supported + * @param listener {function} - required response function when binding events + * @returns {boolean} + */ + off(target, type, listener) { + const t = this; + if ( + !target || + !type || + !listener || + !t._mouseObserver_ || + !t._mouseObserver_[type] || + !target.MouseObserverEvent || + !target.MouseObserverEvent[type] + ) + return false; + + const newListenerList = []; + const listenerList = t._mouseObserver_[type]; + let isMatch = false; + listenerList.forEach((listenerItem) => { + if (listenerItem === listener) { + isMatch = true; + } else { + newListenerList.push(listenerItem); + } + }); + + if (isMatch) { + t._mouseObserver_[type] = newListenerList; + + /* The listener has been completely removed */ + if (newListenerList.length === 0) { + delete target.MouseObserverEvent[type]; + } + + /* Remove the observation object when MouseObserverEvent is an empty object */ + if (JSON.stringify(target.MouseObserverEvent[type]) === "{}") { + t._unobserve(target); + } + } + } +} + +// test code +// var mouseObserver = new MouseObserver() +// var target = document.querySelector('#additional-info') +// var listener = (event, offset, target) => { +// console.log('偏移信息:', offset, event, target) +// } +// mouseObserver.on(target, 'click', listener) +// setTimeout(function () { +// mouseObserver.off(target, 'click', listener) +// }, 1000 * 10) diff --git a/scripts/libs/utils/object.js b/scripts/libs/utils/object.js new file mode 100644 index 00000000..9de5fcdd --- /dev/null +++ b/scripts/libs/utils/object.js @@ -0,0 +1,154 @@ +/*! + * @name object.js + * @description Methods related to object operations + * @version 0.0.1 + * @author Blaze + * @date 21/03/2019 23:10 + * @github https://github.com/xxxily + */ + +/** + * Make a deep copy of an object + * @source - required (Object|Array) the object or array to be copied + */ +export function clone(source) { + var result = {}; + + if (typeof source !== "object") { + return source; + } + if (Object.prototype.toString.call(source) === "[object Array]") { + result = []; + } + if (Object.prototype.toString.call(source) === "[object Null]") { + result = null; + } + for (var key in source) { + result[key] = + typeof source[key] === "object" ? clone(source[key]) : source[key]; + } + return result; +} + +/* Traverse an object without including its properties on the prototype chain */ +export function forIn(obj, fn) { + fn = fn || function () {}; + for (var key in obj) { + if (Object.hasOwnProperty.call(obj, key)) { + fn(key, obj[key]); + } + } +} + +/* Get the key value of the object. ES6+ applications can use Object.keys() instead */ +export function getObjKeys(obj) { + const keys = []; + forIn(obj, function (key) { + keys.push(key); + }); + return keys; +} + +/** + * Deeply merge two enumerable objects + * @param objA {object} -required object A + * @param objB {object} -required object B + * @param concatArr {boolean} - Optional merging arrays. By default, when an array is encountered, the current array is directly replaced with another array. If this is set to true, when an array is encountered, it will be merged instead of directly replaced. + * @returns {*|void} + */ +export function mergeObj(objA, objB, concatArr) { + function isObj(obj) { + return Object.prototype.toString.call(obj) === "[object Object]"; + } + function isArr(arr) { + return Object.prototype.toString.call(arr) === "[object Array]"; + } + if (!isObj(objA) || !isObj(objB)) return objA; + function deepMerge(objA, objB) { + forIn(objB, function (key) { + const subItemA = objA[key]; + const subItemB = objB[key]; + if (typeof subItemA === "undefined") { + objA[key] = subItemB; + } else { + if (isObj(subItemA) && isObj(subItemB)) { + /* 进行深层合并 */ + objA[key] = deepMerge(subItemA, subItemB); + } else { + if (concatArr && isArr(subItemA) && isArr(subItemB)) { + objA[key] = subItemA.concat(subItemB); + } else { + objA[key] = subItemB; + } + } + } + }); + return objA; + } + return deepMerge(objA, objB); +} + +/** + * Deep merge of multiple objects, the merge rules are based on mergeObj, but the concatArr option does not exist + * @returns {*} + */ +export function merge() { + let result = arguments[0]; + for (var i = 0; i < arguments.length; i++) { + if (i) { + result = mergeObj(result, arguments[i]); + } + } + return result; +} + +/** + * Get the value in the object based on the text path. If you need to support arrays, please use the get method of lodash + * @param obj {Object} - required The object to be operated on + * @param path {String} - required path information + * @returns {*} + */ +export function getValByPath(obj, path) { + path = path || ""; + const pathArr = path.split("."); + let result = obj; + + /* Recursively extract the result value */ + for (let i = 0; i < pathArr.length; i++) { + if (!result) break; + result = result[pathArr[i]]; + } + + return result; +} + +/** + * Set the value in the object based on the text path. If you need to support arrays, please use the set method of lodash. + * @param obj {Object} - required The object to be operated on + * @param path {String} - required path information + * @param val {Any} - required If this parameter is not passed, the final result will be set to undefined + * @returns {Boolean} Returns true to indicate successful setting, otherwise setting fails + */ +export function setValByPath(obj, path, val) { + if (!obj || !path || typeof path !== "string") { + return false; + } + + let result = obj; + const pathArr = path.split("."); + + for (let i = 0; i < pathArr.length; i++) { + if (!result) break; + + if (i === pathArr.length - 1) { + result[pathArr[i]] = val; + return Number.isNaN(val) + ? Number.isNaN(result[pathArr[i]]) + : result[pathArr[i]] === val; + } + + result = result[pathArr[i]]; + } + + return false; +} diff --git a/scripts/libs/utils/original.js b/scripts/libs/utils/original.js new file mode 100644 index 00000000..57586f73 --- /dev/null +++ b/scripts/libs/utils/original.js @@ -0,0 +1,50 @@ +/*! + * @name original.js + * @description Store some important native functions to prevent external contamination. This logic should be brought forward as much as possible, otherwise the contaminated functions will be stored. + * @version 0.0.1 + * @author xxxily + * @date 2022/10/16 10:32 + * @github https://github.com/xxxily + */ + +export const original = { + // Prevent defineProperty and defineProperties from being overridden by AOP scripts + Object: { + defineProperty: Object.defineProperty, + defineProperties: Object.defineProperties, + }, + + // Prevent this kind of gameplay:https://juejin.cn/post/6865910564817010702 + Proxy, + + Map, + map: { + clear: Map.prototype.clear, + set: Map.prototype.set, + has: Map.prototype.has, + get: Map.prototype.get, + delete: Map.prototype.delete, + }, + + console: { + log: console.log, + info: console.info, + error: console.error, + warn: console.warn, + table: console.table, + }, + + ShadowRoot, + HTMLMediaElement, + CustomEvent, + // appendChild: Node.prototype.appendChild, + + JSON: { + parse: JSON.parse, + stringify: JSON.stringify, + }, + + alert, + confirm, + prompt, +}; diff --git a/scripts/libs/utils/ready.js b/scripts/libs/utils/ready.js new file mode 100644 index 00000000..9a23962b --- /dev/null +++ b/scripts/libs/utils/ready.js @@ -0,0 +1,56 @@ +/** + *Element listener + * @param selector - required + * @param fn - required, callback when the element exists + * @param shadowRoot - optional specifies to monitor the DOM element under a certain shadowRoot + * Reference: https://javascript.ruanyifeng.com/dom/mutationobserver.html + */ +export function ready(selector, fn, shadowRoot) { + const win = window; + const docRoot = shadowRoot || win.document.documentElement; + if (!docRoot) return false; + const MutationObserver = win.MutationObserver || win.WebKitMutationObserver; + const listeners = docRoot._MutationListeners || []; + + function $ready(selector, fn) { + // Store selectors and callback functions + listeners.push({ + selector: selector, + fn: fn, + }); + + /* Add listening objects */ + if (!docRoot._MutationListeners || !docRoot._MutationObserver) { + docRoot._MutationListeners = listeners; + docRoot._MutationObserver = new MutationObserver(() => { + for (let i = 0; i < docRoot._MutationListeners.length; i++) { + const item = docRoot._MutationListeners[i]; + check(item.selector, item.fn); + } + }); + + docRoot._MutationObserver.observe(docRoot, { + childList: true, + subtree: true, + }); + } + + // Check if the node is already in the DOM + check(selector, fn); + } + + function check(selector, fn) { + const elements = docRoot.querySelectorAll(selector); + for (let i = 0; i < elements.length; i++) { + const element = elements[i]; + element._MutationReadyList_ = element._MutationReadyList_ || []; + if (!element._MutationReadyList_.includes(fn)) { + element._MutationReadyList_.push(fn); + fn.call(element, element); + } + } + } + + const selectorArr = Array.isArray(selector) ? selector : [selector]; + selectorArr.forEach((selector) => $ready(selector, fn)); +} diff --git a/scripts/libs/utils/sort.js b/scripts/libs/utils/sort.js new file mode 100644 index 00000000..0d5d5d9c --- /dev/null +++ b/scripts/libs/utils/sort.js @@ -0,0 +1,17 @@ +export const quickSort = function (arr) { + if (arr.length <= 1) { + return arr; + } + var pivotIndex = Math.floor(arr.length / 2); + var pivot = arr.splice(pivotIndex, 1)[0]; + var left = []; + var right = []; + for (var i = 0; i < arr.length; i++) { + if (arr[i] < pivot) { + left.push(arr[i]); + } else { + right.push(arr[i]); + } + } + return quickSort(left).concat([pivot], quickSort(right)); +}; diff --git a/scripts/libs/utils/typeof.js b/scripts/libs/utils/typeof.js new file mode 100644 index 00000000..52dea48f --- /dev/null +++ b/scripts/libs/utils/typeof.js @@ -0,0 +1,34 @@ +/*! + * @name utils.js + * @description Data type related methods + * @version 0.0.1 + * @author Blaze + * @date 22/03/2019 22:46 + * @github https://github.com/xxxily + */ + +/** + * Accurately obtain the specific type of object. See: https://www.talkingcoder.com/article/6333557442705696719 + * @param obj { all } -Required The object to be judged + * @returns {*} Returns the specific type of judgment + */ +export function getType(obj) { + if (obj == null) { + return String(obj); + } + return typeof obj === "object" || typeof obj === "function" + ? (obj.constructor && + obj.constructor.name && + obj.constructor.name.toLowerCase()) || + /function\s(.+?)\(/.exec(obj.constructor)[1].toLowerCase() + : typeof obj; +} + +export const isType = (obj, typeName) => getType(obj) === typeName; +export const isObj = (obj) => isType(obj, "object"); +export const isErr = (obj) => isType(obj, "error"); +export const isArr = (obj) => isType(obj, "array"); +export const isRegExp = (obj) => isType(obj, "regexp"); +export const isFunction = (obj) => obj instanceof Function; +export const isUndefined = (obj) => isType(obj, "undefined"); +export const isNull = (obj) => isType(obj, "null"); diff --git a/scripts/libs/utils/url.js b/scripts/libs/utils/url.js new file mode 100644 index 00000000..06c5133e --- /dev/null +++ b/scripts/libs/utils/url.js @@ -0,0 +1,103 @@ +/*! + * @name url.js + * @description Related methods for parsing urls + * @version 0.0.1 + * @author Blaze + * @date 27/03/2019 15:52 + * @github https://github.com/xxxily + */ + +/** + * Reference example: + * https://segmentfault.com/a/1190000006215495 + * Note: This method must rely on the browser's DOM object + */ + +export function parseURL(url) { + var a = document.createElement("a"); + a.href = url || window.location.href; + return { + source: url, + protocol: a.protocol.replace(":", ""), + host: a.hostname, + port: a.port, + origin: a.origin, + search: a.search, + query: a.search, + file: (a.pathname.match(/\/([^/?#]+)$/i) || ["", ""])[1], + hash: a.hash.replace("#", ""), + path: a.pathname.replace(/^([^/])/, "/$1"), + relative: (a.href.match(/tps?:\/\/[^/]+(.+)/) || ["", ""])[1], + params: (function () { + var ret = {}; + var seg = []; + var paramArr = a.search.replace(/^\?/, "").split("&"); + + for (var i = 0; i < paramArr.length; i++) { + var item = paramArr[i]; + if (item !== "" && item.indexOf("=")) { + seg.push(item); + } + } + + for (var j = 0; j < seg.length; j++) { + var param = seg[j]; + var idx = param.indexOf("="); + var key = param.substring(0, idx); + var val = param.substring(idx + 1); + if (!key) { + ret[val] = null; + } else { + ret[key] = val; + } + } + return ret; + })(), + }; +} + +/** + * Convert params object into string pattern + * @param params {Object} - required params object + * @returns {string} + */ +export function stringifyParams(params) { + var strArr = []; + + if (!Object.prototype.toString.call(params) === "[object Object]") { + return ""; + } + + for (var key in params) { + if (Object.hasOwnProperty.call(params, key)) { + var val = params[key]; + var valType = Object.prototype.toString.call(val); + + if (val === "" || valType === "[object Undefined]") continue; + + if (val === null) { + strArr.push(key); + } else if (valType === "[object Array]") { + strArr.push(key + "=" + val.join(",")); + } else { + val = (JSON.stringify(val) || "" + val).replace(/(^"|"$)/g, ""); + strArr.push(key + "=" + val); + } + } + } + return strArr.join("&"); +} + +/** + * Restore the url object parsed by parseURL into the url address + * Mainly used to reorganize the url link after the query parameters are dynamically modified. + * @param obj {Object} - required parseURL parses the url object + */ +export function stringifyToUrl(urlObj) { + var query = stringifyParams(urlObj.params) || ""; + if (query) { + query = "?" + query; + } + var hash = urlObj.hash ? "#" + urlObj.hash : ""; + return urlObj.origin + urlObj.path + query + hash; +} diff --git a/scripts/libs/utils/videoCapture.js b/scripts/libs/utils/videoCapture.js new file mode 100644 index 00000000..3cfd736c --- /dev/null +++ b/scripts/libs/utils/videoCapture.js @@ -0,0 +1,119 @@ +/*! + * @name videoCapturer.js + * @version 0.0.1 + * @author Blaze + * @date 2019/9/21 12:03 + * @github https://github.com/xxxily + */ + +async function setClipboard(blob) { + if (navigator.clipboard) { + navigator.clipboard + .write([ + // eslint-disable-next-line no-undef + new ClipboardItem({ + [blob.type]: blob, + }), + ]) + .then(() => { + console.info("[setClipboard] clipboard suc", blob.type); + }) + .catch((e) => { + console.error("[setClipboard] clipboard err", blob.type, e); + }); + } else { + console.error( + "The current website does not support writing data to the clipboard, see:\n https://developer.mozilla.org/en-US/docs/Web/API/Clipboard" + ); + } +} + +var videoCapturer = { + /** + * Take a screenshot + * @param video {dom} -必选 video dom 标签 + * @returns {boolean} + */ + capture(video, download, title) { + if (!video) return false; + const t = this; + const currentTime = `${Math.floor(video.currentTime / 60)}'${( + video.currentTime % 60 + ).toFixed(3)}''`; + const captureTitle = title || `${document.title}_${currentTime}`; + + /* Screenshot core logic */ + video.setAttribute("crossorigin", "anonymous"); + const canvas = document.createElement("canvas"); + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + const context = canvas.getContext("2d"); + context.drawImage(video, 0, 0, canvas.width, canvas.height); + + if (download) { + t.download(canvas, captureTitle, video); + } else { + t.preview(canvas, captureTitle); + } + + return canvas; + }, + /** + * Preview the captured screen content + * @param canvas + */ + preview(canvas, title) { + canvas.style = "max-width:100%"; + const previewPage = window.open("", "_blank"); + previewPage.document.title = `capture previe - ${title || "Untitled"}`; + previewPage.document.body.style.textAlign = "center"; + previewPage.document.body.style.background = "#000"; + previewPage.document.body.appendChild(canvas); + }, + /** + * Canvas downloads the intercepted content + * @param canvas + */ + download(canvas, title, video) { + title = title || "videoCapturer_" + Date.now(); + + try { + /** + * Try copying to clipboard + * Note that some browsers do not support writing 'image/jpeg' type data to the clipboard. Image/jpg can, but the result of toBlob will be png data. + * So here is a new 'image/png' to try to copy to the clipboard, instead of putting setClipboard(blob) in the try below + * In addition, since the automatic downloading of the screenshot below will cause the page to be out of focus, it will also cause the copy to the clipboard to fail, so here we copy it to the clipboard first and then download it. + */ + canvas.toBlob( + function (blob) { + setClipboard(blob); + }, + "image/png", + 0.99 + ); + } catch (e) { + console.error("无法将截图复制到剪贴板。", e); + } + + try { + canvas.toBlob( + function (blob) { + const el = document.createElement("a"); + el.download = `${title}.jpg`; + el.href = URL.createObjectURL(blob); + el.click(); + }, + "image/jpeg", + 0.99 + ); + } catch (e) { + videoCapturer.preview(canvas, title); + console.error( + "The video source is restricted by the CORS logo and screenshots cannot be downloaded directly. See:\n https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" + ); + console.error(video, e); + } + }, +}; + +export default videoCapturer; diff --git a/scripts/libs/workerize/index.js b/scripts/libs/workerize/index.js new file mode 100644 index 00000000..aafb5d71 --- /dev/null +++ b/scripts/libs/workerize/index.js @@ -0,0 +1,91 @@ + +/** TODO: + * - pooling (+ load balancing by tracking # of open calls) + * - queueing (worth it? sortof free via postMessage already) + * + * @example + * let worker = workerize(` + * export function add(a, b) { + * // block for a quarter of a second to demonstrate asynchronicity + * let start = Date.now(); + * while (Date.now()-start < 250); + * return a + b; + * } + * `); + * (async () => { + * console.log('3 + 9 = ', await worker.add(3, 9)); + * console.log('1 + 2 = ', await worker.add(1, 2)); + * })(); + */ + export default function workerize(code, options) { + let exports = {}; + let exportsObjName = `__xpo${Math.random().toString().substring(2)}__`; + if (typeof code==='function') code = `(${Function.prototype.toString.call(code)})(${exportsObjName})`; + code = toCjs(code, exportsObjName, exports) + `\n(${Function.prototype.toString.call(setup)})(self,${exportsObjName},{})`; + let url = URL.createObjectURL(new Blob([code],{ type: 'text/javascript' })), + worker = new Worker(url, options), + term = worker.terminate, + callbacks = {}, + counter = 0, + i; + worker.kill = signal => { + worker.postMessage({ type: 'KILL', signal }); + setTimeout(worker.terminate); + }; + worker.terminate = () => { + URL.revokeObjectURL(url); + term.call(worker); + }; + worker.call = (method, params) => new Promise( (resolve, reject) => { + let id = `rpc${++counter}`; + callbacks[id] = [resolve, reject]; + worker.postMessage({ type: 'RPC', id, method, params }); + }); + worker.rpcMethods = {}; + setup(worker, worker.rpcMethods, callbacks); + worker.expose = methodName => { + worker[methodName] = function() { + return worker.call(methodName, [].slice.call(arguments)); + }; + }; + for (i in exports) if (!(i in worker)) worker.expose(i); + return worker; +} + +function setup(ctx, rpcMethods, callbacks) { + ctx.addEventListener('message', ({ data }) => { + let id = data.id; + if (data.type!=='RPC' || id==null) return; + if (data.method) { + let method = rpcMethods[data.method]; + if (method==null) { + ctx.postMessage({ type: 'RPC', id, error: 'NO_SUCH_METHOD' }); + } + else { + Promise.resolve() + .then( () => method.apply(null, data.params) ) + .then( result => { ctx.postMessage({ type: 'RPC', id, result }); }) + .catch( err => { ctx.postMessage({ type: 'RPC', id, error: ''+err }); }); + } + } + else { + let callback = callbacks[id]; + if (callback==null) throw Error(`Unknown callback ${id}`); + delete callbacks[id]; + if (data.error) callback[1](Error(data.error)); + else callback[0](data.result); + } + }); +} + +function toCjs(code, exportsObjName, exports) { + code = code.replace(/^(\s*)export\s+default\s+/m, (s, before) => { + exports.default = true; + return `${before}${exportsObjName}.default=`; + }); + code = code.replace(/^(\s*)export\s+((?:async\s*)?function(?:\s*\*)?|const|let|var)(\s+)([a-zA-Z$_][a-zA-Z0-9$_]*)/mg, (s, before, type, ws, name) => { + exports[name] = true; + return `${before}${exportsObjName}.${name}=${type}${ws}${name}`; + }); + return `var ${exportsObjName}={};\n${code}\n${exportsObjName};`; +} \ No newline at end of file diff --git a/scripts/templates/data_table/index.html b/scripts/templates/data_table/index.html deleted file mode 100644 index cf17abfc..00000000 --- a/scripts/templates/data_table/index.html +++ /dev/null @@ -1,519 +0,0 @@ - - - - - - - - Useful script - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Name - - Position - - Office - - Age - - Start date - - Salary - -
Tiger NixonSystem ArchitectEdinburgh612011/04/25$320,800
Garrett WintersAccountantTokyo632011/07/25$170,750
Ashton CoxJunior Technical AuthorSan Francisco662009/01/12$86,000
Cedric KellySenior Javascript DeveloperEdinburgh222012/03/29$433,060
Airi SatouAccountantTokyo332008/11/28$162,700
Brielle WilliamsonIntegration SpecialistNew York612012/12/02$372,000
Herrod ChandlerSales AssistantSan Francisco592012/08/06$137,500
Rhona DavidsonIntegration SpecialistTokyo552010/10/14$327,900
Colleen HurstJavascript DeveloperSan Francisco392009/09/15$205,500
Sonya FrostSoftware EngineerEdinburgh232008/12/13$103,600
Jena GainesOffice ManagerLondon302008/12/19$90,560
Quinn FlynnSupport LeadEdinburgh222013/03/03$342,000
Charde MarshallRegional DirectorSan Francisco362008/10/16$470,600
Haley KennedySenior Marketing DesignerLondon432012/12/18$313,500
Tatyana FitzpatrickRegional DirectorLondon192010/03/17$385,750
Michael SilvaMarketing DesignerLondon662012/11/27$198,500
Paul ByrdChief Financial Officer (CFO)New York642010/06/09$725,000
Gloria LittleSystems AdministratorNew York592009/04/10$237,500
Bradley GreerSoftware EngineerLondon412012/10/13$132,000
Dai RiosPersonnel LeadEdinburgh352012/09/26$217,500
Jenette CaldwellDevelopment LeadNew York302011/09/03$345,000
Yuri BerryChief Marketing Officer (CMO)New York402009/06/25$675,000
Caesar VancePre-Sales SupportNew York212011/12/12$106,450
Doris WilderSales AssistantSidney232010/09/20$85,600
Angelica RamosChief Executive Officer (CEO)London472009/10/09$1,200,000
Gavin JoyceDeveloperEdinburgh422010/12/22$92,575
Jennifer ChangRegional DirectorSingapore282010/11/14$357,650
Brenden WagnerSoftware EngineerSan Francisco282011/06/07$206,850
Fiona GreenChief Operating Officer (COO)San Francisco482010/03/11$850,000
Shou ItouRegional MarketingTokyo202011/08/14$163,000
Michelle HouseIntegration SpecialistSidney372011/06/02$95,400
Suki BurksDeveloperLondon532009/10/22$114,500
Prescott BartlettTechnical AuthorLondon272011/05/07$145,000
Gavin CortezTeam LeaderSan Francisco222008/10/26$235,500
Martena MccrayPost-Sales supportEdinburgh462011/03/09$324,050
Unity ButlerMarketing DesignerSan Francisco472009/12/09$85,675
Howard HatfieldOffice ManagerSan Francisco512008/12/16$164,500
Hope FuentesSecretarySan Francisco412010/02/12$109,850
Vivian HarrellFinancial ControllerSan Francisco622009/02/14$452,500
Timothy MooneyOffice ManagerLondon372008/12/11$136,200
Jackson BradshawDirectorNew York652008/09/26$645,750
Olivia LiangSupport EngineerSingapore642011/02/03$234,500
Bruno NashSoftware EngineerLondon382011/05/03$163,500
Sakura YamamotoSupport EngineerTokyo372009/08/19$139,575
Thor WaltonDeveloperNew York612013/08/11$98,540
Finn CamachoSupport EngineerSan Francisco472009/07/07$87,500
Serge BaldwinData CoordinatorSingapore642012/04/09$138,575
Zenaida FrankSoftware EngineerNew York632010/01/04$125,250
Zorita SerranoSoftware EngineerSan Francisco562012/06/01$115,000
Jennifer AcostaJunior Javascript DeveloperEdinburgh432013/02/01$75,650
Cara StevensSales AssistantNew York462011/12/06$145,600
Hermione ButlerRegional DirectorLondon472011/03/21$356,250
Lael GreerSystems AdministratorLondon212009/02/27$103,500
Jonas AlexanderDeveloperSan Francisco302010/07/14$86,500
Shad DeckerRegional DirectorEdinburgh512008/11/13$183,000
Michael BruceJavascript DeveloperSingapore292011/06/27$183,000
Donna SniderCustomer SupportNew York272011/01/25$112,000
Name - Position - Office - Age - Start date - Salary -
- - - \ No newline at end of file diff --git a/scripts/templates/data_table/main.js b/scripts/templates/data_table/main.js deleted file mode 100644 index 3e6b8854..00000000 --- a/scripts/templates/data_table/main.js +++ /dev/null @@ -1,7 +0,0 @@ -$(document).ready(function () { - $("#dtBasicExample").DataTable({ - // paging: true, - // ordering: true, - // info: true, - }); -}); diff --git a/scripts/templates/data_table/style.css b/scripts/templates/data_table/style.css deleted file mode 100644 index 8a1cd5c7..00000000 --- a/scripts/templates/data_table/style.css +++ /dev/null @@ -1,4 +0,0 @@ -body { - max-width: 1000px; - margin: auto; -} \ No newline at end of file From a8a768724c2a9bf7acee0373231fa3cd5a51520b Mon Sep 17 00:00:00 2001 From: "hoang.tran12" Date: Wed, 27 Mar 2024 15:20:08 +0700 Subject: [PATCH 02/40] add fireship --- popup/tabs.js | 5 ++-- scripts/fireship_vip.js | 54 +++++++++++++++++++++++++++++++++++++++++ scripts/index.js | 2 ++ 3 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 scripts/fireship_vip.js diff --git a/popup/tabs.js b/popup/tabs.js index bf7a2b6d..b8d77d86 100644 --- a/popup/tabs.js +++ b/popup/tabs.js @@ -251,10 +251,11 @@ const tabs = [ s.leakCheck, createTitle("--- Unlock web ---", "--- Mở khoá web ---"), s.medium_readFullArticle, - s.envato_bypassPreview, + s.fireship_vip, s.scribd_bypassPreview, - s.studyphim_unlimited, s.studocu_bypassPreview, + s.studyphim_unlimited, + s.envato_bypassPreview, createTitle("--- Unlock function ---", "--- Mở khoá chức năng ---"), s.detect_zeroWidthCharacters, s.simpleAllowCopy, diff --git a/scripts/fireship_vip.js b/scripts/fireship_vip.js new file mode 100644 index 00000000..41b9d288 --- /dev/null +++ b/scripts/fireship_vip.js @@ -0,0 +1,54 @@ +export default { + icon: "https://fireship.io/img/favicon.png", + name: { + en: "Fireship - PRO unlocked", + vi: "Fireship - Mở khoá PRO", + }, + description: { + en: "Unlock all Fireship PRO courses/lessons (saved $399 USD)", + vi: "Mở khoá tất cả khoá học/bài giảng PRO trên Fireship (tiết kiệm $399 USD)", + }, + + whiteList: ["https://fireship.io/*"], + + onDocumentIdle: () => { + // ==UserScript== + // @name Freeship + // @namespace lemons + // @version 1.6 + // @description Unlock all Fireship PRO courses/lessons. + // @author lemons + // @match https://fireship.io/* + // @icon https://em-content.zobj.net/source/apple/391/fire_1f525.png + // @grant none + // @downloadURL https://update.greasyfork.org/scripts/455330/Freeship.user.js + // @updateURL https://update.greasyfork.org/scripts/455330/Freeship.meta.js + // ==/UserScript== + + // prettier-ignore + setInterval(async () => { + document.querySelectorAll("[free=\"\"]").forEach(el => el.setAttribute("free", true)) // set all elements with the attribute free set to "" to true + + if (document.querySelector("if-access [slot=\"granted\"]")) { // replace HOW TO ENROLL to YOU HAVE ACCESS + document.querySelector("if-access [slot=\"denied\"]").remove() + document.querySelector("if-access [slot=\"granted\"]").setAttribute("slot", "denied") + } + + if (document.querySelector("video-player")?.shadowRoot?.querySelector(".vid")?.innerHTML) return; // return if no video player + const vimeoId = Number(atob(document.querySelector("global-data").vimeo)) - Number(document.querySelector("head").getAttribute("data-build")); // get id for vimeo video + const youtubeId = atob(document.querySelector("global-data").youtube); // get id for vimeo video + + if (youtubeId) { // if there is an id, + document.querySelector("video-player").setAttribute("free", true) // set free to true + document.querySelector("video-player").shadowRoot.querySelector(".vid").innerHTML = `` // set video + return; + } + if (vimeoId) { // if there is an id, + document.querySelector("video-player").setAttribute("free", true) // set free to true + const html = (await fetch(`https://vimeo.com/api/oembed.json?url=https%3A%2F%2Fvimeo.com%2F${vimeoId}&id=${vimeoId}`).then(r=>r.json())).html + document.querySelector("video-player").shadowRoot.querySelector(".vid").innerHTML = html // set video + return; + } + }, 500) + }, +}; diff --git a/scripts/index.js b/scripts/index.js index a482c906..95b06a96 100644 --- a/scripts/index.js +++ b/scripts/index.js @@ -179,6 +179,7 @@ import leakCheck from "./leakCheck.js"; import whellOfNames_hack from "./whellOfNames_hack.js"; import saveAllVideo from "./saveAllVideo.js"; import fb_bulkDownload from "./fb_bulkDownload.js"; +import fireship_vip from "./fireship_vip.js"; // inject badges const allScripts = { @@ -379,6 +380,7 @@ const allScripts = { whellOfNames_hack: addBadge(whellOfNames_hack, BADGES.new), saveAllVideo: addBadge(saveAllVideo, BADGES.new), fb_bulkDownload: addBadge(fb_bulkDownload, BADGES.hot), + fireship_vip: addBadge(fireship_vip, BADGES.new), }; // alert(Object.keys(allScripts).length); From 9ed36134fff2efa49a4566ebc7dba8d3df069678 Mon Sep 17 00:00:00 2001 From: "hoang.tran12" Date: Wed, 27 Mar 2024 16:01:23 +0700 Subject: [PATCH 03/40] update yt5s --- scripts/youtube_downloadVideo.js | 41 ++++++++++++++++---------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/scripts/youtube_downloadVideo.js b/scripts/youtube_downloadVideo.js index fcb9de0a..db58d962 100644 --- a/scripts/youtube_downloadVideo.js +++ b/scripts/youtube_downloadVideo.js @@ -21,36 +21,28 @@ export default { let options = [ { name: "yt1s.com", - func: (url) => { - window.open("https://yt1s.com/vi/youtube-to-mp4?q=" + url); - }, + func: (url) => "https://yt1s.com/vi/youtube-to-mp4?q=" + url, }, { - name: "10downloader.com", - func: (url) => { - window.open("https://10downloader.com/download?v=" + url); - }, + name: "yt5s.com", + func: (url) => url.replace("youtube", "youtube5s"), }, { - name: "ymp4.com", - func: (url) => { - window.open("https://ymp4.download/en50/?url=" + url); - }, + name: "10downloader.com", + func: (url) => "https://10downloader.com/download?v=" + url, }, { name: "9xbuddy.com", - func: (url) => { - window.open("https://9xbuddy.com/process?url=" + url); - }, + func: (url) => "https://9xbuddy.com/process?url=" + url, }, { name: "getlinks.vip", - url: "https://getlinks.vip/vi/youtube/", - func: (url) => { - window.open( - "https://getlinks.vip/vi/youtube/" + getIdFromYoutubeURL(url) - ); - }, + func: (url) => + "https://getlinks.vip/vi/youtube/" + getIdFromYoutubeURL(url), + }, + { + name: "ymp4.com", + func: (url) => "https://ymp4.download/en50/?url=" + url, }, ]; @@ -63,7 +55,14 @@ export default { if (choose != null && choose >= 0 && choose < options.length) { let url = prompt("Nhập link youtube:", location.href); - url && options[choose].func(url); + if (url) { + url = options[choose].func(url); + let myWin = window.open( + url, + "Download Youtube Video", + "directories=no,titlebar=no,toolbar=no,location=no,status=no,menubar=no,scrollbars=no,resizable=no,width=800, height=900" + ); + } } }, }; From c39ec6c961b3e344b0da68cacfeb53cefe7371b1 Mon Sep 17 00:00:00 2001 From: "hoang.tran12" Date: Wed, 27 Mar 2024 16:01:33 +0700 Subject: [PATCH 04/40] add greasyfork link --- scripts/fb_downloadWatchingVideo.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/fb_downloadWatchingVideo.js b/scripts/fb_downloadWatchingVideo.js index 9b95f73c..ab23dbdc 100644 --- a/scripts/fb_downloadWatchingVideo.js +++ b/scripts/fb_downloadWatchingVideo.js @@ -12,6 +12,8 @@ export default { vi: "Tải bất kỳ video facebook nào mà bạn đang xem (watch/story/comment/reel/chat/bình luận/tin nhắn)", }, whiteList: ["https://*.facebook.com/*"], + infoLink: + "https://greasyfork.org/en/scripts/477748-facebook-video-downloader", onClickExtension: async function () { let { closeLoading, setLoadingText } = showLoading( From 2b545f223b700eec0b41e861bf5eb85353d5a2ae Mon Sep 17 00:00:00 2001 From: "hoang.tran12" Date: Wed, 27 Mar 2024 16:50:54 +0700 Subject: [PATCH 05/40] remove saveAllVideo in youtube section --- popup/tabs.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/popup/tabs.js b/popup/tabs.js index b8d77d86..5c0c1416 100644 --- a/popup/tabs.js +++ b/popup/tabs.js @@ -62,10 +62,10 @@ const tabs = [ { ...CATEGORY.download, scripts: [ - s.saveAllVideo, s.getFavicon, s.download_watchingVideo, s.getLinkLuanxt, + s.saveAllVideo, // s.bookmark_exporter, createTitle("--- Music ---", "--- Nhạc ---"), s.showTheAudios, @@ -177,7 +177,6 @@ const tabs = [ { ...CATEGORY.youtube, scripts: [ - s.saveAllVideo, s.youtube_downloadVideo, s.pictureInPicture, s.youtube_toggleLight, From 5d7f96b7481a0720d492bc31a0cd30f8f492030b Mon Sep 17 00:00:00 2001 From: "hoang.tran12" Date: Wed, 27 Mar 2024 16:51:14 +0700 Subject: [PATCH 06/40] fix luanxt - md5 - WIP --- scripts/getLinkLuanxt.js | 14 ++- scripts/libs/crypto/md5.js | 190 +++++++++++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+), 8 deletions(-) create mode 100644 scripts/libs/crypto/md5.js diff --git a/scripts/getLinkLuanxt.js b/scripts/getLinkLuanxt.js index 0d13f1d7..b34ba4ad 100644 --- a/scripts/getLinkLuanxt.js +++ b/scripts/getLinkLuanxt.js @@ -1,4 +1,5 @@ import { showLoading } from "./helpers/utils.js"; +import { md5 } from "./libs/crypto/md5.js"; export default { icon: "https://luanxt.com/get-link-mp3-320-lossless-vip-zing/favicon.ico", @@ -10,7 +11,8 @@ export default { en: "Using API from luanxt.com. Download Zing MP3, Zing Video Clip, Zing TV, NhacCuaTui, YouTube, SoundCloud, Nhac.vn, ChiaSeNhac.vn, Facebook Video, Keeng Audio, Keeng Video, Keeng Phim", vi: "Sử dụng API của luanxt.com. Tải Zing MP3, Zing Video Clip, Zing TV, NhacCuaTui, YouTube, SoundCloud, Nhac.vn, ChiaSeNhac.vn, Facebook Video, Keeng Audio, Keeng Video, Keeng Phim", }, - + infoLink: "https://luanxt.com/get-link-mp3-320-lossless-vip-zing/", + onClickExtension: async function () { // https://luanxt.com/get-link-mp3-320-lossless-vip-zing/ @@ -27,13 +29,8 @@ export default { // lxtS = "xt1>>8" + xtSalt; let lxtS = "xt1>>8(k[1>>8]>>>k%2=0).fromCharCode"; - //prettier-ignore - const xtHs = (()=>{ - function e(n,r){var t=(65535&n)+(65535&r);return(n>>16)+(r>>16)+(t>>16)<<16|65535&t}function n(n,r,t,o,u,i){return e(function(n,r){return n<>>32-r}(e(e(r,n),e(o,i)),u),t)}function r(r,t,o,e,u,i,a){return n(t&o|~t&e,r,t,u,i,a)}function i(r,t,o,e,u,i,a){return n(t&e|o&~e,r,t,u,i,a)}function o(r,t,o,e,u,i,a){return n(t^o^e,r,t,u,i,a)}function a(r,t,o,e,u,i,a){return n(o^(t|~e),r,t,u,i,a)}function s(n,t){n[t>>5]|=128<>>9<<4)]=t;var u,c,f,h,l,g=1732584193,d=-271733879,v=-1732584194,s=271733878;for(u=0;u>5]>>>r%32&255);return t}function l(n){var r,t=[];for(t[(n.length>>2)-1]=void 0,r=0;r>5]|=(255&n.charCodeAt(r/8))<>>4&15)+"0123456789abcdef".charAt(15&r);return o}function f(n){return unescape(encodeURIComponent(n))}function h(n){return function(n){return u(s(l(n),8*n.length))}(f(n))}function p(n,r){return function(n,r){var t,o,e=l(n),i=[],a=[];for(i[15]=a[15]=void 0,e.length>16&&(e=s(e,8*n.length)),t=0;t<16;t+=1)i[t]=909522486^e[t],a[t]=1549556828^e[t];return o=s(i.concat(l(r)),512+8*r.length),u(s(a.concat(o),640))}(f(n),f(r))} - return function d(n,r,t){return r?t?p(r,n):function(n,r){return c(p(n,r))}(r,n):t?h(n):function(n){return c(h(n))}(n)} - })(); const xtSign = (t) => - xtHs(t + lxtS + "function s = module.exports=s(2)").substr(1, 8); + md5(t + lxtS + "function s = module.exports=s(2)").substr(1, 8); async function getLuanxtUserToken() { let res = await fetch(apiBaseURL); @@ -42,7 +39,7 @@ export default { } async function getLinkLuanxt(link, token) { - let sig = xtSign(`${link}${token}`); + let sig = "667c818f"; // xtSign(`${link}${token}`); let res = await fetch(`${apiBaseURL}api/get-link`, { method: "POST", @@ -158,6 +155,7 @@ export default { ); try { let userToken = await getLuanxtUserToken(); + prompt("Token luanxt:", userToken); setLoadingText("Đang get link từ luanxt..."); let json = await getLinkLuanxt(url, userToken); diff --git a/scripts/libs/crypto/md5.js b/scripts/libs/crypto/md5.js new file mode 100644 index 00000000..bf01aba9 --- /dev/null +++ b/scripts/libs/crypto/md5.js @@ -0,0 +1,190 @@ +// https://www.myersdaily.org/joseph/javascript/md5-text.html + +function md5cycle(x, k) { + var a = x[0], + b = x[1], + c = x[2], + d = x[3]; + + a = ff(a, b, c, d, k[0], 7, -680876936); + d = ff(d, a, b, c, k[1], 12, -389564586); + c = ff(c, d, a, b, k[2], 17, 606105819); + b = ff(b, c, d, a, k[3], 22, -1044525330); + a = ff(a, b, c, d, k[4], 7, -176418897); + d = ff(d, a, b, c, k[5], 12, 1200080426); + c = ff(c, d, a, b, k[6], 17, -1473231341); + b = ff(b, c, d, a, k[7], 22, -45705983); + a = ff(a, b, c, d, k[8], 7, 1770035416); + d = ff(d, a, b, c, k[9], 12, -1958414417); + c = ff(c, d, a, b, k[10], 17, -42063); + b = ff(b, c, d, a, k[11], 22, -1990404162); + a = ff(a, b, c, d, k[12], 7, 1804603682); + d = ff(d, a, b, c, k[13], 12, -40341101); + c = ff(c, d, a, b, k[14], 17, -1502002290); + b = ff(b, c, d, a, k[15], 22, 1236535329); + + a = gg(a, b, c, d, k[1], 5, -165796510); + d = gg(d, a, b, c, k[6], 9, -1069501632); + c = gg(c, d, a, b, k[11], 14, 643717713); + b = gg(b, c, d, a, k[0], 20, -373897302); + a = gg(a, b, c, d, k[5], 5, -701558691); + d = gg(d, a, b, c, k[10], 9, 38016083); + c = gg(c, d, a, b, k[15], 14, -660478335); + b = gg(b, c, d, a, k[4], 20, -405537848); + a = gg(a, b, c, d, k[9], 5, 568446438); + d = gg(d, a, b, c, k[14], 9, -1019803690); + c = gg(c, d, a, b, k[3], 14, -187363961); + b = gg(b, c, d, a, k[8], 20, 1163531501); + a = gg(a, b, c, d, k[13], 5, -1444681467); + d = gg(d, a, b, c, k[2], 9, -51403784); + c = gg(c, d, a, b, k[7], 14, 1735328473); + b = gg(b, c, d, a, k[12], 20, -1926607734); + + a = hh(a, b, c, d, k[5], 4, -378558); + d = hh(d, a, b, c, k[8], 11, -2022574463); + c = hh(c, d, a, b, k[11], 16, 1839030562); + b = hh(b, c, d, a, k[14], 23, -35309556); + a = hh(a, b, c, d, k[1], 4, -1530992060); + d = hh(d, a, b, c, k[4], 11, 1272893353); + c = hh(c, d, a, b, k[7], 16, -155497632); + b = hh(b, c, d, a, k[10], 23, -1094730640); + a = hh(a, b, c, d, k[13], 4, 681279174); + d = hh(d, a, b, c, k[0], 11, -358537222); + c = hh(c, d, a, b, k[3], 16, -722521979); + b = hh(b, c, d, a, k[6], 23, 76029189); + a = hh(a, b, c, d, k[9], 4, -640364487); + d = hh(d, a, b, c, k[12], 11, -421815835); + c = hh(c, d, a, b, k[15], 16, 530742520); + b = hh(b, c, d, a, k[2], 23, -995338651); + + a = ii(a, b, c, d, k[0], 6, -198630844); + d = ii(d, a, b, c, k[7], 10, 1126891415); + c = ii(c, d, a, b, k[14], 15, -1416354905); + b = ii(b, c, d, a, k[5], 21, -57434055); + a = ii(a, b, c, d, k[12], 6, 1700485571); + d = ii(d, a, b, c, k[3], 10, -1894986606); + c = ii(c, d, a, b, k[10], 15, -1051523); + b = ii(b, c, d, a, k[1], 21, -2054922799); + a = ii(a, b, c, d, k[8], 6, 1873313359); + d = ii(d, a, b, c, k[15], 10, -30611744); + c = ii(c, d, a, b, k[6], 15, -1560198380); + b = ii(b, c, d, a, k[13], 21, 1309151649); + a = ii(a, b, c, d, k[4], 6, -145523070); + d = ii(d, a, b, c, k[11], 10, -1120210379); + c = ii(c, d, a, b, k[2], 15, 718787259); + b = ii(b, c, d, a, k[9], 21, -343485551); + + x[0] = add32(a, x[0]); + x[1] = add32(b, x[1]); + x[2] = add32(c, x[2]); + x[3] = add32(d, x[3]); +} + +function cmn(q, a, b, x, s, t) { + a = add32(add32(a, q), add32(x, t)); + return add32((a << s) | (a >>> (32 - s)), b); +} + +function ff(a, b, c, d, x, s, t) { + return cmn((b & c) | (~b & d), a, b, x, s, t); +} + +function gg(a, b, c, d, x, s, t) { + return cmn((b & d) | (c & ~d), a, b, x, s, t); +} + +function hh(a, b, c, d, x, s, t) { + return cmn(b ^ c ^ d, a, b, x, s, t); +} + +function ii(a, b, c, d, x, s, t) { + return cmn(c ^ (b | ~d), a, b, x, s, t); +} + +function md51(s) { + txt = ""; + var n = s.length, + state = [1732584193, -271733879, -1732584194, 271733878], + i; + for (i = 64; i <= s.length; i += 64) { + md5cycle(state, md5blk(s.substring(i - 64, i))); + } + s = s.substring(i - 64); + var tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for (i = 0; i < s.length; i++) + tail[i >> 2] |= s.charCodeAt(i) << (i % 4 << 3); + tail[i >> 2] |= 0x80 << (i % 4 << 3); + if (i > 55) { + md5cycle(state, tail); + for (i = 0; i < 16; i++) tail[i] = 0; + } + tail[14] = n * 8; + md5cycle(state, tail); + return state; +} + +/* there needs to be support for Unicode here, + * unless we pretend that we can redefine the MD-5 + * algorithm for multi-byte characters (perhaps + * by adding every four 16-bit characters and + * shortening the sum to 32 bits). Otherwise + * I suggest performing MD-5 as if every character + * was two bytes--e.g., 0040 0025 = @%--but then + * how will an ordinary MD-5 sum be matched? + * There is no way to standardize text to something + * like UTF-8 before transformation; speed cost is + * utterly prohibitive. The JavaScript standard + * itself needs to look at this: it should start + * providing access to strings as preformed UTF-8 + * 8-bit unsigned value arrays. + */ +function md5blk(s) { + /* I figured global was faster. */ + var md5blks = [], + i; /* Andy King said do it this way. */ + for (i = 0; i < 64; i += 4) { + md5blks[i >> 2] = + s.charCodeAt(i) + + (s.charCodeAt(i + 1) << 8) + + (s.charCodeAt(i + 2) << 16) + + (s.charCodeAt(i + 3) << 24); + } + return md5blks; +} + +var hex_chr = "0123456789abcdef".split(""); + +function rhex(n) { + var s = "", + j = 0; + for (; j < 4; j++) + s += hex_chr[(n >> (j * 8 + 4)) & 0x0f] + hex_chr[(n >> (j * 8)) & 0x0f]; + return s; +} + +function hex(x) { + for (var i = 0; i < x.length; i++) x[i] = rhex(x[i]); + return x.join(""); +} + +export function md5(s) { + return hex(md51(s)); +} + +/* this function is much faster, + so if possible we use it. Some IEs + are the only ones I know of that + need the idiotic second function, + generated by an if clause. */ + +function add32(a, b) { + return (a + b) & 0xffffffff; +} + +if (md5("hello") != "5d41402abc4b2a76b9719d911017c592") { + function add32(x, y) { + var lsw = (x & 0xffff) + (y & 0xffff), + msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xffff); + } +} From 9715ed7bbd49bcbfa1cf1d655cb900e9eff99ba4 Mon Sep 17 00:00:00 2001 From: "hoang.tran12" Date: Thu, 28 Mar 2024 15:11:21 +0700 Subject: [PATCH 07/40] add /*html*/ --- scripts/backup/scripts.js | 6 +-- scripts/detect_zeroWidthCharacters.js | 4 +- scripts/fb_whoIsTyping.js | 2 +- scripts/freesound_downloadAudio.js | 42 -------------------- scripts/getFavicon.js | 4 +- scripts/ggDrive_downloadAllVideosInFolder.js | 8 ++-- scripts/ggdrive_downloadVideo.js | 4 +- scripts/helpers/utils.js | 4 +- scripts/nhaccuatui_downloader.js | 8 ++-- scripts/shortenURL.js | 2 +- scripts/showTheAudios.js | 19 +++++++-- scripts/showTheImages.js | 8 ++-- scripts/showTheVideos.js | 24 ++++++++--- scripts/transfer_sh.js | 8 ++-- 14 files changed, 62 insertions(+), 81 deletions(-) delete mode 100644 scripts/freesound_downloadAudio.js diff --git a/scripts/backup/scripts.js b/scripts/backup/scripts.js index 33dd491d..9c4c5ce6 100644 --- a/scripts/backup/scripts.js +++ b/scripts/backup/scripts.js @@ -361,7 +361,7 @@ javascript: if (window.location.hostname.includes("reddit")) { window.open(url, "_self"); } function infoPop() { - var a = `

+ var a = /*html*/ `

Welcome to the Reddit Toolkit!

  • Upvote all will up vote all all of the posts on a subreddit or even of an individual user. This will work even for subs that you are not subscribed to!
    @@ -369,7 +369,7 @@ javascript: if (window.location.hostname.includes("reddit")) {
  • Downvote all will down vote all all of the posts on a subreddit or even of an individual user. This will work even for subs that you are not subscribed to!
    CAVEAT! - See above!
  • Old/New Reddit toggle. This will toggle between old and new Reddit. Most of these tools ONLY work in Old Reddit.
  • -
  • Stealth Mode. This will hide much of the graphics and headers/footers that give away the fact that you are surfing Reddit. Often used by people who are at work.
  • +
  • Stealth Mode. This will hide much of the graphics and headers/footers that give away the fact that you are surfing Reddit. Often used by people who are at work.
  • Coder Mode. Looks like you have some kind of coding interface open. Another way of reading Reddit when you are supposed to be doing something else!
  • OpenInNewTab. Clicking this will not make any visible changes on the page, however, all of the links you click on will open in a new tab!
  • GetRedditVideo - This will open an interface that will let you download a Reddit hosted video.
  • @@ -497,4 +497,4 @@ function getLinkFCode() { chrome.runtime.getPackageDirectoryEntry(function (root) { console.log(root); -}); \ No newline at end of file +}); diff --git a/scripts/detect_zeroWidthCharacters.js b/scripts/detect_zeroWidthCharacters.js index 6e585117..38627892 100644 --- a/scripts/detect_zeroWidthCharacters.js +++ b/scripts/detect_zeroWidthCharacters.js @@ -5,8 +5,8 @@ export default { vi: "Phát hiện ký tự ẩn (Zero-Width)", }, description: { - en: "Detects zero-width characters, highlights the characters and containing DOM element.\n\nClick for more detail.", - vi: "Phát hiện ký tự ẩn (zero-width) trong văn bản cho trình duyệt, e-mail client, trình soạn thảo văn bản,...\n\nBấm để xem thêm chi tiết.", + en: "Detects zero-width characters, highlights the characters and containing DOM element.\n\nClick ? for more detail.", + vi: "Phát hiện ký tự ẩn (zero-width) trong văn bản cho trình duyệt, e-mail client, trình soạn thảo văn bản,...\n\nBấm ? để xem thêm chi tiết.", }, infoLink: "https://viblo.asia/p/ky-tu-zero-width-sat-thu-vo-hinh-nam-giua-doan-van-ban-thuan-vo-hai-L4x5xM7qKBM", diff --git a/scripts/fb_whoIsTyping.js b/scripts/fb_whoIsTyping.js index bcda8e77..82156385 100644 --- a/scripts/fb_whoIsTyping.js +++ b/scripts/fb_whoIsTyping.js @@ -60,7 +60,7 @@ export default { if (!exist) { exist = document.createElement("div"); exist.id = divId; - exist.innerHTML = `
    + exist.innerHTML = /*html*/ `
    `; diff --git a/scripts/freesound_downloadAudio.js b/scripts/freesound_downloadAudio.js deleted file mode 100644 index 75e05b6c..00000000 --- a/scripts/freesound_downloadAudio.js +++ /dev/null @@ -1,42 +0,0 @@ -import { getCurrentTab, showLoading } from "./helpers/utils.js"; - -export default { - icon: "https://freesound.org/favicon.ico", - name: { - en: "Freesound - Download audio", - vi: "Freesound - Tải âm thanh", - }, - description: { - en: "Download audio on freesound.org", - vi: "Tải âm thanh trên freesound.org", - }, - - onClickExtension: async function () { - // https://github.com/soimort/you-get/blob/develop/src/you_get/extractors/freesound.py - - let tab = await getCurrentTab(); - let url = prompt("Nhập link freesound: ", tab.url); - if (url == null) return; - - let { closeLoading } = showLoading("Đang tìm file âm thanh..."); - try { - let res = await fetch(url); - let html = await res.text(); - - // prettier-ignore - let title = new RegExp(' { - return `
    + return /*html*/ `
    @@ -66,7 +66,7 @@ export default { let escapeHTMLPolicy = trustedTypes.createPolicy("forceInner", { createHTML: (to_escape) => to_escape, }); - win.document.body.innerHTML = escapeHTMLPolicy.createHTML(`
    + win.document.body.innerHTML = escapeHTMLPolicy.createHTML(/*html*/ `
    '); + let observer = new MutationObserver(ms => ms.forEach(m => m.addedNodes.forEach(node => this.detect(node)))); + observer.observe(document.body, {childList: true, subtree: true}); + }, + detect: function(node) { + let article = node.tagName == 'ARTICLE' && node || node.tagName == 'DIV' && (node.querySelector('article') || node.closest('article')); + if (article) this.addButtonTo(article); + let listitems = node.tagName == 'LI' && node.getAttribute('role') == 'listitem' && [node] || node.tagName == 'DIV' && node.querySelectorAll('li[role="listitem"]'); + if (listitems) this.addButtonToMedia(listitems); + }, + addButtonTo: function (article) { + if (article.dataset.detected) return; + article.dataset.detected = 'true'; + let media_selector = [ + 'a[href*="/photo/1"]', + 'div[role="progressbar"]', + 'div[data-testid="playButton"]', + 'a[href="/settings/content_you_see"]', //hidden content + 'div.media-image-container', // for tweetdeck + 'div.media-preview-container', // for tweetdeck + 'div[aria-labelledby]>div:first-child>div[role="button"][tabindex="0"]' //for audio (experimental) + ]; + let media = article.querySelector(media_selector.join(',')); + if (media) { + let status_id = article.querySelector('a[href*="/status/"]').href.split('/status/').pop().split('/').shift(); + let btn_group = article.querySelector('div[role="group"]:last-of-type, ul.tweet-actions, ul.tweet-detail-actions'); + let btn_share = Array.from(btn_group.querySelectorAll(':scope>div>div, li.tweet-action-item>a, li.tweet-detail-action-item>a')).pop().parentNode; + let btn_down = btn_share.cloneNode(true); + if (is_tweetdeck) { + btn_down.firstElementChild.innerHTML = '' + this.svg + ''; + btn_down.firstElementChild.removeAttribute('rel'); + btn_down.classList.replace("pull-left", "pull-right"); + } else { + btn_down.querySelector('svg').innerHTML = this.svg; + } + let is_exist = history.indexOf(status_id) >= 0; + this.status(btn_down, 'tmd-down'); + this.status(btn_down, is_exist ? 'completed' : 'download', is_exist ? lang.completed : lang.download); + btn_group.insertBefore(btn_down, btn_share.nextSibling); + btn_down.onclick = () => this.click(btn_down, status_id, is_exist); + if (show_sensitive) { + let btn_show = article.querySelector('div[aria-labelledby] div[role="button"][tabindex="0"]:not([data-testid]) > div[dir] > span > span'); + if (btn_show) btn_show.click(); + } + } + let imgs = article.querySelectorAll('a[href*="/photo/"]'); + if (imgs.length > 1) { + let status_id = article.querySelector('a[href*="/status/"]').href.split('/status/').pop().split('/').shift(); + let btn_group = article.querySelector('div[role="group"]:last-of-type'); + let btn_share = Array.from(btn_group.querySelectorAll(':scope>div>div')).pop().parentNode; + imgs.forEach(img => { + let index = img.href.split('/status/').pop().split('/').pop(); + let is_exist = history.indexOf(status_id) >= 0; + let btn_down = document.createElement('div'); + btn_down.innerHTML = '
    ' + this.svg + '
    '; + btn_down.classList.add('tmd-down', 'tmd-img'); + this.status(btn_down, 'download'); + img.parentNode.appendChild(btn_down); + btn_down.onclick = e => { + e.preventDefault(); + this.click(btn_down, status_id, is_exist, index); + } + }); + } + }, + addButtonToMedia: function(listitems) { + listitems.forEach(li => { + if (li.dataset.detected) return; + li.dataset.detected = 'true'; + let status_id = li.querySelector('a[href*="/status/"]').href.split('/status/').pop().split('/').shift(); + let is_exist = history.indexOf(status_id) >= 0; + let btn_down = document.createElement('div'); + btn_down.innerHTML = '
    ' + this.svg + '
    '; + btn_down.classList.add('tmd-down', 'tmd-media'); + this.status(btn_down, is_exist ? 'completed' : 'download', is_exist ? lang.completed : lang.download); + li.appendChild(btn_down); + btn_down.onclick = () => this.click(btn_down, status_id, is_exist); + }); + }, + click: async function (btn, status_id, is_exist, index) { + if (btn.classList.contains('loading')) return; + this.status(btn, 'loading'); + // let out = (await GM_getValue('filename', filename)).split('\n').join(''); + let out = filename.split('\n').join(''); + // let save_history = await GM_getValue('save_history', true); + let save_history = true + let json = await this.fetchJson(status_id); + console.log(json) + let tweet = json.legacy; + let user = json.core.user_results.result.legacy; + let invalid_chars = {'\\': '\', '\/': '/', '\|': '|', '<': '<', '>': '>', ':': ':', '*': '*', '?': '?', '"': '"', '\u200b': '', '\u200c': '', '\u200d': '', '\u2060': '', '\ufeff': '', '🔞': ''}; + let datetime = out.match(/{date-time(-local)?:[^{}]+}/) ? out.match(/{date-time(?:-local)?:([^{}]+)}/)[1].replace(/[\\/|<>*?:"]/g, v => invalid_chars[v]) : 'YYYYMMDD-hhmmss'; + let info = {}; + info['status-id'] = status_id; + info['user-name'] = user.name.replace(/([\\/|*?:"]|[\u200b-\u200d\u2060\ufeff]|🔞)/g, v => invalid_chars[v]); + info['user-id'] = user.screen_name; + info['date-time'] = this.formatDate(tweet.created_at, datetime); + info['date-time-local'] = this.formatDate(tweet.created_at, datetime, true); + info['full-text'] = tweet.full_text.split('\n').join(' ').replace(/\s*https:\/\/t\.co\/\w+/g, '').replace(/[\\/|<>*?:"]|[\u200b-\u200d\u2060\ufeff]/g, v => invalid_chars[v]); + let medias = tweet.extended_entities && tweet.extended_entities.media; + if (index) medias = [medias[index - 1]]; + if (medias?.length > 0) { + let tasks = medias.length; + let tasks_result = []; + medias.forEach((media, i) => { + info.url = media.type == 'photo' ? media.media_url_https + ':orig' : media.video_info.variants.filter(n => n.content_type == 'video/mp4').sort((a, b) => b.bitrate - a.bitrate)[0].url; + info.file = info.url.split('/').pop().split(/[:?]/).shift(); + info['file-name'] = info.file.split('.').shift(); + info['file-ext'] = info.file.split('.').pop(); + info['file-type'] = media.type.replace('animated_', ''); + info.out = (out.replace(/\.?{file-ext}/, '') + ((medias.length > 1 || index) && !out.match('{file-name}') ? '-' + (index ? index - 1 : i) : '') + '.{file-ext}').replace(/{([^{}:]+)(:[^{}]+)?}/g, (match, name) => info[name]); + this.downloader.add({ + url: info.url, + name: info.out, + onload: () => { + tasks -= 1; + tasks_result.push(((medias.length > 1 || index) ? (index ? index : i + 1) + ': ' : '') + lang.completed); + this.status(btn, null, tasks_result.sort().join('\n')); + if (tasks === 0) { + this.status(btn, 'completed', lang.completed); + if (save_history && !is_exist) { + history.push(status_id); + this.storage(status_id); + } + } + }, + onerror: result => { + tasks = -1; + tasks_result.push((medias.length > 1 ? i + 1 + ': ' : '') + result.details.current); + this.status(btn, 'failed', tasks_result.sort().join('\n')); + } + }); + }); + } else { + this.status(btn, 'failed', 'MEDIA_NOT_FOUND'); + } + }, + status: function (btn, css, title, style) { + if (css) { + btn.classList.remove('download', 'completed', 'loading', 'failed'); + btn.classList.add(css); + } + if (title) btn.title = title; + if (style) btn.style.cssText = style; + }, + fetchJson: async function (status_id) { + let base_url = `https://${host}/i/api/graphql/NmCeCgkVlsRGS1cAwqtgmw/TweetDetail`; + let variables = { + "focalTweetId":status_id, + "with_rux_injections":false, + "includePromotedContent":true, + "withCommunity":true, + "withQuickPromoteEligibilityTweetFields":true, + "withBirdwatchNotes":true, + "withVoice":true, + "withV2Timeline":true + }; + let features = { + "rweb_lists_timeline_redesign_enabled":true, + "responsive_web_graphql_exclude_directive_enabled":true, + "verified_phone_label_enabled":false, + "creator_subscriptions_tweet_preview_api_enabled":true, + "responsive_web_graphql_timeline_navigation_enabled":true, + "responsive_web_graphql_skip_user_profile_image_extensions_enabled":false, + "tweetypie_unmention_optimization_enabled":true, + "responsive_web_edit_tweet_api_enabled":true, + "graphql_is_translatable_rweb_tweet_is_translatable_enabled":true, + "view_counts_everywhere_api_enabled":true, + "longform_notetweets_consumption_enabled":true, + "responsive_web_twitter_article_tweet_consumption_enabled":false, + "tweet_awards_web_tipping_enabled":false, + "freedom_of_speech_not_reach_fetch_enabled":true, + "standardized_nudges_misinfo":true, + "tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled":true, + "longform_notetweets_rich_text_read_enabled":true, + "longform_notetweets_inline_media_enabled":true, + "responsive_web_media_download_video_enabled":false, + "responsive_web_enhance_cards_enabled":false + }; + let url = encodeURI(`${base_url}?variables=${JSON.stringify(variables)}&features=${JSON.stringify(features)}`); + let cookies = this.getCookie(); + let headers = { + 'authorization': 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA', + 'x-twitter-active-user': 'yes', + 'x-twitter-client-language': cookies.lang, + 'x-csrf-token': cookies.ct0 + }; + if (cookies.ct0.length == 32) headers['x-guest-token'] = cookies.gt; + let tweet_detail = await fetch(url, {headers: headers}).then(result => result.json()); + let tweet_entrie = tweet_detail.data.threaded_conversation_with_injections_v2.instructions[0].entries.find(n => n.entryId == `tweet-${status_id}`); + let tweet_result = tweet_entrie.content.itemContent.tweet_results.result; + return tweet_result.tweet || tweet_result; + }, + getCookie: function (name) { + let cookies = {}; + document.cookie.split(';').filter(n => n.indexOf('=') > 0).forEach(n => { + n.replace(/^([^=]+)=(.+)$/, (match, name, value) => { + cookies[name.trim()] = value.trim(); + }); + }); + return name ? cookies[name] : cookies; + }, + storage: async function (value) { + let data = JSON.parse(localStorage.getItem('ufs-twitter-download_history') || '[]'); + let data_length = data.length; + if (value) { + if (Array.isArray(value)) data = data.concat(value); + else if (data.indexOf(value) < 0) data.push(value); + } else return data; + if (data.length > data_length) + localStorage.setItem('ufs-twitter-download_history', JSON.stringify(data)); + }, + storage_obsolete: function (is_remove) { + let data = JSON.parse(localStorage.getItem('history') || '[]'); + if (is_remove) localStorage.removeItem('history'); + else return data; + }, + formatDate: function (i, o, tz) { + let d = new Date(i); + if (tz) d.setMinutes(d.getMinutes() - d.getTimezoneOffset()); + let m = ['JAN','FEB','MAR','APR','MAY','JUN','JUL','AUG','SEP','OCT','NOV','DEC']; + let v = { + YYYY: d.getUTCFullYear().toString(), + YY: d.getUTCFullYear().toString(), + MM: d.getUTCMonth() + 1, + MMM: m[d.getUTCMonth()], + DD: d.getUTCDate(), + hh: d.getUTCHours(), + mm: d.getUTCMinutes(), + ss: d.getUTCSeconds(), + h2: d.getUTCHours() % 12, + ap: d.getUTCHours() < 12 ? 'AM' : 'PM' + }; + return o.replace(/(YY(YY)?|MMM?|DD|hh|mm|ss|h2|ap)/g, n => ('0' + v[n]).substr(-n.length)); + }, + downloader: (function () { + let tasks = [], thread = 0, max_thread = 2, retry = 0, max_retry = 2, failed = 0, notifier, has_failed = false; + return { + add: function (task) { + tasks.push(task); + if (thread < max_thread) { + thread += 1; + this.next(); + } else this.update(); + }, + next: async function () { + let task = tasks.shift(); + await this.start(task); + if (tasks.length > 0 && thread <= max_thread) this.next(); + else thread -= 1; + this.update(); + }, + start: function (task) { + this.update(); + return new Promise(resolve => { + UsefulScriptGlobalPageContext.Utils.downloadBlobUrl(task.url, task.name) + .then(() => { + task.onload(); + resolve(); + }) + .catch(e => { + this.retry(task, e); + resolve(); + }) + }); + }, + retry: function (task, result) { + retry += 1; + if (retry == 3) max_thread = 1; + if (task.retry && task.retry >= max_retry || + result.details && result.details.current == 'USER_CANCELED') { + task.onerror(result); + failed += 1; + } else { + if (max_thread == 1) task.retry = (task.retry || 0) + 1; + this.add(task); + } + }, + update: function() { + if (!notifier) { + notifier = document.createElement('div'); + notifier.title = 'Twitter Media Downloader'; + notifier.classList.add('tmd-notifier'); + notifier.innerHTML = '|'; + document.body.appendChild(notifier); + } + if (failed > 0 && !has_failed) { + has_failed = true; + notifier.innerHTML += '|'; + let clear = document.createElement('label'); + notifier.appendChild(clear); + clear.onclick = () => { + notifier.innerHTML = '|'; + failed = 0; + has_failed = false; + this.update(); + }; + } + notifier.firstChild.innerText = thread; + notifier.firstChild.nextElementSibling.innerText = tasks.length; + if (failed > 0) notifier.lastChild.innerText = failed; + if (thread > 0 || tasks.length > 0 || failed > 0) notifier.classList.add('running'); + else notifier.classList.remove('running'); + } + }; + })(), + language: { + en: {download: 'Download', completed: 'Download Completed', settings: 'Settings', dialog: {title: 'Download Settings', save: 'Save', save_history: 'Remember download history', clear_history: '(Clear)', clear_confirm: 'Clear download history?', show_sensitive: 'Always show sensitive content', pattern: 'File Name Pattern'}}, + ja: {download: 'ダウンロード', completed: 'ダウンロード完了', settings: '設定', dialog: {title: 'ダウンロード設定', save: '保存', save_history: 'ダウンロード履歴を保存する', clear_history: '(クリア)', clear_confirm: 'ダウンロード履歴を削除する?', show_sensitive: 'センシティブな内容を常に表示する', pattern: 'ファイル名パターン'}}, + zh: {download: '下载', completed: '下载完成', settings: '设置', dialog: {title: '下载设置', save: '保存', save_history: '保存下载记录', clear_history: '(清除)', clear_confirm: '确认要清除下载记录?', show_sensitive: '自动显示敏感的内容', pattern: '文件名格式'}}, + 'zh-Hant': {download: '下載', completed: '下載完成', settings: '設置', dialog: {title: '下載設置', save: '保存', save_history: '保存下載記錄', clear_history: '(清除)', clear_confirm: '確認要清除下載記錄?', show_sensitive: '自動顯示敏感的内容', pattern: '文件名規則'}} + }, + css: ` + .tmd-down {margin-left: 12px; order: 99;} + .tmd-down:hover > div > div > div > div {color: rgba(29, 161, 242, 1.0);} + .tmd-down:hover > div > div > div > div > div {background-color: rgba(29, 161, 242, 0.1);} + .tmd-down:active > div > div > div > div > div {background-color: rgba(29, 161, 242, 0.2);} + .tmd-down:hover svg {color: rgba(29, 161, 242, 1.0);} + .tmd-down:hover div:first-child:not(:last-child) {background-color: rgba(29, 161, 242, 0.1);} + .tmd-down:active div:first-child:not(:last-child) {background-color: rgba(29, 161, 242, 0.2);} + .tmd-down.tmd-media {position: absolute; right: 0;} + .tmd-down.tmd-media > div {display: flex; border-radius: 99px; margin: 2px;} + .tmd-down.tmd-media > div > div {display: flex; margin: 6px; color: #fff;} + .tmd-down.tmd-media:hover > div {background-color: rgba(255,255,255, 0.6);} + .tmd-down.tmd-media:hover > div > div {color: rgba(29, 161, 242, 1.0);} + .tmd-down.tmd-media:not(:hover) > div > div {filter: drop-shadow(0 0 1px #000);} + .tmd-down g {display: none;} + .tmd-down.download g.download, .tmd-down.completed g.completed, .tmd-down.loading g.loading,.tmd-down.failed g.failed {display: unset;} + .tmd-down.loading svg {animation: spin 1s linear infinite;} + @keyframes spin {0% {transform: rotate(0deg);} 100% {transform: rotate(360deg);}} + .tmd-btn {display: inline-block; background-color: #1DA1F2; color: #FFFFFF; padding: 0 20px; border-radius: 99px;} + .tmd-tag {display: inline-block; background-color: #FFFFFF; color: #1DA1F2; padding: 0 10px; border-radius: 10px; border: 1px solid #1DA1F2; font-weight: bold; margin: 5px;} + .tmd-btn:hover {background-color: rgba(29, 161, 242, 0.9);} + .tmd-tag:hover {background-color: rgba(29, 161, 242, 0.1);} + .tmd-notifier {display: none; position: fixed; left: 16px; bottom: 16px; color: #000; background: #fff; border: 1px solid #ccc; border-radius: 8px; padding: 4px;} + .tmd-notifier.running {display: flex; align-items: center;} + .tmd-notifier label {display: inline-flex; align-items: center; margin: 0 8px;} + .tmd-notifier label:before {content: " "; width: 32px; height: 16px; background-position: center; background-repeat: no-repeat;} + .tmd-notifier label:nth-child(1):before {background-image:url("data:image/svg+xml;charset=utf8,");} + .tmd-notifier label:nth-child(2):before {background-image:url("data:image/svg+xml;charset=utf8,");} + .tmd-notifier label:nth-child(3):before {background-image:url("data:image/svg+xml;charset=utf8,");} + .tmd-down.tmd-img {position: absolute; right: 0; bottom: 0; display: none !important;} + .tmd-down.tmd-img > div {display: flex; border-radius: 99px; margin: 2px; background-color: rgba(255,255,255, 0.6);} + .tmd-down.tmd-img > div > div {display: flex; margin: 6px; color: #fff !important;} + .tmd-down.tmd-img:not(:hover) > div > div {filter: drop-shadow(0 0 1px #000);} + .tmd-down.tmd-img:hover > div > div {color: rgba(29, 161, 242, 1.0);} + :hover > .tmd-down.tmd-img, .tmd-img.loading, .tmd-img.completed, .tmd-img.failed {display: block !important;} + .tweet-detail-action-item {width: 20% !important;} + `, + css_ss: ` + /* show sensitive in media tab */ + li[role="listitem"]>div>div>div>div:not(:last-child) {filter: none;} + li[role="listitem"]>div>div>div>div+div:last-child {display: none;} + `, + svg: ` + + + + + ` + }; + })(); + + TMD.init(); + }, +}; diff --git a/scripts/twitter_downloadButton.png b/scripts/twitter_downloadButton.png new file mode 100644 index 0000000000000000000000000000000000000000..bbbe3077fead67235a2f65554776e81b8c4f7ea3 GIT binary patch literal 125846 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℘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ℓ#H~vi*|TesQi(lJ67edPVsjQLbk!*E2TTJ(}$r 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 C`>lll literal 0 HcmV?d00001 diff --git a/scripts/youtube_downloadVideo.js b/scripts/youtube_downloadVideo.js index e38654b4..7ca13cb1 100644 --- a/scripts/youtube_downloadVideo.js +++ b/scripts/youtube_downloadVideo.js @@ -12,9 +12,9 @@ export default { onClick: function () { // https://stackoverflow.com/a/8260383/11898496 function getIdFromYoutubeURL(url) { - var regExp = + let regExp = /.*(?:youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=)([^#\&\?]*).*/; - var match = url.match(regExp); + let match = url.match(regExp); return match && match[1].length == 11 ? match[1] : false; } diff --git a/scripts/youtube_localDownloader.html b/scripts/youtube_localDownloader.html new file mode 100644 index 00000000..e710c44e --- /dev/null +++ b/scripts/youtube_localDownloader.html @@ -0,0 +1,14 @@ + + + + + + + Useful Script - YouTube Local Downloader + + + + + + + diff --git a/scripts/youtube_localDownloader.js b/scripts/youtube_localDownloader.js index bcd46d02..4db11b90 100644 --- a/scripts/youtube_localDownloader.js +++ b/scripts/youtube_localDownloader.js @@ -1,427 +1,33 @@ +import { runScriptInCurrentTab } from "./helpers/utils.js"; + export default { icon: "https://www.youtube.com/s/desktop/accca349/img/favicon_48x48.png", name: { en: "Youtube local downloader", - vi: "Youtube tải video", + vi: "Youtube tải video local", }, description: { en: "", vi: "", }, - whiteList: ["https://*.youtube.com/*"], - - onClick: () => {}, - - onDocumentStart: async () => { - const app = {}; - const $ = (s, x = document) => x.querySelector(s); - - // hook fetch response - const ff = fetch; - window.fetch = (...args) => { - if (args[0] instanceof Request) { - return ff(...args).then((resp) => { - if (resp.url.includes("player")) { - resp.clone().json().then(load); - } - return resp; - }); - } - return ff(...args); - }; - - async function load(playerResponse) { - try { - const basejs = - (typeof ytplayer !== "undefined" && - "config" in ytplayer && - ytplayer.config.assets - ? "https://" + location.host + ytplayer.config.assets.js - : "web_player_context_config" in ytplayer - ? "https://" + - location.host + - ytplayer.web_player_context_config.jsUrl - : null) || $('script[src$="base.js"]').src; - const res = await fetch(basejs); - const text = await res.text(); - const decsig = parseDecsig(text); - const id = parseQuery(location.search).v; - const data = parseResponse(id, playerResponse, decsig); - console.log("video loaded: %s", id); - app.isLiveStream = - data.playerResponse.playabilityStatus.liveStreamability != null; - app.id = id; - app.stream = data.stream; - app.adaptive = data.adaptive; - app.details = data.details; - console.log(app); - } catch (err) { - alert( - "Failed to get video infomation for unknown reason, refresh the page may work." - ); - console.error("load", err); - } - } - - const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - const parseDecsig = (data) => { - try { - if (data.startsWith("var script")) { - // they inject the script via script tag - const obj = {}; - const document = { - createElement: () => obj, - head: { appendChild: () => {} }, - }; - eval(data); - data = obj.innerHTML; - } - const fnnameresult = /=([a-zA-Z0-9\$_]+?)\(decodeURIComponent/.exec( - data - ); - const fnname = fnnameresult[1]; - const _argnamefnbodyresult = new RegExp( - escapeRegExp(fnname) + "=function\\((.+?)\\){((.+)=\\2.+?)}" - ).exec(data); - const [_, argname, fnbody] = _argnamefnbodyresult; - const helpernameresult = /;([a-zA-Z0-9$_]+?)\..+?\(/.exec(fnbody); - const helpername = helpernameresult[1]; - const helperresult = new RegExp( - "var " + escapeRegExp(helpername) + "={[\\s\\S]+?};" - ).exec(data); - const helper = helperresult[0]; - console.log( - `parsedecsig result: %s=>{%s\n%s}`, - argname, - helper, - fnbody - ); - return new Function([argname], helper + "\n" + fnbody); - } catch (e) { - console.error("parsedecsig error: %o", e); - console.info("script content: %s", data); - console.info( - 'If you encounter this error, please copy the full "script content" to https://pastebin.com/ for me.' - ); - } - }; - const parseQuery = (s) => - [...new URLSearchParams(s).entries()].reduce( - (acc, [k, v]) => ((acc[k] = v), acc), - {} - ); - const parseResponse = (id, playerResponse, decsig) => { - console.log(`video %s playerResponse: %o`, id, playerResponse); - let stream = []; - if (playerResponse.streamingData.formats) { - stream = playerResponse.streamingData.formats.map((x) => - Object.assign({}, x, parseQuery(x.cipher || x.signatureCipher)) - ); - console.log(`video %s stream: %o`, id, stream); - for (const obj of stream) { - if (obj.s) { - obj.s = decsig(obj.s); - obj.url += `&${obj.sp}=${encodeURIComponent(obj.s)}`; - } - } - } + whiteList: ["https://*youtube.com/*"], - let adaptive = []; - if (playerResponse.streamingData.adaptiveFormats) { - adaptive = playerResponse.streamingData.adaptiveFormats.map((x) => - Object.assign({}, x, parseQuery(x.cipher || x.signatureCipher)) - ); - console.log(`video %s adaptive: %o`, id, adaptive); - for (const obj of adaptive) { - if (obj.s) { - obj.s = decsig(obj.s); - obj.url += `&${obj.sp}=${encodeURIComponent(obj.s)}`; - } - } - } - console.log(`video %s result: %o`, id, { stream, adaptive }); - return { - stream, - adaptive, - details: playerResponse.videoDetails, - playerResponse, - }; - }; - - window.addEventListener("load", () => { - const firstResp = window?.ytplayer?.config?.args?.raw_player_response; - if (firstResp) { - load(firstResp); - } + onClickExtension: async () => { + const yt_data = await runScriptInCurrentTab(() => { + return document.getElementsByTagName("ytd-app")[0].data.playerResponse; }); - // ========================== video downloader ========================== - UsefulScriptGlobalPageContext.DOM.injectScriptSrc( - "https://unpkg.com/@ffmpeg/ffmpeg@0.6.1/dist/ffmpeg.min.js" - ); - UsefulScriptGlobalPageContext.DOM.injectScriptSrc( - "https://unpkg.com/vue@2.6.10/dist/vue.js" - ); - - async function downloadBlobUrlWithProgressMultiChunk( - url, - progressCallback, - chunkSize = 65536 - ) { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Error: ${response.status} - ${response.statusText}`); - } - const contentLength = response.headers.get("content-length"); - const total = parseInt(contentLength, 10); - let loaded = 0; - const startTime = Date.now(); - - const chunks = []; - const numChunks = Math.ceil(total / chunkSize); - const promises = []; - - for (let i = 0; i < numChunks; i++) { - const start = i * chunkSize; - const end = Math.min(start + chunkSize - 1, total - 1); - promises.push( - fetch(url + `&range=${start}-${end}`).then((res) => res.blob()) - ); - } - - await Promise.all(promises) - .then((downloadedChunks) => { - chunks.push(...downloadedChunks); - loaded = total; - progressCallback?.({ - loaded, - total, - speed: loaded / ((Date.now() - startTime + 1) / 1000), - }); - }) - .catch((error) => { - console.error("Download error:", error); - }); - - return new Blob(chunks, { - type: response.headers.get("content-type"), - }); + if (!yt_data) { + alert("Không tìm thấy video data"); + return; } - const xhrDownloadUint8Array = async ( - { url, contentLength }, - progressCb - ) => { - if (typeof contentLength === "string") - contentLength = parseInt(contentLength); - - progressCb({ - loaded: 0, - total: contentLength, - speed: 0, - }); - - const chunkSize = 65536; - const getBuffer = async (start, end) => { - let res = await fetch(url + `&range=${start}-${end ? end - 1 : ""}`); - return await res.arrayBuffer(); - }; - - const data = new Uint8Array(contentLength); - let downloaded = 0; - const startTime = Date.now(); - for (let start = 0; start < contentLength; start += chunkSize) { - try { - const exceeded = start + chunkSize > contentLength; - const curChunkSize = exceeded ? contentLength - start : chunkSize; - const end = exceeded ? null : start + chunkSize; - const buf = await getBuffer(start, end); - console.log("dl done", url, start, end); - downloaded += curChunkSize; - data.set(new Uint8Array(buf), start); - const ds = (Date.now() - startTime + 1) / 1000; - progressCb({ - loaded: downloaded, - total: contentLength, - speed: downloaded / ds, - }); - } catch (e) { - console.log("Download error", e); - } - } - return data; - }; - - let ffWorker; - const mergeVideo = async (video, audio) => { - if (!ffWorker) { - ffWorker = FFmpeg.createWorker({ - logger: DEBUG ? (m) => logger.log(m.message) : () => {}, - }); - await ffWorker.load(); - } - await ffWorker.write("video.mp4", video); - await ffWorker.write("audio.mp4", audio); - await ffWorker.run("-i video.mp4 -i audio.mp4 -c copy output.mp4", { - input: ["video.mp4", "audio.mp4"], - output: "output.mp4", - }); - const { data } = await ffWorker.read("output.mp4"); - await ffWorker.remove("output.mp4"); - return data; - }; - - const triggerDownload = (url, filename) => { - const a = document.createElement("a"); - a.href = url; - a.download = filename; - document.body.appendChild(a); - a.click(); - a.remove(); - }; - - const dlModalTemplate = ` -
    -
    Merging video, please wait...
    -
    -
    -

    Video

    - -
    - {{video.speed}} kB/s - {{video.loaded}}/{{video.total}} MB -
    -
    -
    -

    Audio

    - -
    - {{audio.speed}} kB/s - {{audio.loaded}}/{{audio.total}} MB -
    -
    -
    -
    -`; - - async function openDownloadModel(adaptive, title) { - const win = open( - "", - "Video Download", - `toolbar=no,height=400,width=400,left=${screenLeft},top=${screenTop}` - ); - const div = win.document.createElement("div"); - win.document.body.appendChild(div); - win.document.title = `Downloading "${title}"`; - const dlModalApp = new Vue({ - template: dlModalTemplate, - data() { - return { - video: { - progress: 0, - total: 0, - loaded: 0, - speed: 0, - }, - audio: { - progress: 0, - total: 0, - loaded: 0, - speed: 0, - }, - merging: false, - }; - }, - methods: { - async start(adaptive, title) { - win.onbeforeunload = () => true; - // YouTube's default order is descending by video quality - const videoObj = adaptive - .filter( - (x) => - x.mimeType.includes("video/mp4") || - x.mimeType.includes("video/webm") - ) - .map((v) => { - const [_, quality, fps] = /(\d+)p(\d*)/.exec(v.qualityLabel); - v.qualityNum = parseInt(quality); - v.fps = fps ? parseInt(fps) : 30; - return v; - }) - .sort((a, b) => { - if (a.qualityNum === b.qualityNum) return b.fps - a.fps; // ex: 30-60=-30, then a will be put before b - return b.qualityNum - a.qualityNum; - })[0]; - const audioObj = adaptive.find((x) => - x.mimeType.includes("audio/mp4") - ); - const vPromise = xhrDownloadUint8Array(videoObj, (e) => { - this.video.progress = (e.loaded / e.total) * 100; - this.video.loaded = (e.loaded / 1024 / 1024).toFixed(2); - this.video.total = (e.total / 1024 / 1024).toFixed(2); - this.video.speed = (e.speed / 1024).toFixed(2); - }); - const aPromise = xhrDownloadUint8Array(audioObj, (e) => { - this.audio.progress = (e.loaded / e.total) * 100; - this.audio.loaded = (e.loaded / 1024 / 1024).toFixed(2); - this.audio.total = (e.total / 1024 / 1024).toFixed(2); - this.audio.speed = (e.speed / 1024).toFixed(2); - }); - - // const vPromise = downloadBlobUrlWithProgressMultiChunk( - // videoObj.url, - // ({ loaded, total, speed }) => { - // this.video.progress = (loaded / total) * 100; - // this.video.loaded = (loaded / 1024 / 1024).toFixed(2); - // this.video.total = (total / 1024 / 1024).toFixed(2); - // this.video.speed = (speed / 1024).toFixed(2); - // } - // ); - // const aPromise = downloadBlobUrlWithProgressMultiChunk( - // audioObj.url, - // ({ loaded, total, speed }) => { - // this.audio.progress = (loaded / total) * 100; - // this.audio.loaded = (loaded / 1024 / 1024).toFixed(2); - // this.audio.total = (total / 1024 / 1024).toFixed(2); - // this.audio.speed = (speed / 1024).toFixed(2); - // } - // ); - const [varr, aarr] = await Promise.all([vPromise, aPromise]); - this.merging = true; - win.onunload = () => { - // trigger download when user close it - const bvurl = URL.createObjectURL(new Blob([varr])); - const baurl = URL.createObjectURL(new Blob([aarr])); - triggerDownload(bvurl, title + "-videoonly.mp4"); - triggerDownload(baurl, title + "-audioonly.mp4"); - }; - const result = await Promise.race([ - mergeVideo(varr, aarr), - sleep(1000 * 25).then(() => null), - ]); - if (!result) { - alert("An error has occurred when merging video"); - const bvurl = URL.createObjectURL(new Blob([varr])); - const baurl = URL.createObjectURL(new Blob([aarr])); - triggerDownload(bvurl, title + "-videoonly.mp4"); - triggerDownload(baurl, title + "-audioonly.mp4"); - return this.close(); - } - this.merging = false; - const url = URL.createObjectURL(new Blob([result])); - triggerDownload(url, title + ".mp4"); - win.onbeforeunload = null; - win.onunload = null; - win.close(); - }, - }, - }).$mount(div); - dlModalApp.start(adaptive, title); - } + localStorage.setItem( + "ufs_youtube_localDownloader", + JSON.stringify(yt_data) + ); - window.ufs_download_video = () => { - openDownloadModel(app.adaptive, app.details?.title); - }; + window.open("/scripts/youtube_localDownloader.html"); }, }; diff --git a/scripts/youtube_localDownloader_main.js b/scripts/youtube_localDownloader_main.js new file mode 100644 index 00000000..3ff9248f --- /dev/null +++ b/scripts/youtube_localDownloader_main.js @@ -0,0 +1,96 @@ +function formatSize(size) { + size = Number(size); + + if (!size) return "?"; + + // format to KB, MB, GB + if (size < 1024) { + return size + "B"; + } + if (size < 1024 * 1024) { + return (size / 1024).toFixed(2) + "KB"; + } + if (size < 1024 * 1024 * 1024) { + return (size / (1024 * 1024)).toFixed(2) + "MB"; + } + return (size / (1024 * 1024 * 1024)).toFixed(2) + "GB"; +} + +window.onload = () => { + const yt_data = JSON.parse( + localStorage.getItem("ufs_youtube_localDownloader") ?? "{}" + ); + + console.log(yt_data); + + const streamingData = [ + ...(yt_data.streamingData?.formats || []), + ...(yt_data.streamingData?.adaptiveFormats || []), + ].filter((_) => _.url); + + const videoDetails = yt_data.videoDetails; + const videos = streamingData.filter((d) => d.mimeType.includes("video")); + const audios = streamingData.filter((d) => d.mimeType.includes("audio")); + const captions = + yt_data.captions?.playerCaptionsTracklistRenderer?.captionTracks || []; + + console.log(captions); + + // video details + const detailDiv = document.createElement("div"); + const thumbs = videoDetails.thumbnail?.thumbnails || []; + const lastThumbnail = thumbs[thumbs.length - 1]; + + detailDiv.innerHTML = ` + +

    ${videoDetails.title}

    +

    ${videoDetails.author}

    +

    ${videoDetails.shortDescription}

    `; + document.body.appendChild(detailDiv); + + // video + for (let video of videos) { + const div = document.createElement("div"); + div.innerHTML = ` +
    +

    + ${video.qualityLabel} + - ${video.width}x${video.height} + - ${formatSize(video.contentLength)} + ${video.audioQuality ? "" : " (no audio)"} +

    +
    + `; + document.body.appendChild(div); + } + + // audio + for (let audio of audios) { + const div = document.createElement("div"); + div.innerHTML = ` + +

    + ${audio.audioTrack?.displayName || audio.audioQuality} + - ${formatSize(audio.contentLength)} +

    + `; + document.body.appendChild(div); + } + + // caption + for (let caption of captions) { + const div = document.createElement("div"); + div.innerHTML = ` +

    ${caption.name?.simpleText}

    + + Download + + `; + document.body.appendChild(div); + } +}; From 3f8ef170163195c295925f5c82604fef3af9d99a Mon Sep 17 00:00:00 2001 From: HoangTran <99.hoangtran@gmail.com> Date: Mon, 1 Apr 2024 00:36:32 +0700 Subject: [PATCH 28/40] fix bugs --- scripts/content-scripts/run_scripts.js | 23 +++++++------ .../scripts/ufs_global_webpage_context.js | 20 ++++++++++- scripts/douyin_downloadAllVideoUser.js | 25 ++++++++------ scripts/douyin_downloadWachingVideo.js | 34 +++++++++++++++---- scripts/helpers/utils.js | 23 +++++++------ scripts/tiktok_downloadWatchingVideo.js | 2 ++ scripts/youtube_toggleLight.js | 5 +-- 7 files changed, 93 insertions(+), 39 deletions(-) diff --git a/scripts/content-scripts/run_scripts.js b/scripts/content-scripts/run_scripts.js index c8f6bbb8..746020d6 100644 --- a/scripts/content-scripts/run_scripts.js +++ b/scripts/content-scripts/run_scripts.js @@ -66,8 +66,8 @@ function checkWillRun(script) { let url = location.href; let hasWhiteList = script.whiteList?.length > 0; let hasBlackList = script.blackList?.length > 0; - let inWhiteList = matchPatterns(url, script.whiteList || []); - let inBlackList = matchPatterns(url, script.blackList || []); + let inWhiteList = matchOneOfPatterns(url, script.whiteList || []); + let inBlackList = matchOneOfPatterns(url, script.blackList || []); return ( (!hasWhiteList && !hasBlackList) || (hasWhiteList && inWhiteList) || @@ -75,14 +75,17 @@ function checkWillRun(script) { ); } -function matchPatterns(url, patterns) { +function matchOneOfPatterns(url, patterns) { for (let pattern of patterns) { - // Replace wildcard characters * with regex wildcard .* - const regexRule = pattern.replace(/\*/g, ".*"); - // Create a regex pattern from the rule - const reg = new RegExp("^" + regexRule + "$"); - // Check if the URL matches the pattern - if (!reg.test(url)) return false; + const regex = new RegExp( + "^" + + pattern + .split("*") + .map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")) + .join(".*") + + "$" + ); + if (regex.test(url)) return true; } - return true; + return false; } diff --git a/scripts/content-scripts/scripts/ufs_global_webpage_context.js b/scripts/content-scripts/scripts/ufs_global_webpage_context.js index 7f7a0c6a..4734d82e 100644 --- a/scripts/content-scripts/scripts/ufs_global_webpage_context.js +++ b/scripts/content-scripts/scripts/ufs_global_webpage_context.js @@ -223,6 +223,24 @@ const UsefulScriptGlobalPageContext = { }, }, Utils: { + formatSize(size, fixed = 0) { + size = Number(size); + + if (!size) return "?"; + + // format to KB, MB, GB + if (size < 1024) { + return size + "B"; + } + if (size < 1024 * 1024) { + return (size / 1024).toFixed(fixed) + "KB"; + } + if (size < 1024 * 1024 * 1024) { + return (size / (1024 * 1024)).toFixed(fixed) + "MB"; + } + return (size / (1024 * 1024 * 1024)).toFixed(fixed) + "GB"; + }, + // modified by chatgpt based on: https://gist.github.com/jcouyang/632709f30e12a7879a73e9e132c0d56b promiseAllStepN(n, list) { const head = list.slice(0, n); @@ -394,7 +412,7 @@ const UsefulScriptGlobalPageContext = { alert("Error: " + error); } }, - async downloadBlobUrlWithProgress(url, progressCallback) { + async getBlobFromUrlWithProgress(url, progressCallback) { const response = await fetch(url, {}); if (!response.ok) { throw new Error(`Error: ${response.status} - ${response.statusText}`); diff --git a/scripts/douyin_downloadAllVideoUser.js b/scripts/douyin_downloadAllVideoUser.js index b4aaa55d..71e6744f 100644 --- a/scripts/douyin_downloadAllVideoUser.js +++ b/scripts/douyin_downloadAllVideoUser.js @@ -32,8 +32,8 @@ export default { "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin", }, - referrer: - "https://www.douyin.com/user/MS4wLjABAAAA5A-hCBCTdv102baOvaoZqg7nCIW_Bn_YBA0Aiz9uYPY", + referrer: location.href, + // "https://www.douyin.com/user/MS4wLjABAAAA5A-hCBCTdv102baOvaoZqg7nCIW_Bn_YBA0Aiz9uYPY", referrerPolicy: "strict-origin-when-cross-origin", body: null, method: "GET", @@ -69,17 +69,22 @@ export default { console.log(moredata); hasMore = moredata["has_more"]; max_cursor = moredata["max_cursor"]; - for (var item of moredata["aweme_list"]) { - let url = item.video.play_addr.url_list[0]; + for (var item of moredata["aweme_list"] || []) { + try { + let url = item.video.play_addr.url_list[0]; - if (url.startsWith("https")) { - result.push(url); - } else { - result.push(url.replace("http", "https")); + if (url.startsWith("https")) { + result.push(url); + } else { + result.push(url.replace("http", "https")); + } + // console.clear(); + console.log("Number of videos: " + result.length); + } catch (e) { + console.log("ERROR: ", e); } - // console.clear(); - console.log("Number of videos: " + result.length); } + console.log(hasMore); } saveToFile(result.join("\n")); alert( diff --git a/scripts/douyin_downloadWachingVideo.js b/scripts/douyin_downloadWachingVideo.js index 47589df3..826d19e4 100644 --- a/scripts/douyin_downloadWachingVideo.js +++ b/scripts/douyin_downloadWachingVideo.js @@ -1,4 +1,4 @@ -import { runScriptInCurrentTab } from "./helpers/utils.js"; +import { runScriptInCurrentTab, showLoading } from "./helpers/utils.js"; export default { icon: "https://www.douyin.com/favicon.ico", @@ -13,8 +13,16 @@ export default { whiteList: ["https://www.douyin.com/*"], onClickExtension: async function () { - const { downloadURL, downloadBlobUrl } = - UsefulScriptGlobalPageContext.Utils; + const { + downloadURL, + downloadBlob, + getBlobFromUrlWithProgress, + formatSize, + } = UsefulScriptGlobalPageContext.Utils; + + const { closeLoading, setLoadingText } = showLoading( + "Đang tìm video url..." + ); const src = await runScriptInCurrentTab(async () => { return await UsefulScriptGlobalPageContext.DOM.getWatchingVideoSrc(); @@ -22,9 +30,23 @@ export default { if (!src) { alert("Không tìm thấy video nào."); - return; + } else { + setLoadingText("Đang tải video..."); + downloadURL(src, "douyin_video.mp4"); + // const blob = await getBlobFromUrlWithProgress( + // src, + // ({ loaded, total, speed }) => { + // const percent = ((loaded / total) * 100) | 0; + // setLoadingText( + // `Đang tải video...
    ` + + // `Vui lòng không tắt popup
    ` + + // `${formatSize(loaded)}/${formatSize(total)} (${percent}%)` + + // ` - ${formatSize(speed)}/s` + // ); + // } + // ); + // await downloadBlob(blob, "douyin_video.mp4"); } - - downloadBlobUrl(src, "douyin_video.mp4"); + closeLoading(); }, }; diff --git a/scripts/helpers/utils.js b/scripts/helpers/utils.js index 0f7efbdc..398e94f3 100644 --- a/scripts/helpers/utils.js +++ b/scripts/helpers/utils.js @@ -156,8 +156,8 @@ export function checkBlackWhiteList(script, url) { b = script.blackList || [], hasWhiteList = w.length > 0, hasBlackList = b.length > 0, - inWhiteList = matchPatterns(url, w) ?? true, - inBlackList = matchPatterns(url, b) ?? false; + inWhiteList = matchOneOfPatterns(url, w) ?? true, + inBlackList = matchOneOfPatterns(url, b) ?? false; let willRun = (!hasWhiteList && !hasBlackList) || @@ -167,16 +167,19 @@ export function checkBlackWhiteList(script, url) { return willRun; } -function matchPatterns(url, patterns) { +function matchOneOfPatterns(url, patterns) { for (let pattern of patterns) { - // Replace wildcard characters * with regex wildcard .* - const regexRule = pattern.replace(/\*/g, ".*"); - // Create a regex pattern from the rule - const reg = new RegExp("^" + regexRule + "$"); - // Check if the URL matches the pattern - if (!reg.test(url)) return false; + const regex = new RegExp( + "^" + + pattern + .split("*") + .map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")) + .join(".*") + + "$" + ); + if (regex.test(url)) return true; } - return true; + return false; } // https://stackoverflow.com/a/68634884/11898496 diff --git a/scripts/tiktok_downloadWatchingVideo.js b/scripts/tiktok_downloadWatchingVideo.js index ccb6384e..ad541fcd 100644 --- a/scripts/tiktok_downloadWatchingVideo.js +++ b/scripts/tiktok_downloadWatchingVideo.js @@ -11,6 +11,8 @@ export default { vi: "Tải video tiktok bạn đang xem (không watermark)", }, + whiteList: ["https://www.tiktok.com/*"], + onClickExtension: async function () { const { closeLoading, setLoadingText } = showLoading("Đang lấy video id.."); diff --git a/scripts/youtube_toggleLight.js b/scripts/youtube_toggleLight.js index d4b1307e..4c8056b5 100644 --- a/scripts/youtube_toggleLight.js +++ b/scripts/youtube_toggleLight.js @@ -8,7 +8,7 @@ export default { en: "Toggle light on/off to focus to youtube video", vi: "Tắt/Mở đèn để tập trung xem video youtube", }, - whiteList: ["*://www.youtube.com/*"], + whiteList: ["*://www.youtube.com/*"], onClick: function () { ["#below", "#secondary", "#masthead-container"].forEach((_) => { @@ -21,8 +21,9 @@ export default { }); }); - document.querySelector("#player-theater-container")?.scrollIntoView?.({ + document.querySelector("ytd-player")?.scrollIntoView?.({ behavior: "smooth", + block: "center", }); }, }; From 5427bd836407bb8a643ef9931c4b97703022f100 Mon Sep 17 00:00:00 2001 From: HoangTran <99.hoangtran@gmail.com> Date: Mon, 1 Apr 2024 00:48:08 +0700 Subject: [PATCH 29/40] update --- working_note.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/working_note.md b/working_note.md index d30bb3d2..c8ecf8f0 100644 --- a/working_note.md +++ b/working_note.md @@ -14,7 +14,7 @@ - [ ] Test thử [rapid api](https://rapidapi.com/) -- [ ] **(IN-PROGRESS)** Youtube local download => lấy data từ ytplayer.config.args.raw_player_response +- [ ] **(IN-PROGRESS)** Youtube local download => lấy data từ ytplayer.config.args.raw_player_response => Làm UI - [ ] html2img khá ngon, nhưng chưa biết xài vô cái gì From a24d9601605965d7f3ed4425d682546bec31c3bb Mon Sep 17 00:00:00 2001 From: "hoang.tran12" Date: Mon, 1 Apr 2024 10:51:59 +0700 Subject: [PATCH 30/40] update zing --- scripts/zingmp3_downloadMusic.js | 34 ++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/scripts/zingmp3_downloadMusic.js b/scripts/zingmp3_downloadMusic.js index c9500fdd..9d1b7f95 100644 --- a/scripts/zingmp3_downloadMusic.js +++ b/scripts/zingmp3_downloadMusic.js @@ -1,3 +1,5 @@ +import { getCurrentTab } from "./helpers/utils.js"; + export default { icon: "https://zjs.zmdcdn.me/zmp3-desktop/releases/v1.7.64/static/media/icon_zing_mp3_60.f6b51045.svg", name: { @@ -227,16 +229,40 @@ export default { }; (async function () { - // window.open(await ZingMp3.search('game thủ liên minh')); + // window.open(await ZingMp3.search("game thủ liên minh")); // window.open(await ZingMp3.getLastPlaying()); // window.open(await ZingMp3.getHome()); // window.open(await ZingMp3.getChartHome()); - // window.open(await ZingMp3.getInfoMusic('ZWFE8OUO')) + // window.open(await ZingMp3.getInfoMusic("ZWFE8OUO")); + // window.open(await ZingMp3.getStreaming("Z6WZD78I")); - let url = prompt("Nhap link bai hat: ", location.href); + const tab = await getCurrentTab(); + let url = prompt("Nhap link bai hat: ", tab.url); if (url) { let songid = ZingMp3.getSongIdFromURL(url); - if (songid) window.open(await ZingMp3.getStreaming(songid)); + if (songid) { + try { + const streamUrl = await ZingMp3.getStreaming(songid); + const res = await fetch(streamUrl); + const json = await res.json(); + console.log(json); + if (json.err) throw new Error(json.msg); + if (!json.data) throw new Error("No Data"); + + let options = Object.keys(json.data); + let choice = prompt( + "Chọn chất lượng nhạc: \n" + options.join("\n"), + options[options.length - 1] + ); + + if (choice !== null) { + let url = json.data[choice]; + window.open(url); + } + } catch (error) { + alert("ERROR: " + error); + } + } } })(); }, From fbb08976e721e13c1222daea0b156e5d25599548 Mon Sep 17 00:00:00 2001 From: "hoang.tran12" Date: Mon, 1 Apr 2024 10:52:16 +0700 Subject: [PATCH 31/40] optimize --- scripts/content-scripts/document_start.js | 6 +-- scripts/content-scripts/utils.js | 48 ----------------------- 2 files changed, 2 insertions(+), 52 deletions(-) delete mode 100644 scripts/content-scripts/utils.js diff --git a/scripts/content-scripts/document_start.js b/scripts/content-scripts/document_start.js index ce42eb60..17f5c0ed 100644 --- a/scripts/content-scripts/document_start.js +++ b/scripts/content-scripts/document_start.js @@ -19,7 +19,7 @@ // run all scripts that has onDocumentStart event (async () => { - injectScript( + import( chrome.runtime.getURL( "/scripts/content-scripts/scripts/ufs_global_webpage_context.js" ) @@ -38,9 +38,7 @@ }) ); - injectScript( - chrome.runtime.getURL("/scripts/content-scripts/run_scripts.js") - ); + import(chrome.runtime.getURL("/scripts/content-scripts/run_scripts.js")); })(); // Run script on user click (if clicked script has onClickContentScript event) diff --git a/scripts/content-scripts/utils.js b/scripts/content-scripts/utils.js deleted file mode 100644 index 2d61fc57..00000000 --- a/scripts/content-scripts/utils.js +++ /dev/null @@ -1,48 +0,0 @@ -export function getURL(fileName) { - return "/scripts/" + fileName; -} - -export function injectScript( - filePathOrUrl, - type = "application/javascript", - isExternal = false -) { - try { - var s = document.createElement("script"); - s.src = isExternal ? filePathOrUrl : chrome.runtime.getURL(filePathOrUrl); - s.type = type; - s.onload = function () { - console.log("Useful-scripts injected " + s.src); - this.remove(); - }; - s.onerror = function (e) { - console.log("ERROR: Useful-scripts inject script FAILED " + s.src, e); - this.remove(); - }; - (document.head || document.documentElement).appendChild(s); - } catch (e) { - console.log( - "ERROR: Useful-scripts inject script FAILED " + filePathOrUrl, - e - ); - } -} - -// TODO: https://developer.chrome.com/docs/extensions/reference/scripting/#method-insertCSS -// https://stackoverflow.com/a/17840622 -export function injectCss(url_file_code, type = "file") { - if (type === "file" || type === "url") { - var css = document.createElement("link"); - css.rel = "stylesheet"; - css.href = - type === "file" ? chrome.runtime.getURL(url_file_code) : url_file_code; - document.head.appendChild(css); - console.log("Useful-scripts injected " + css.href); - } else if (type === "code") { - var css = document.createElement("style"); - if ("textContent" in css) css.textContent = url_file_code; - else css.innerText = url_file_code; - document.head.appendChild(css); - console.log("Useful-scripts injected " + css); - } -} From 759efa941f61179fe5e016636950444d2b132c41 Mon Sep 17 00:00:00 2001 From: "hoang.tran12" Date: Mon, 1 Apr 2024 11:37:19 +0700 Subject: [PATCH 32/40] oh no --- working_note.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/working_note.md b/working_note.md index c8ecf8f0..d2dff8d6 100644 --- a/working_note.md +++ b/working_note.md @@ -25,3 +25,5 @@ - [x] làm cho xong soundcloud_downloadMusic hoặc xóa => Xong rồi, Ngon - [ ] Move transfer.sh sang popup + +- [ ] text to qrcode không còn hoạt động From 4ddf80fe8b97f76f349fdf5372a8d9d4e863f9f4 Mon Sep 17 00:00:00 2001 From: "hoang.tran12" Date: Mon, 1 Apr 2024 17:44:07 +0700 Subject: [PATCH 33/40] WIP --- scripts/youtube_localDownloader.html | 2 + scripts/youtube_localDownloader_main.js | 83 +++++++++++++++++-------- 2 files changed, 58 insertions(+), 27 deletions(-) diff --git a/scripts/youtube_localDownloader.html b/scripts/youtube_localDownloader.html index e710c44e..a6def7d3 100644 --- a/scripts/youtube_localDownloader.html +++ b/scripts/youtube_localDownloader.html @@ -5,6 +5,8 @@ Useful Script - YouTube Local Downloader + + diff --git a/scripts/youtube_localDownloader_main.js b/scripts/youtube_localDownloader_main.js index 3ff9248f..74dd443b 100644 --- a/scripts/youtube_localDownloader_main.js +++ b/scripts/youtube_localDownloader_main.js @@ -1,20 +1,50 @@ -function formatSize(size) { - size = Number(size); +const { formatSize, promiseAllStepN } = UsefulScriptGlobalPageContext.Utils; +const { injectScriptSrc } = UsefulScriptGlobalPageContext.DOM; - if (!size) return "?"; +const xhrDownloadUint8Array = async ({ url, contentLength }, progressCb) => { + if (typeof contentLength === "string") + contentLength = parseInt(contentLength); + progressCb({ + loaded: 0, + total: contentLength, + speed: 0, + }); + const chunkSize = 65536; + const getBuffer = (start, end) => + fetch(url + `&range=${start}-${end ? end - 1 : ""}`).then((r) => + r.arrayBuffer() + ); + const data = new Uint8Array(contentLength); + let downloaded = 0; + const tasks = []; + const startTime = Date.now(); - // format to KB, MB, GB - if (size < 1024) { - return size + "B"; + for (let start = 0; start < contentLength; start += chunkSize) { + const exceeded = start + chunkSize > contentLength; + const curChunkSize = exceeded ? contentLength - start : chunkSize; + const end = exceeded ? null : start + chunkSize; + tasks.push(() => { + console.log("dl start", url, start, end); + return getBuffer(start, end) + .then((buf) => { + console.log("dl done", url, start, end); + downloaded += curChunkSize; + data.set(new Uint8Array(buf), start); + const ds = (Date.now() - startTime + 1) / 1000; + progressCb({ + loaded: downloaded, + total: contentLength, + speed: downloaded / ds, + }); + }) + .catch((err) => { + console.log("Download error"); + }); + }); } - if (size < 1024 * 1024) { - return (size / 1024).toFixed(2) + "KB"; - } - if (size < 1024 * 1024 * 1024) { - return (size / (1024 * 1024)).toFixed(2) + "MB"; - } - return (size / (1024 * 1024 * 1024)).toFixed(2) + "GB"; -} + await promiseAllStepN(6, tasks); + return data; +}; window.onload = () => { const yt_data = JSON.parse( @@ -50,20 +80,19 @@ window.onload = () => { // video for (let video of videos) { - const div = document.createElement("div"); - div.innerHTML = ` - -

    + const button = document.createElement("button"); + button.style = "display:block;margin-bottom:5px"; + button.innerHTML = ` ${video.qualityLabel} - ${video.width}x${video.height} - - ${formatSize(video.contentLength)} - ${video.audioQuality ? "" : " (no audio)"} -

    -
    - `; - document.body.appendChild(div); + - ${formatSize(video.contentLength, 2)} + ${video.audioQuality ? "" : " (no audio)"}`; + button.onclick = () => { + let data = xhrDownloadUint8Array(video, (progress) => { + console.log(progress); + }); + }; + document.body.appendChild(button); } // audio @@ -73,7 +102,7 @@ window.onload = () => {

    ${audio.audioTrack?.displayName || audio.audioQuality} - - ${formatSize(audio.contentLength)} + - ${formatSize(audio.contentLength, 2)}

    `; document.body.appendChild(div); From eaa57653171e22fcec47c822ad4cb291628f4f22 Mon Sep 17 00:00:00 2001 From: "hoang.tran12" Date: Mon, 1 Apr 2024 18:26:49 +0700 Subject: [PATCH 34/40] file saver - WIP --- scripts/_test.js | 26 +-- scripts/libs/file-saver/index.js | 203 ++++++++++++++++++++++++ scripts/youtube_localDownloader.html | 1 + scripts/youtube_localDownloader_main.js | 5 + 4 files changed, 217 insertions(+), 18 deletions(-) create mode 100644 scripts/libs/file-saver/index.js diff --git a/scripts/_test.js b/scripts/_test.js index ee8f8529..c5d280d7 100644 --- a/scripts/_test.js +++ b/scripts/_test.js @@ -8,25 +8,15 @@ export default { en: "", vi: "", }, - whiteList: ["https://graph.facebook.com/*"], onClick: async () => { - let ACCESS_TOKEN = prompt("Nhập access token của bạn vào đây"); - if (!ACCESS_TOKEN) return; - - let id = prompt("Nhập ID của user, group, page cần lấy group id", ""); - if (!id) return; - - alert("Xem kết quả trong console"); - fetch( - `https://graph.facebook.com/v13.0/${id}/albums?fields=type,name,count,link,created_time&limit=100&access_token=${ACCESS_TOKEN}` - ) - .then((res) => res.json()) - .then((json) => { - console.log(json.data); - }) - .catch((err) => { - console.log(err); - }); + try { + let abc = getEventListeners( + document.querySelector("#wheelCanvas") + ).click[0].listener.toString(); + console.log(abc); + } catch (e) { + console.error(e); + } }, }; diff --git a/scripts/libs/file-saver/index.js b/scripts/libs/file-saver/index.js new file mode 100644 index 00000000..a2bb5962 --- /dev/null +++ b/scripts/libs/file-saver/index.js @@ -0,0 +1,203 @@ +// prettier-ignore +(function (global, factory) { + if (typeof define === "function" && define.amd) { + define([], factory); + } else if (typeof exports !== "undefined") { + factory(); + } else { + var mod = { + exports: {} + }; + factory(); + global.FileSaver = mod.exports; + } + })(this, function () { + "use strict"; + + /* + * FileSaver.js + * A saveAs() FileSaver implementation. + * + * By Eli Grey, http://eligrey.com + * + * License : https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md (MIT) + * source : http://purl.eligrey.com/github/FileSaver.js + */ + // The one and only way of getting global scope in all environments + // https://stackoverflow.com/q/3277182/1008999 + var _global = typeof window === 'object' && window.window === window ? window : typeof self === 'object' && self.self === self ? self : typeof global === 'object' && global.global === global ? global : void 0; + + function bom(blob, opts) { + if (typeof opts === 'undefined') opts = { + autoBom: false + };else if (typeof opts !== 'object') { + console.warn('Deprecated: Expected third argument to be a object'); + opts = { + autoBom: !opts + }; + } // prepend BOM for UTF-8 XML and text/* types (including HTML) + // note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF + + if (opts.autoBom && /^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) { + return new Blob([String.fromCharCode(0xFEFF), blob], { + type: blob.type + }); + } + + return blob; + } + + function download(url, name, opts) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', url); + xhr.responseType = 'blob'; + + let startTime = Date.now(); + + xhr.onload = function () { + saveAs(xhr.response, name, opts); + }; + + xhr.onprogress = function (e) { + if (typeof opts.onprogress === 'function') { + opts.onprogress({ + loaded: e.loaded, + total: e.total, + percent: e.loaded / e.total * 100, + speed: e.loaded / (Date.now() - startTime), + e + }); + } + }; + + xhr.onerror = function () { + console.error('could not download file'); + }; + + xhr.send(); + } + + function corsEnabled(url) { + var xhr = new XMLHttpRequest(); // use sync to avoid popup blocker + + xhr.open('HEAD', url, false); + + try { + xhr.send(); + } catch (e) {} + + return xhr.status >= 200 && xhr.status <= 299; + } // `a.click()` doesn't work for all browsers (#465) + + + function click(node) { + try { + node.dispatchEvent(new MouseEvent('click')); + } catch (e) { + var evt = document.createEvent('MouseEvents'); + evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null); + node.dispatchEvent(evt); + } + } // Detect WebView inside a native macOS app by ruling out all browsers + // We just need to check for 'Safari' because all other browsers (besides Firefox) include that too + // https://www.whatismybrowser.com/guides/the-latest-user-agent/macos + + + var isMacOSWebView = /Macintosh/.test(navigator.userAgent) && /AppleWebKit/.test(navigator.userAgent) && !/Safari/.test(navigator.userAgent); + var saveAs = _global.saveAs || ( // probably in some web worker + typeof window !== 'object' || window !== _global ? function saveAs() {} + /* noop */ + // Use download attribute first if possible (#193 Lumia mobile) unless this is a macOS WebView + : 'download' in HTMLAnchorElement.prototype && !isMacOSWebView ? function saveAs(blob, name, opts) { + var URL = _global.URL || _global.webkitURL; + var a = document.createElement('a'); + name = name || blob.name || 'download'; + a.download = name; + a.rel = 'noopener'; // tabnabbing + // TODO: detect chrome extensions & packaged apps + // a.target = '_blank' + + if (typeof blob === 'string') { + // Support regular links + a.href = blob; + + if (a.origin !== location.origin) { + corsEnabled(a.href) ? download(blob, name, opts) : click(a, a.target = '_blank'); + } else { + click(a); + } + } else { + // Support blobs + a.href = URL.createObjectURL(blob); + setTimeout(function () { + URL.revokeObjectURL(a.href); + }, 4E4); // 40s + + setTimeout(function () { + click(a); + }, 0); + } + } // Use msSaveOrOpenBlob as a second approach + : 'msSaveOrOpenBlob' in navigator ? function saveAs(blob, name, opts) { + name = name || blob.name || 'download'; + + if (typeof blob === 'string') { + if (corsEnabled(blob)) { + download(blob, name, opts); + } else { + var a = document.createElement('a'); + a.href = blob; + a.target = '_blank'; + setTimeout(function () { + click(a); + }); + } + } else { + navigator.msSaveOrOpenBlob(bom(blob, opts), name); + } + } // Fallback to using FileReader and a popup + : function saveAs(blob, name, opts, popup) { + // Open a popup immediately do go around popup blocker + // Mostly only available on user interaction and the fileReader is async so... + popup = popup || open('', '_blank'); + + if (popup) { + popup.document.title = popup.document.body.innerText = 'downloading...'; + } + + if (typeof blob === 'string') return download(blob, name, opts); + var force = blob.type === 'application/octet-stream'; + + var isSafari = /constructor/i.test(_global.HTMLElement) || _global.safari; + + var isChromeIOS = /CriOS\/[\d]+/.test(navigator.userAgent); + + if ((isChromeIOS || force && isSafari || isMacOSWebView) && typeof FileReader !== 'undefined') { + // Safari doesn't allow downloading of blob URLs + var reader = new FileReader(); + + reader.onloadend = function () { + var url = reader.result; + url = isChromeIOS ? url : url.replace(/^data:[^;]*;/, 'data:attachment/file;'); + if (popup) popup.location.href = url;else location = url; + popup = null; // reverse-tabnabbing #460 + }; + + reader.readAsDataURL(blob); + } else { + var URL = _global.URL || _global.webkitURL; + var url = URL.createObjectURL(blob); + if (popup) popup.location = url;else location.href = url; + popup = null; // reverse-tabnabbing #460 + + setTimeout(function () { + URL.revokeObjectURL(url); + }, 4E4); // 40s + } + }); + _global.saveAs = saveAs.saveAs = saveAs; + + if (typeof module !== 'undefined') { + module.exports = saveAs; + } + }); diff --git a/scripts/youtube_localDownloader.html b/scripts/youtube_localDownloader.html index a6def7d3..1c20924f 100644 --- a/scripts/youtube_localDownloader.html +++ b/scripts/youtube_localDownloader.html @@ -6,6 +6,7 @@ Useful Script - YouTube Local Downloader + diff --git a/scripts/youtube_localDownloader_main.js b/scripts/youtube_localDownloader_main.js index 74dd443b..cc05bd70 100644 --- a/scripts/youtube_localDownloader_main.js +++ b/scripts/youtube_localDownloader_main.js @@ -91,6 +91,11 @@ window.onload = () => { let data = xhrDownloadUint8Array(video, (progress) => { console.log(progress); }); + // saveAs(video.url, "video.mp4", { + // onprogress: (e) => { + // console.log(e); + // }, + // }); }; document.body.appendChild(button); } From f62e665c8e9a161dab4b6c664156590d5b16287b Mon Sep 17 00:00:00 2001 From: HoangTran <99.hoangtran@gmail.com> Date: Tue, 2 Apr 2024 01:00:32 +0700 Subject: [PATCH 35/40] . --- scripts/libs/html2img/index.js | 3 --- scripts/libs/utils/sort.js | 17 ----------------- scripts/{helpers => libs/utils}/xmlParser.js | 0 3 files changed, 20 deletions(-) delete mode 100644 scripts/libs/html2img/index.js delete mode 100644 scripts/libs/utils/sort.js rename scripts/{helpers => libs/utils}/xmlParser.js (100%) diff --git a/scripts/libs/html2img/index.js b/scripts/libs/html2img/index.js deleted file mode 100644 index a2ba8e70..00000000 --- a/scripts/libs/html2img/index.js +++ /dev/null @@ -1,3 +0,0 @@ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).htmlToImage={})}(this,(function(t){"use strict";function e(t,e,n,r){return new(n||(n=Promise))((function(i,o){function u(t){try{a(r.next(t))}catch(t){o(t)}}function c(t){try{a(r.throw(t))}catch(t){o(t)}}function a(t){var e;t.done?i(t.value):(e=t.value,e instanceof n?e:new n((function(t){t(e)}))).then(u,c)}a((r=r.apply(t,e||[])).next())}))}function n(t,e){var n,r,i,o,u={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return o={next:c(0),throw:c(1),return:c(2)},"function"==typeof Symbol&&(o[Symbol.iterator]=function(){return this}),o;function c(c){return function(a){return function(c){if(n)throw new TypeError("Generator is already executing.");for(;o&&(o=0,c[0]&&(u=0)),u;)try{if(n=1,r&&(i=2&c[0]?r.return:c[0]?r.throw||((i=r.return)&&i.call(r),0):r.next)&&!(i=i.call(r,c[1])).done)return i;switch(r=0,i&&(c=[2&c[0],i.value]),c[0]){case 0:case 1:i=c;break;case 4:return u.label++,{value:c[1],done:!1};case 5:u.label++,r=c[1],c=[0];continue;case 7:c=u.ops.pop(),u.trys.pop();continue;default:if(!(i=u.trys,(i=i.length>0&&i[i.length-1])||6!==c[0]&&2!==c[0])){u=0;continue}if(3===c[0]&&(!i||c[1]>i[0]&&c[1]a||t.height>a)&&(t.width>a&&t.height>a?t.width>t.height?(t.height*=a/t.width,t.width=a):(t.width*=a/t.height,t.height=a):t.width>a?(t.height*=a/t.width,t.width=a):(t.width*=a/t.height,t.height=a))}(s),s.style.width="".concat(d),s.style.height="".concat(v),r.backgroundColor&&(f.fillStyle=r.backgroundColor,f.fillRect(0,0,s.width,s.height)),f.drawImage(u,0,0,s.width,s.height),[2,s]}}))}))}t.getFontEmbedCSS=function(t,r){return void 0===r&&(r={}),e(this,void 0,void 0,(function(){return n(this,(function(e){return[2,J(t,r)]}))}))},t.toBlob=function(t,r){return void 0===r&&(r={}),e(this,void 0,void 0,(function(){return n(this,(function(e){switch(e.label){case 0:return[4,K(t,r)];case 1:return[4,s(e.sent())];case 2:return[2,e.sent()]}}))}))},t.toCanvas=K,t.toJpeg=function(t,r){return void 0===r&&(r={}),e(this,void 0,void 0,(function(){return n(this,(function(e){switch(e.label){case 0:return[4,K(t,r)];case 1:return[2,e.sent().toDataURL("image/jpeg",r.quality||1)]}}))}))},t.toPixelData=function(t,r){return void 0===r&&(r={}),e(this,void 0,void 0,(function(){var e,i,o,u;return n(this,(function(n){switch(n.label){case 0:return e=c(t,r),i=e.width,o=e.height,[4,K(t,r)];case 1:return u=n.sent(),[2,u.getContext("2d").getImageData(0,0,i,o).data]}}))}))},t.toPng=function(t,r){return void 0===r&&(r={}),e(this,void 0,void 0,(function(){return n(this,(function(e){switch(e.label){case 0:return[4,K(t,r)];case 1:return[2,e.sent().toDataURL()]}}))}))},t.toSvg=X})); -//# sourceMappingURL=html-to-image.js.map -// 1.1.11 \ No newline at end of file diff --git a/scripts/libs/utils/sort.js b/scripts/libs/utils/sort.js deleted file mode 100644 index 0d5d5d9c..00000000 --- a/scripts/libs/utils/sort.js +++ /dev/null @@ -1,17 +0,0 @@ -export const quickSort = function (arr) { - if (arr.length <= 1) { - return arr; - } - var pivotIndex = Math.floor(arr.length / 2); - var pivot = arr.splice(pivotIndex, 1)[0]; - var left = []; - var right = []; - for (var i = 0; i < arr.length; i++) { - if (arr[i] < pivot) { - left.push(arr[i]); - } else { - right.push(arr[i]); - } - } - return quickSort(left).concat([pivot], quickSort(right)); -}; diff --git a/scripts/helpers/xmlParser.js b/scripts/libs/utils/xmlParser.js similarity index 100% rename from scripts/helpers/xmlParser.js rename to scripts/libs/utils/xmlParser.js From a1e059493fbc269430169799edb7bd128f8ecf90 Mon Sep 17 00:00:00 2001 From: "hoang.tran12" Date: Tue, 2 Apr 2024 17:54:28 +0700 Subject: [PATCH 36/40] fix qrcode --- scripts/libs/qrcode/index.js | 1 + scripts/textToQRCode.html | 33 +++++++++++++++++++++++++++++ scripts/textToQRCode.js | 18 ++++++---------- scripts/textToQRCode_main.js | 19 +++++++++++++++++ scripts/webToQRCode.js | 40 ++++++++++++++++++++++++------------ 5 files changed, 86 insertions(+), 25 deletions(-) create mode 100644 scripts/libs/qrcode/index.js create mode 100644 scripts/textToQRCode.html create mode 100644 scripts/textToQRCode_main.js diff --git a/scripts/libs/qrcode/index.js b/scripts/libs/qrcode/index.js new file mode 100644 index 00000000..d5f3ca88 --- /dev/null +++ b/scripts/libs/qrcode/index.js @@ -0,0 +1 @@ +var QRCode;!function(){function a(a){this.mode=c.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var b=[],d=0,e=this.data.length;e>d;d++){var f=this.data.charCodeAt(d);f>65536?(b[0]=240|(1835008&f)>>>18,b[1]=128|(258048&f)>>>12,b[2]=128|(4032&f)>>>6,b[3]=128|63&f):f>2048?(b[0]=224|(61440&f)>>>12,b[1]=128|(4032&f)>>>6,b[2]=128|63&f):f>128?(b[0]=192|(1984&f)>>>6,b[1]=128|63&f):b[0]=f,this.parsedData=this.parsedData.concat(b)}this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function i(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c=f;f++){var h=0;switch(b){case d.L:h=l[f][0];break;case d.M:h=l[f][1];break;case d.Q:h=l[f][2];break;case d.H:h=l[f][3]}if(h>=e)break;c++}if(c>l.length)throw new Error("Too long data");return c}function s(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(this.modules[a+c][b+d]=c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?!0:!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=f.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f=g;g++)for(var h=-2;2>=h;h++)this.modules[d+g][e+h]=-2==g||2==g||-2==h||2==h||0==g&&0==h?!0:!1}},setupTypeNumber:function(a){for(var b=f.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=f.getBCHTypeInfo(c),e=0;15>e;e++){var g=!a&&1==(1&d>>e);6>e?this.modules[e][8]=g:8>e?this.modules[e+1][8]=g:this.modules[this.moduleCount-15+e][8]=g}for(var e=0;15>e;e++){var g=!a&&1==(1&d>>e);8>e?this.modules[8][this.moduleCount-e-1]=g:9>e?this.modules[8][15-e-1+1]=g:this.modules[8][15-e-1]=g}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,g=0,h=this.moduleCount-1;h>0;h-=2)for(6==h&&h--;;){for(var i=0;2>i;i++)if(null==this.modules[d][h-i]){var j=!1;g>>e));var k=f.getMask(b,d,h-i);k&&(j=!j),this.modules[d][h-i]=j,e--,-1==e&&(g++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,d){for(var e=j.getRSBlocks(a,c),g=new k,h=0;h8*l)throw new Error("code length overflow. ("+g.getLengthInBits()+">"+8*l+")");for(g.getLengthInBits()+4<=8*l&&g.put(0,4);0!=g.getLengthInBits()%8;)g.putBit(!1);for(;;){if(g.getLengthInBits()>=8*l)break;if(g.put(b.PAD0,8),g.getLengthInBits()>=8*l)break;g.put(b.PAD1,8)}return b.createBytes(g,e)},b.createBytes=function(a,b){for(var c=0,d=0,e=0,g=new Array(b.length),h=new Array(b.length),j=0;j=0?p.get(q):0}}for(var r=0,m=0;mm;m++)for(var j=0;jm;m++)for(var j=0;j=0;)b^=f.G15<=0;)b^=f.G18<>>=1;return b},getPatternPosition:function(a){return f.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case e.PATTERN000:return 0==(b+c)%2;case e.PATTERN001:return 0==b%2;case e.PATTERN010:return 0==c%3;case e.PATTERN011:return 0==(b+c)%3;case e.PATTERN100:return 0==(Math.floor(b/2)+Math.floor(c/3))%2;case e.PATTERN101:return 0==b*c%2+b*c%3;case e.PATTERN110:return 0==(b*c%2+b*c%3)%2;case e.PATTERN111:return 0==(b*c%3+(b+c)%2)%2;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new i([1],0),c=0;a>c;c++)b=b.multiply(new i([1,g.gexp(c)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case c.MODE_NUMBER:return 10;case c.MODE_ALPHA_NUM:return 9;case c.MODE_8BIT_BYTE:return 8;case c.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case c.MODE_NUMBER:return 12;case c.MODE_ALPHA_NUM:return 11;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case c.MODE_NUMBER:return 14;case c.MODE_ALPHA_NUM:return 13;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||(0!=h||0!=i)&&g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,(0==j||4==j)&&(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},g={glog:function(a){if(1>a)throw new Error("glog("+a+")");return g.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return g.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},h=0;8>h;h++)g.EXP_TABLE[h]=1<h;h++)g.EXP_TABLE[h]=g.EXP_TABLE[h-4]^g.EXP_TABLE[h-5]^g.EXP_TABLE[h-6]^g.EXP_TABLE[h-8];for(var h=0;255>h;h++)g.LOG_TABLE[g.EXP_TABLE[h]]=h;i.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),c=0;cf;f++)for(var g=c[3*f+0],h=c[3*f+1],i=c[3*f+2],k=0;g>k;k++)e.push(new j(h,i));return e},j.getRsBlockTable=function(a,b){switch(b){case d.L:return j.RS_BLOCK_TABLE[4*(a-1)+0];case d.M:return j.RS_BLOCK_TABLE[4*(a-1)+1];case d.Q:return j.RS_BLOCK_TABLE[4*(a-1)+2];case d.H:return j.RS_BLOCK_TABLE[4*(a-1)+3];default:return void 0}},k.prototype={get:function(a){var b=Math.floor(a/8);return 1==(1&this.buffer[b]>>>7-a%8)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(1&a>>>b-c-1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var l=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],o=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function g(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var b=this._htOption,c=this._el,d=a.getModuleCount();Math.floor(b.width/d),Math.floor(b.height/d),this.clear();var h=g("svg",{viewBox:"0 0 "+String(d)+" "+String(d),width:"100%",height:"100%",fill:b.colorLight});h.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),c.appendChild(h),h.appendChild(g("rect",{fill:b.colorDark,width:"1",height:"1",id:"template"}));for(var i=0;d>i;i++)for(var j=0;d>j;j++)if(a.isDark(i,j)){var k=g("use",{x:String(i),y:String(j)});k.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),h.appendChild(k)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),p="svg"===document.documentElement.tagName.toLowerCase(),q=p?o:m()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function d(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&_fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",void 0}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var b=1/window.devicePixelRatio,c=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,d,e,f,g,h,i,j){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*b;else"undefined"==typeof j&&(arguments[1]*=b,arguments[2]*=b,arguments[3]*=b,arguments[4]*=b);c.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=n(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&d.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){for(var b=this._htOption,c=this._el,d=a.getModuleCount(),e=Math.floor(b.width/d),f=Math.floor(b.height/d),g=[''],h=0;d>h;h++){g.push("");for(var i=0;d>i;i++)g.push('');g.push("")}g.push("
    "),c.innerHTML=g.join("");var j=c.childNodes[0],k=(b.width-j.offsetWidth)/2,l=(b.height-j.offsetHeight)/2;k>0&&l>0&&(j.style.margin=l+"px "+k+"px")},a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCode=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:d.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._android=n(),this._el=a,this._oQRCode=null,this._oDrawing=new q(this._el,this._htOption),this._htOption.text&&this.makeCode(this._htOption.text)},QRCode.prototype.makeCode=function(a){this._oQRCode=new b(r(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCode.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCode.prototype.clear=function(){this._oDrawing.clear()},QRCode.CorrectLevel=d}(); diff --git a/scripts/textToQRCode.html b/scripts/textToQRCode.html new file mode 100644 index 00000000..77f04d66 --- /dev/null +++ b/scripts/textToQRCode.html @@ -0,0 +1,33 @@ + + + + + + + Text to QRCode - Useful script + + + + + + + +
    +
    + + + + + diff --git a/scripts/textToQRCode.js b/scripts/textToQRCode.js index 5ad986ec..973fd460 100644 --- a/scripts/textToQRCode.js +++ b/scripts/textToQRCode.js @@ -1,3 +1,5 @@ +import { popupCenter } from "./helpers/utils.js"; + export default { icon: ``, name: { @@ -13,17 +15,9 @@ export default { let text = prompt("Enter text / Nhập chữ:", ""); if (text === null) return; - let url = - "http://chart.apis.google.com/chart?cht=qr&chs=300x300&chl=" + text; - let w = window.open( - url, - "w", - "location=no,status=yes,menubar=no,scrollbars=no,resizable=yes,width=500,height=500,modal=yes,dependent=yes" - ); - if (w) { - setTimeout("w.focus()", 1000); - } else { - location = url; - } + popupCenter({ + url: chrome.runtime.getURL("/scripts/textToQRCode.html?text=" + text), + title: "Text To QRCode", + }); }, }; diff --git a/scripts/textToQRCode_main.js b/scripts/textToQRCode_main.js new file mode 100644 index 00000000..0c8b70c1 --- /dev/null +++ b/scripts/textToQRCode_main.js @@ -0,0 +1,19 @@ +window.onload = () => { + let input = document.getElementById("text"); + let qrcode = new QRCode("qrcode"); + + let textFromUrl = new URLSearchParams(window.location.search).get("text"); + if (textFromUrl) { + qrcode.makeCode(textFromUrl); + input.value = textFromUrl; + } + + input.addEventListener("keyup", (event) => { + qrcode.makeCode(event.target.value); + }); + input.addEventListener("blur", (event) => { + qrcode.makeCode(event.target.value); + }); + + window.resizeTo(400, 400); +}; diff --git a/scripts/webToQRCode.js b/scripts/webToQRCode.js index 59327560..d6a9ddbe 100644 --- a/scripts/webToQRCode.js +++ b/scripts/webToQRCode.js @@ -1,3 +1,5 @@ +import { getCurrentTab, popupCenter } from "./helpers/utils.js"; + export default { icon: ``, name: { @@ -9,19 +11,31 @@ export default { vi: "Chuyển URL của trang web sang QR Code", }, - onClick: function () { - var url = - "http://chart.apis.google.com/chart?cht=qr&chs=300x300&chl=" + - encodeURIComponent(location.href); - w = open( - url, - "w", - "location=no,status=yes,menubar=no,scrollbars=no,resizable=yes,width=500,height=500,modal=yes,dependent=yes" - ); - if (w) { - setTimeout("w.focus()", 1000); - } else { - location = url; + onClickExtension: async function () { + let tab = await getCurrentTab(); + let url = tab.url; + if (!url) { + alert("Không tìm thấy url web hiện tại"); + return; } + + popupCenter({ + url: chrome.runtime.getURL("/scripts/textToQRCode.html?text=" + url), + title: "Text To QRCode", + }); + + // var url = + // "http://chart.apis.google.com/chart?cht=qr&chs=300x300&chl=" + + // encodeURIComponent(location.href); + // w = open( + // url, + // "w", + // "location=no,status=yes,menubar=no,scrollbars=no,resizable=yes,width=500,height=500,modal=yes,dependent=yes" + // ); + // if (w) { + // setTimeout("w.focus()", 1000); + // } else { + // location = url; + // } }, }; From b473ec52e67c0acfbcec3daf716d87fc61901e52 Mon Sep 17 00:00:00 2001 From: "hoang.tran12" Date: Tue, 2 Apr 2024 17:56:32 +0700 Subject: [PATCH 37/40] remove unused code --- scripts/webToQRCode.js | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/scripts/webToQRCode.js b/scripts/webToQRCode.js index d6a9ddbe..70276963 100644 --- a/scripts/webToQRCode.js +++ b/scripts/webToQRCode.js @@ -23,19 +23,5 @@ export default { url: chrome.runtime.getURL("/scripts/textToQRCode.html?text=" + url), title: "Text To QRCode", }); - - // var url = - // "http://chart.apis.google.com/chart?cht=qr&chs=300x300&chl=" + - // encodeURIComponent(location.href); - // w = open( - // url, - // "w", - // "location=no,status=yes,menubar=no,scrollbars=no,resizable=yes,width=500,height=500,modal=yes,dependent=yes" - // ); - // if (w) { - // setTimeout("w.focus()", 1000); - // } else { - // location = url; - // } }, }; From a15860c635a88c74fb876589c488fcd3bf58831e Mon Sep 17 00:00:00 2001 From: HoangTran <99.hoangtran@gmail.com> Date: Wed, 3 Apr 2024 01:36:00 +0700 Subject: [PATCH 38/40] optimize --- manifest.json | 5 +- popup/helpers/category.js | 21 --- popup/tabs.js | 144 ++++++++++-------- scripts/bing_imageCreator.js | 112 -------------- scripts/changeAudioOutput.css | 84 ----------- scripts/changeAudioOutput.js | 224 ++++++++++++++-------------- scripts/dreamai.js | 15 -- scripts/fb_downloadWatchingVideo.js | 2 +- scripts/huggingface.js | 15 -- scripts/index.js | 33 +--- scripts/insta_getAllUserMedia.js | 8 +- scripts/insta_getToken.js | 40 ----- scripts/insta_storySaver.js | 119 --------------- scripts/jsonformatter.js | 36 ----- scripts/pixaiart.js | 15 -- scripts/playgroundai.js | 15 -- scripts/skybox_blockadelabs.js | 15 -- scripts/stable_diffusion_baseten.js | 15 -- scripts/stable_diffusion_demo.js | 33 ---- scripts/textToQRCode.html | 33 ---- scripts/textToQRCode.js | 23 --- scripts/textToQRCode_main.js | 19 --- scripts/transfer_sh.js | 219 --------------------------- scripts/webToQRCode.js | 27 ---- 24 files changed, 209 insertions(+), 1063 deletions(-) delete mode 100644 scripts/bing_imageCreator.js delete mode 100644 scripts/changeAudioOutput.css delete mode 100644 scripts/dreamai.js delete mode 100644 scripts/huggingface.js delete mode 100644 scripts/insta_getToken.js delete mode 100644 scripts/insta_storySaver.js delete mode 100644 scripts/jsonformatter.js delete mode 100644 scripts/pixaiart.js delete mode 100644 scripts/playgroundai.js delete mode 100644 scripts/skybox_blockadelabs.js delete mode 100644 scripts/stable_diffusion_baseten.js delete mode 100644 scripts/stable_diffusion_demo.js delete mode 100644 scripts/textToQRCode.html delete mode 100644 scripts/textToQRCode.js delete mode 100644 scripts/textToQRCode_main.js delete mode 100644 scripts/transfer_sh.js delete mode 100644 scripts/webToQRCode.js diff --git a/manifest.json b/manifest.json index 1c519d73..be6218f4 100644 --- a/manifest.json +++ b/manifest.json @@ -29,7 +29,10 @@ "content_scripts": [ { "matches": [""], - "js": ["scripts/content-scripts/document_start.js"], + "js": [ + "scripts/content-scripts/document_start.js", + "scripts/content-scripts/scripts/ufs_global_webpage_context.js" + ], "run_at": "document_start" }, { diff --git a/popup/helpers/category.js b/popup/helpers/category.js index 590a3ffb..64bb8265 100644 --- a/popup/helpers/category.js +++ b/popup/helpers/category.js @@ -32,13 +32,6 @@ export const CATEGORY = { vi: ` Tự động chạy`, }, }, - ai: { - id: "ai", - name: { - en: ` AI`, - vi: ` AI`, - }, - }, search: { id: "search", name: { @@ -88,20 +81,6 @@ export const CATEGORY = { vi: ` Tiktok`, }, }, - shopping: { - id: "shopping", - name: { - en: ` Shopping`, - vi: ` Mua sắm`, - }, - }, - github: { - id: "github", - name: { - en: ` Github`, - vi: ` Github`, - }, - }, automation: { id: "automation", name: { diff --git a/popup/tabs.js b/popup/tabs.js index 7406fcd1..15585a51 100644 --- a/popup/tabs.js +++ b/popup/tabs.js @@ -25,20 +25,6 @@ const specialTabs = [ ]; const tabs = [ - { - ...CATEGORY.ai, - scripts: [ - s.huggingface, - createTitle("--- AI Art ---", "--- AI Art - Tranh/Ảnh ---"), - s.bing_imageCreator, - s.pixaiart, - s.playgroundai, - s.dreamai, - s.skybox_blockadelabs, - s.stable_diffusion_demo, - s.stable_diffusion_baseten, - ], - }, { ...CATEGORY.search, scripts: [ @@ -120,7 +106,7 @@ const tabs = [ s.fb_downloadCommentVideo, s.fb_videoDownloader, s.fb_getAvatarFromUid, - // s.fb_storyInfo, + s.fb_storyInfo, createTitle("--- Bulk Download ---", "--- Tải hàng loạt ---"), s.fb_bulkDownload, s.fb_downloadAlbumMedia, @@ -169,11 +155,8 @@ const tabs = [ { ...CATEGORY.instagram, scripts: [ - // s.insta_getToken, s.insta_getUserInfo, - createTitle("--- Download ---", "--- Tải xuống ---"), s.insta_injectDownloadBtn, - s.insta_storySaver, createTitle("--- Bulk Download ---", "--- Tải hàng loạt ---"), s.insta_getAllUserMedia, s.insta_getAllImagesInNewFeed, @@ -208,19 +191,6 @@ const tabs = [ s.doutube_getAllVideoInUserProfile, ], }, - { - ...CATEGORY.shopping, - scripts: [ - s.shopee_topVariation, - s.shopee_totalSpendMoney, - s.shopee_totalSpendMoney_excel, - s.tiki_totalSpendMoney, - ], - }, - { - ...CATEGORY.github, - scripts: [s.github_goToAnyCommit, s.githubdev, s.github1s], - }, { ...CATEGORY.automation, scripts: [ @@ -228,26 +198,30 @@ const tabs = [ s.unshorten, s.textToSpeech, s.changeAudioOutput, + s.send_shareFiles, createTitle("--- Image ---", "--- Ảnh ---"), - s.textToQRCode, - s.webToQRCode, s.screenshotFullPage, s.vuiz_createLogo, createTitle("--- Automation ---", "--- Tự động hoá ---"), s.passwordGenerator, s.getAllEmailsInWeb, - s.jsonformatter, s.performanceAnalyzer, s.scrollToVeryEnd, s.dino_hack, + createTitle("--- Github ---", "--- Github ---"), + s.github_goToAnyCommit, + s.githubdev, + s.github1s, + createTitle("--- Shopping ---", "--- Mua sắm ---"), + s.shopee_topVariation, + s.shopee_totalSpendMoney, + s.shopee_totalSpendMoney_excel, + s.tiki_totalSpendMoney, createTitle("--- PDF ---", "--- PDF ---"), s.webToPDF, s.fastDoc, s.smartPDF, s.pdfstuffs, - createTitle("--- Share ---", "--- Chia sẻ ---"), - s.send_shareFiles, - s.transfer_sh, ], }, { @@ -268,7 +242,6 @@ const tabs = [ s.simpleAllowCopy, s.reEnableContextMenu, s.showHiddenFields, - // s.passwordFieldToggle, s.viewCookies, s.removeCookies, s.viewBrowserInfo, @@ -312,6 +285,22 @@ const recommendTab = { scripts: [ { name: { en: "--- Same author ---", vi: "--- Cùng tác giả ---" } }, { + id: "recommend_LOL2D", + icon: "https://hoangtran0410.github.io/LOL2D/favicon/apple-touch-icon.png", + name: { + en: "LOL2D - League of Legends 2D", + vi: "LOL2D - Liên minh huyền thoại 2D", + }, + description: { + en: "Play League of Legends right on your browser", + vi: "Chơi Liên minh huyền thoại ngay trên trình duyệt", + img: "https://raw.githubusercontent.com/HoangTran0410/LOL2D/main/assets/images/screenshots/Screenshot_4.jpg", + }, + onClickExtension: () => + window.open("https://github.com/HoangTran0410/LOL2D"), + }, + { + id: "recommend_RevealDeletedFBMessage", icon: "https://github.com/HoangTran0410/RevealDeletedFBMessages/raw/master/icons/icon48.png", name: { en: "Reveal Deleted FB Message", @@ -325,6 +314,7 @@ const recommendTab = { window.open("https://github.com/HoangTran0410/RevealDeletedFBMessages"), }, { + id: "recommend_FBMediaDownloader", icon: "https://www.facebook.com/favicon.ico", name: { en: "FB Media Downloader", vi: "FB Media Downloader" }, description: { @@ -336,6 +326,7 @@ const recommendTab = { }, { name: { en: "--- Web ---", vi: "--- Web hay ---" } }, { + id: "recommend_YouCom", icon: "https://you.com/favicon/apple-touch-icon-72x72.png", name: { en: "You.com", vi: "You.com" }, description: { @@ -345,6 +336,7 @@ const recommendTab = { onClickExtension: () => window.open("https://you.com/"), }, { + id: "recommend_ItTools", icon: "https://it-tools.tech/favicon-32x32.png", name: { en: "IT Tools", vi: "IT Tools" }, description: { @@ -355,18 +347,7 @@ const recommendTab = { }, { name: { en: "--- Extensions ---", vi: "--- Extensions hay ---" } }, { - icon: "https://lh3.googleusercontent.com/2GdtpZt9NWFkfrfLZnWL2gM2UdCOsgpQhhdxSx4wPw5Iz10NcT433g3iHyAAZ8J-ZCyz3gwLKR1kJQC0PidRVKKJ1Ws=w128-h128-e365-rj-sc0x00ffffff", - name: { en: "J2Team Security", vi: "J2Team Security" }, - description: { - en: "Use fb better with more security and tools", - vi: "Dùng fb sướng hơn bao giờ hết", - }, - onClickExtension: () => - window.open( - "https://chrome.google.com/webstore/detail/j2team-security/hmlcjjclebjnfohgmgikjfnbmfkigocc" - ), - }, - { + id: "recommend_CRXViewer", icon: "https://lh3.googleusercontent.com/fD5QA80tZj1up43xmnxnxiqKNEq7515-HNtLfjoZlz_I626zxXmjlhKaQPUme_evpCEnN5-U7VnG3VfOcnTPzv_i=w128-h128-e365-rj-sc0x00ffffff", name: { en: "CRX Viewer", vi: "CRX Viewer" }, description: { @@ -379,18 +360,33 @@ const recommendTab = { ), }, { - icon: "https://lh3.googleusercontent.com/nnMASpwJY4U5ukhKl4PfIdaOpuKXNrVvfIc9n8-NJOJIY7m3RLgsazN6ATmDkXyaMll8zADOXuBR574MwC7T71kJcQ=w128-h128-e365-rj-sc0x00ffffff", - name: { en: "Adblock Plus", vi: "Adblock Plus" }, + id: "recommend_uBlockOrigin", + icon: "https://lh3.googleusercontent.com/rrgyVBVte7CfjjeTU-rCHDKba7vtq-yn3o8-10p5b6QOj_2VCDAO3VdggV5fUnugbG2eDGPPjoJ9rsiU_tUZBExgLGc=s60", + name: { en: "uBlock Origin", vi: "uBlock Origin" }, description: { en: "Block advertisements for all website", vi: "Chặn quảng cáo cho mọi website", }, onClickExtension: () => window.open( - "https://chrome.google.com/webstore/detail/adblock-plus-free-ad-bloc/cfhdojbkjhnklbpkdaibdccddilifddb" + "https://chromewebstore.google.com/detail/ublock-origin/cjpalhdlnbpafiamejdnhcphjbkeiagm" + ), + }, + { + id: "recommend_DarkReader", + icon: "https://lh3.googleusercontent.com/T66wTLk-gpBBGsMm0SDJJ3VaI8YM0Utr8NaGCSANmXOfb84K-9GmyXORLKoslfxtasKtQ4spDCdq_zlp_t3QQ6SI0A=w128-h128-e365-rj-sc0x00ffffff", + name: { en: "Dark reader", vi: "Dark reader" }, + description: { + en: "Darkmode for every website", + vi: "Chế độ tối cho mọi trang web", + }, + onClickExtension: () => + window.open( + "https://chrome.google.com/webstore/detail/dark-reader/eimadpbcbfnmbkopoojfekhnkhdbieeh" ), }, { + id: "recommend_GoogleTranslate", icon: "https://lh3.googleusercontent.com/3ZU5aHnsnQUl9ySPrGBqe5LXz_z9DK05DEfk10tpKHv5cvG19elbOr0BdW_k8GjLMFDexT2QHlDwAmW62iLVdek--Q=w128-h128-e365-rj-sc0x00ffffff", name: { en: "Google translate", vi: "Google dịch" }, description: { @@ -403,6 +399,7 @@ const recommendTab = { ), }, { + id: "recommend_NSFWFilter", icon: "https://lh3.googleusercontent.com/M_2Q8eJAj1ejsRg30LuJs_Q94Jk7d-6ZbE5cyddULweH5LrfsVJtjK8zbpSjwA3G9oHwZeyHyrYrr971kqLwtNNP=w128-h128-e365-rj-sc0x00ffffff", name: { en: "NSFW Filter: Hide NSFW content", @@ -418,33 +415,52 @@ const recommendTab = { ), }, { - icon: "https://lh3.googleusercontent.com/tGvFFAf_mkjk-mfiRipdYU_WTMCZSReAy4opGxvWJppyHzHTKy6f1NO1tSpV998-ZcKJjPOWpWbtEFLEMr0Y_SyBKA=w128-h128-e365-rj-sc0x00ffffff", + id: "recommend_Violentmonkey", + icon: "https://violentmonkey.github.io/favicon-32x32.png?v=e0d9ed50fb982761b0f7cdea8b093ae9", + name: { + en: "Violentmonkey", + vi: "Violentmonkey", + }, + description: { + en: "An open source userscript manager.", + vi: "Trình quản lý userscript tốt.", + }, + onClickExtension: () => window.open("https://violentmonkey.github.io/"), + }, + { + id: "recommend_Extensity", + icon: "https://lh3.googleusercontent.com/mgOg2hnGuthlYj-MEUXedWn_s9QjTXBwusffIAhbIuHM8L3K2c5cq1xf7bCzbRE5f9E6RXaGLPNEuJEt4hP6sLDL=s60", name: { - en: "DYL Download Facebook Video", - vi: "DYL Download Facebook Video", + en: "Extensity", + vi: "Extensity", }, description: { - en: "Video, Story, download with one click", - vi: "Tải video, story facebook với 1 nút nhấn", + en: "Extension manager - Quickly enable/disable browser extensions", + vi: "Trình quản lý extension - Nhanh chóng tắt/mở extension của trình duyệt", }, onClickExtension: () => window.open( - "https://chrome.google.com/webstore/detail/dyl-download-facebook-vid/honmapcmnfgjmahijdniaaollhhfpcnj?hl=vi" + "https://chromewebstore.google.com/detail/extensity/jjmflmamggggndanpgfnpelongoepncg" ), }, { - icon: "https://lh3.googleusercontent.com/T66wTLk-gpBBGsMm0SDJJ3VaI8YM0Utr8NaGCSANmXOfb84K-9GmyXORLKoslfxtasKtQ4spDCdq_zlp_t3QQ6SI0A=w128-h128-e365-rj-sc0x00ffffff", - name: { en: "Dark reader", vi: "Dark reader" }, + id: "recommend_BookmarkSidebar", + icon: "https://lh3.googleusercontent.com/4kT7DxtoPSmSLzTit1w2Vbx7b1L2zkASTrqGzEpBW-qs2EwmLYzBTyv0cvlGZo-rD-s732OIrUXX-C33RHPSFvOj=s0", + name: { + en: "Bookmark Sidebar", + vi: "Bookmark Sidebar", + }, description: { - en: "Darkmode for every website", - vi: "Chế độ tối cho mọi trang web", + en: "Very good Bookmark manager, find your bookmarks faster.", + vi: "Trình quản lý extension ngon, tìm kiếm bookmark nhanh hơn bao giờ hết.", }, onClickExtension: () => window.open( - "https://chrome.google.com/webstore/detail/dark-reader/eimadpbcbfnmbkopoojfekhnkhdbieeh" + "https://chromewebstore.google.com/detail/thanh-d%E1%BA%A5u-trang/jdbnofccmhefkmjbkkdkfiicjkgofkdh" ), }, { + id: "recommend_Beecost", icon: "https://lh3.googleusercontent.com/QeCUs-fM4mwAmBVRS0VU8NrjJnDnbSsXoqUrCbd8ZbHou03FBPEQOYHAcdcL_rn7NMrUpWMcXoG2m_CrKtAhc-wLgLU=w128-h128-e365-rj-sc0x00ffffff", name: { en: "Beecost", vi: "Beecost" }, description: { diff --git a/scripts/bing_imageCreator.js b/scripts/bing_imageCreator.js deleted file mode 100644 index e2e563b9..00000000 --- a/scripts/bing_imageCreator.js +++ /dev/null @@ -1,112 +0,0 @@ -import { showLoading, openPopupWithHtml, sleep } from "./helpers/utils.js"; - -export default { - icon: "https://www.bing.com/favicon.ico", - name: { - en: "Bing image creator", - vi: "Bing image creator", - }, - description: { - en: "Microsoft bing AI image generator", - vi: "Trình tạo ảnh bằng AI của microsoft bing", - }, - infoLink: "https://www.bing.com/create", - - onClickExtension: () => { - let prompt_text = window.prompt("Enter prompt to create image", ""); - - if (prompt_text) { - getBingImages(prompt_text).then((urls) => { - if (urls) { - let html = - `

    ${prompt_text}

    ` + - urls - .map( - (url) => - `${url}` + - `` - ) - .join(""); - openPopupWithHtml(html, 600, window.screen.availHeight); - } - }); - } - - async function getBingImages(prompt_text) { - const BING_URL = "https://www.bing.com"; - let { setLoadingText, closeLoading } = showLoading("Prepairing..."); - - try { - let url_encoded_prompt = encodeURIComponent(prompt_text); - let url = `${BING_URL}/images/create?q=${url_encoded_prompt}&rt=3&FORM=GENCRE`; - - setLoadingText("Redirecting to bing..."); - let resp = await fetch(url, { - method: "POST", - // redirect: "manual", - headers: { - accept: - "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", - "accept-language": "en-US,en;q=0.9", - "cache-control": "max-age=0", - "content-type": "application/x-www-form-urlencoded", - referrer: "https://www.bing.com/images/create/", - origin: "https://www.bing.com", - "user-agent": - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63", - }, - }); - - setLoadingText("Getting request id..."); - let request_id = new URLSearchParams(resp.url).get("id"); - let polling_url = `${BING_URL}/images/create/async/results/${request_id}?q=${url_encoded_prompt}`; - - setLoadingText("Waiting for results..."); - let text, - image_links, - waitCount = 0; - - while (true) { - resp = await fetch(polling_url); - if (resp.status !== 200) { - throw Error("Could not get results"); - } - text = await resp.text(); - if (!text) { - waitCount++; - setLoadingText(`Waiting for results... (${waitCount}s)`); - await sleep(1000); - } else { - console.log(text); - - if (text.startsWith('{"errorMessage"')) { - throw Error( - "Get Error: " + text + "\n\n Please enter another prompt." - ); - } - - setLoadingText("Extracting images from data..."); - const regex = /src="([^"]+)"/g; - image_links = []; - let match; - while ((match = regex.exec(text)) !== null) { - image_links.push(match[1]); - } - - if (image_links.length > 0) { - break; - } - } - } - - setLoadingText("Opening images..."); - let image_links_full = image_links.map((link) => link.split("?")[0]); // remove url search params - return image_links_full; - } catch (e) { - alert("ERROR: " + e); - } finally { - closeLoading(); - } - } - }, -}; diff --git a/scripts/changeAudioOutput.css b/scripts/changeAudioOutput.css deleted file mode 100644 index a1055ac0..00000000 --- a/scripts/changeAudioOutput.css +++ /dev/null @@ -1,84 +0,0 @@ -.ufs-change-audio-output { - width: 320px; - height: 120px; - position: fixed; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - - background-color: #fff; - border: 1px solid rgba(0, 0, 0, .08); - border-radius: 5px; - z-Index: 999999; -} - -.ufs-change-audio-output .navbar { - width: 100%; - height: 28px; - background-color: rgba(0, 0, 0, .05); -} - -.ufs-change-audio-output .close-btn { - padding: 6px 8px; - background-color: transparent; - color: black; - position: absolute; - right: 0; - top: 0; - border: none; - cursor: pointer; -} - -.ufs-change-audio-output .close-btn:hover { - background-color: #e74c3c; - color: white; -} - -.ufs-change-audio-output .title { - font-size: 12px; - position: absolute; - left: 6px; - top: 8px; -} - -.ufs-change-audio-output .title-link { - font-weight: 500; - text-decoration: none; -} - -.ufs-change-audio-output .popup-content { - height: 92px; - display: relative; - width: 100%; -} - -.ufs-change-audio-output .bottom-area { - position: absolute; - left: 10px; - right: 10px; - bottom: 10px; -} - -.ufs-change-audio-output .submit-btn { - background-color: #3498db; - color: #FFF; - width: 100%; - padding: 6px; - border: none; - border-radius: 3px; - cursor: pointer; -} - -.ufs-change-audio-output .submit-btn:disabled { - background-color: #999; -} - -.ufs-change-audio-output .submit-btn:hover:not(:disabled) { - background-color: #2a7bb1; -} - -.ufs-change-audio-output .device-selector { - margin: 10px; - padding: 5px; - width: calc(100% - 20px); -} \ No newline at end of file diff --git a/scripts/changeAudioOutput.js b/scripts/changeAudioOutput.js index 840107d4..951c41f9 100644 --- a/scripts/changeAudioOutput.js +++ b/scripts/changeAudioOutput.js @@ -5,119 +5,127 @@ export default { vi: "Thay đổi đầu ra âm thanh", }, description: { - en: "Pick a default audio output device, customizable for each browser tab.", - vi: "Thay đổi đầu ra âm thanh của trang web đang mở.\nMỗi tab có thể chọn đầu ra khác nhau (tai nghe/loa).", + en: "Pick a default audio output device, customizable for each browser tab.

    eg. listen to youtube by headphone in tab 1, play music by external speaker in tab 2.", + vi: "Thay đổi đầu ra âm thanh của trang web đang mở.
    Mỗi tab có thể chọn đầu ra khác nhau (tai nghe/loa).

    Ví dụ: nghe youtube bằng tai nghe ở tab 1,
    nghe nhạc bằng loa ở tab 2.", }, - infoLink: "https://www.facebook.com/groups/j2team.community/posts/1362716140727169/", + infoLink: + "https://www.facebook.com/groups/j2team.community/posts/1362716140727169/", // Source: https://gist.github.com/monokaijs/44ef4bd0770f83272b83c038a2769c90 - onClick: async () => { - let key = "ufs-audio-output-switcher"; - let exist = document.querySelector("#" + key); - if (exist) { - exist.style.display = ""; - return; - } - - if ( - !confirm( - "Vui lòng cho phép truy cập audio/video để chức năng có thể chạy." - ) - ) - return; - - function disableBtn(btn, disable = true) { - if (disable) { - btn.classList.add("disabled"); - btn.disabled = true; - } else { - btn.classList.remove("disabled"); - btn.disabled = false; - } - } - - let popupParent = document.createElement("div"); - popupParent.id = key; - popupParent.className = "ufs-change-audio-output"; - - /* navigation bar */ - let navbar = document.createElement("div"); - navbar.className = "navbar"; - - let closeButton = document.createElement("button"); - closeButton.className = "close-btn"; - closeButton.innerText = "Close"; - closeButton.onclick = function (e) { - popupParent.style.display = "none"; - }; - navbar.appendChild(closeButton); - - /* PLEASE DO NOT REMOVE CREDIT LINE */ - let popupTitle = document.createElement("div"); - popupTitle.className = "title"; - popupTitle.innerHTML = - "SoundSwitcher - by NorthStudio"; - navbar.appendChild(popupTitle); - - let titleLink = popupTitle.querySelector("a"); - titleLink.className = "title-link"; - - let popupContent = document.createElement("div"); - popupContent.className = "popup-content"; - - let bottomArea = document.createElement("div"); - bottomArea.className = "bottom-area"; - - let submitButton = document.createElement("button"); - submitButton.className = "submit-btn"; - submitButton.innerText = "Waiting for permission..."; - disableBtn(submitButton); - - submitButton.onclick = async function (e) { - if (submitButton.disabled) return; - let audio_video = Array.from(document.querySelectorAll("audio,video")); - - if (!audio_video.length) - return alert("Không tìm thấy âm thanh/video nào trong trang web"); - - submitButton.innerText = "Setting..."; - disableBtn(submitButton); - for (let el of audio_video) { - await el.setSinkId(deviceSelector.value); + onClick: () => { + (async function () { + try { + await navigator.mediaDevices.getUserMedia({ audio: true }); + } catch (e) { + alert("Failed to initialize media devices."); } + let popupParent = document.createElement("div"); + /* positioning for the dialog */ + popupParent.style.width = "320px"; + popupParent.style.height = "120px"; + popupParent.style.position = "fixed"; + popupParent.style.left = "50%"; + popupParent.style.top = "50%"; + popupParent.style.marginLeft = "-160px"; + popupParent.style.marginTop = "-60px"; + /* stylizing the dialog */ + popupParent.style.backgroundColor = "#fff"; + popupParent.style.border = "1px solid rgba(0, 0, 0, .08)"; + popupParent.style.zIndex = "999999"; + + /* navigation bar */ + let navbar = document.createElement("div"); + navbar.style.width = "100%"; + navbar.style.height = "28px"; + navbar.style.backgroundColor = "rgba(0, 0, 0, .05)"; + + let closeButton = document.createElement("button"); + closeButton.innerText = "Close"; + closeButton.style.padding = "6px 8px"; + closeButton.style.backgroundColor = "transparent"; + closeButton.style.color = "black"; + closeButton.style.position = "absolute"; + closeButton.style.right = "0"; + closeButton.style.top = "0"; + closeButton.style.border = "none"; + + navbar.appendChild(closeButton); + + closeButton.onclick = function (e) { + popupParent.parentNode.removeChild(popupParent); + }; + closeButton.onmouseover = function (e) { + e.target.style.backgroundColor = "#e74c3c"; + e.target.style.color = "white"; + }; + closeButton.onmouseleave = function (e) { + e.target.style.backgroundColor = "transparent"; + e.target.style.color = "black"; + }; + + /* PLEASE DO NOT REMOVE CREDIT LINE */ + let popupTitle = document.createElement("div"); + popupTitle.innerHTML = + "SoundSwitcher - by NorthStudio"; + popupTitle.style.fontSize = "12px"; + popupTitle.style.position = "absolute"; + popupTitle.style.left = "6px"; + popupTitle.style.top = "8px"; + navbar.appendChild(popupTitle); + + let titleLink = popupTitle.querySelector("a"); + titleLink.style.fontWeight = "500"; + titleLink.style.textDecoration = "none"; + + let popupContent = document.createElement("div"); + popupContent.style.height = "92px"; + popupContent.style.display = "relative"; + popupContent.style.width = "100%"; + + let bottomArea = document.createElement("div"); + bottomArea.style.position = "absolute"; + bottomArea.style.left = "10px"; + bottomArea.style.right = "10px"; + bottomArea.style.bottom = "10px"; + + let submitButton = document.createElement("button"); + submitButton.style.backgroundColor = "#3498db"; + submitButton.style.color = "#FFF"; + submitButton.style.width = "100%"; + submitButton.style.padding = "6px"; + submitButton.style.border = "none"; + submitButton.style.borderRadius = "3px"; submitButton.innerText = "Set Device"; - disableBtn(submitButton, false); - }; - - bottomArea.appendChild(submitButton); - - let deviceSelector = document.createElement("select"); - deviceSelector.className = "device-selector"; - - popupContent.appendChild(bottomArea); - popupParent.appendChild(navbar); - popupParent.appendChild(popupContent); - document.body.appendChild(popupParent); - UsefulScriptGlobalPageContext.DOM.injectCssFile( - await UsefulScriptGlobalPageContext.Extension.getURL( - "scripts/changeAudioOutput.css" - ) - ); - - // ====================== Main ====================== - await navigator.mediaDevices.getUserMedia({ audio: true, video: true }); - submitButton.innerText = "Set Device"; - disableBtn(submitButton, false); - - navigator.mediaDevices.enumerateDevices().then((devices) => { - devices = devices.filter((x) => x.kind === "audiooutput"); - devices.forEach((device) => { - let choice = document.createElement("option"); - choice.innerText = device.label; - choice.value = device.deviceId; - deviceSelector.appendChild(choice); + + submitButton.onclick = function (e) { + document.querySelectorAll("audio,video").forEach((el) => { + el.setSinkId(deviceSelector.value); + }); + }; + + bottomArea.appendChild(submitButton); + + let deviceSelector = document.createElement("select"); + navigator.mediaDevices.enumerateDevices().then((devices) => { + devices = devices.filter((x) => x.kind === "audiooutput"); + devices.forEach((device) => { + let choice = document.createElement("option"); + choice.innerText = device.label; + choice.value = device.deviceId; + deviceSelector.appendChild(choice); + }); + popupContent.appendChild(deviceSelector); }); - popupContent.appendChild(deviceSelector); - }); + + deviceSelector.style.margin = "10px"; + deviceSelector.style.padding = "5px"; + deviceSelector.style.width = "calc(100% - 20px)"; + + popupContent.appendChild(bottomArea); + + popupParent.appendChild(navbar); + popupParent.appendChild(popupContent); + document.body.appendChild(popupParent); + })(); }, }; diff --git a/scripts/dreamai.js b/scripts/dreamai.js deleted file mode 100644 index 32e4fc75..00000000 --- a/scripts/dreamai.js +++ /dev/null @@ -1,15 +0,0 @@ -export default { - icon: "https://dream.ai/favicon.ico", - name: { - en: "Dream by WOMBO", - vi: "Dream by WOMBO", - }, - description: { - en: "High Quality Artwork In Seconds", - vi: "Trình tạo hình ảnh chất lượng cao trong vài giây", - }, - - onClickExtension: () => { - window.open("https://dream.ai"); - }, -}; diff --git a/scripts/fb_downloadWatchingVideo.js b/scripts/fb_downloadWatchingVideo.js index ab23dbdc..36320e8c 100644 --- a/scripts/fb_downloadWatchingVideo.js +++ b/scripts/fb_downloadWatchingVideo.js @@ -21,7 +21,7 @@ export default { ); try { let listVideoId = await shared.getListVideoIdInWebsite(); - let watchingVideoId = listVideoId[0]; + let watchingVideoId = listVideoId?.[0]; if (!watchingVideoId) throw Error("Không tìm thấy video nào"); setLoadingText("Đang lấy token dtsg..."); diff --git a/scripts/huggingface.js b/scripts/huggingface.js deleted file mode 100644 index b45dd9a9..00000000 --- a/scripts/huggingface.js +++ /dev/null @@ -1,15 +0,0 @@ -export default { - icon: "https://huggingface.co/favicon.ico", - name: { - en: "Hugging Face", - vi: "Hugging Face", - }, - description: { - en: "The AI community building the future.", - vi: "Cộng đồng AI xây dựng tương lai", - }, - - onClickExtension: () => { - window.open("https://huggingface.co/spaces?sort=likes"); - }, -}; diff --git a/scripts/index.js b/scripts/index.js index 1312a351..a1c1bcc6 100644 --- a/scripts/index.js +++ b/scripts/index.js @@ -16,7 +16,6 @@ import fb_getAllUidFromFriendsPage from "./fb_getAllUidFromFriendsPage.js"; import fb_getAllUidOfGroupMembers from "./fb_getAllUidOfGroupMembers.js"; import fb_getAvatarFromUid from "./fb_getAvatarFromUid.js"; import fb_downloadAlbumMedia from "./fb_downloadAlbumMedia.js"; -import insta_getToken from "./insta_getToken.js"; import insta_getUserInfo from "./insta_getUserInfo.js"; import insta_getAllUserMedia from "./insta_getAllUserMedia.js"; import insta_getAllImagesInNewFeed from "./insta_getAllImagesInNewFeed.js"; @@ -29,12 +28,9 @@ import doutube_downloadWatchingVideo from "./doutube_downloadWatchingVideo.js"; import doutube_getAllVideoInUserProfile from "./doutube_getAllVideoInUserProfile.js"; import darkModePDF from "./darkModePDF.js"; import webToPDF from "./webToPDF.js"; -import webToQRCode from "./webToQRCode.js"; -import textToQRCode from "./textToQRCode.js"; import scrollToVeryEnd from "./scrollToVeryEnd.js"; import passwordGenerator from "./passwordGenerator.js"; import search_sharedAccount from "./search_sharedAccount.js"; -// import passwordFieldToggle from "./passwordFieldToggle.js"; import checkWebDie from "./checkWebDie.js"; import removeCookies from "./removeCookies.js"; import simpleAllowCopy from "./simpleAllowCopy.js"; @@ -93,8 +89,6 @@ import getFavicon from "./getFavicon.js"; import fb_checkToken from "./fb_checkToken.js"; import fb_getTokenCampaigns from "./fb_getTokenCampaigns.js"; import unshorten from "./unshorten.js"; -import transfer_sh from "./transfer_sh.js"; -import jsonformatter from "./jsonformatter.js"; import screenshotFullPage from "./screenshotFullPage.js"; import visualEvent from "./visualEvent.js"; import fb_videoDownloader from "./fb_videoDownloader.js"; @@ -103,13 +97,12 @@ import douyin_downloadWachingVideo from "./douyin_downloadWachingVideo.js"; import douyin_downloadAllVideoUser from "./douyin_downloadAllVideoUser.js"; import showTheVideos from "./showTheVideos.js"; import fb_storySaver from "./fb_storySaver.js"; -import insta_storySaver from "./insta_storySaver.js"; import whatApp_storySaver from "./whatApp_storySaver.js"; import send_shareFiles from "./send_shareFiles.js"; import fb_downloadCommentVideo from "./fb_downloadCommentVideo.js"; import scribd_downloadDocuments from "./scribd_downloadDocuments.js"; import fb_toggleNewFeed from "./fb_toggleNewFeed.js"; -// import fb_storyInfo from "./fb_storyInfo.js"; +import fb_storyInfo from "./fb_storyInfo.js"; import envato_bypassPreview from "./envato_bypassPreview.js"; import shopee_topVariation from "./shopee_topVariation.js"; import ggdrive_downloadVideo from "./ggdrive_downloadVideo.js"; @@ -155,14 +148,6 @@ import fb_messengerCount from "./fb_messengerCount.js"; import fb_searchGroupForOther from "./fb_searchGroupForOther.js"; import fb_searchPageForOther from "./fb_searchPageForOther.js"; import fb_fetchAllAddedFriends from "./fb_fetchAllAddedFriends.js"; -import bing_imageCreator from "./bing_imageCreator.js"; -import stable_diffusion_baseten from "./stable_diffusion_baseten.js"; -import stable_diffusion_demo from "./stable_diffusion_demo.js"; -import dreamai from "./dreamai.js"; -import playgroundai from "./playgroundai.js"; -import pixaiart from "./pixaiart.js"; -import skybox_blockadelabs from "./skybox_blockadelabs.js"; -import huggingface from "./huggingface.js"; import tailieu_vn from "./tailieu_vn.js"; import fb_downloadWallMediaFromPosts from "./fb_downloadWallMediaFromPosts.js"; import fb_getAllAlbumInformation from "./fb_getAllAlbumInformation.js"; @@ -206,7 +191,6 @@ const allScripts = { fb_getAllUidOfGroupMembers: addBadge(fb_getAllUidOfGroupMembers, BADGES.hot), fb_getAvatarFromUid, fb_downloadAlbumMedia, - insta_getToken: addBadge(insta_getToken, BADGES.hot), insta_getUserInfo: insta_getUserInfo, insta_getAllUserMedia, insta_getAllImagesInNewFeed: addBadge( @@ -225,8 +209,6 @@ const allScripts = { doutube_getAllVideoInUserProfile, darkModePDF: addBadge(darkModePDF, BADGES.hot), webToPDF, - webToQRCode, - textToQRCode: addBadge(textToQRCode, BADGES.hot), scrollToVeryEnd, passwordGenerator: addBadge(passwordGenerator, BADGES.hot), search_sharedAccount: addBadge(search_sharedAccount, BADGES.hot), @@ -289,8 +271,6 @@ const allScripts = { fb_checkToken: fb_checkToken, fb_getTokenCampaigns: fb_getTokenCampaigns, unshorten: addBadge(unshorten, BADGES.hot), - transfer_sh: transfer_sh, - jsonformatter: jsonformatter, screenshotFullPage: screenshotFullPage, visualEvent: visualEvent, fb_videoDownloader: addBadge(fb_videoDownloader, BADGES.new), @@ -305,13 +285,12 @@ const allScripts = { ), showTheVideos: showTheVideos, fb_storySaver: addBadge(fb_storySaver, BADGES.new), - insta_storySaver: insta_storySaver, whatApp_storySaver: whatApp_storySaver, send_shareFiles: send_shareFiles, fb_downloadCommentVideo: addBadge(fb_downloadCommentVideo, BADGES.hot), scribd_downloadDocuments: addBadge(scribd_downloadDocuments, BADGES.new), fb_toggleNewFeed: fb_toggleNewFeed, - // fb_storyInfo: addBadge(fb_storyInfo, BADGES.beta), + fb_storyInfo: addBadge(fb_storyInfo, BADGES.beta), envato_bypassPreview: envato_bypassPreview, shopee_topVariation: addBadge(shopee_topVariation, BADGES.hot), ggdrive_downloadVideo: ggdrive_downloadVideo, @@ -360,14 +339,6 @@ const allScripts = { fb_searchGroupForOther: addBadge(fb_searchGroupForOther, BADGES.hot), fb_searchPageForOther: addBadge(fb_searchPageForOther, BADGES.hot), fb_fetchAllAddedFriends: fb_fetchAllAddedFriends, - bing_imageCreator: bing_imageCreator, - stable_diffusion_baseten: stable_diffusion_baseten, - stable_diffusion_demo: stable_diffusion_demo, - dreamai: dreamai, - playgroundai: playgroundai, - pixaiart: pixaiart, - skybox_blockadelabs: skybox_blockadelabs, - huggingface: huggingface, tailieu_vn: tailieu_vn, fb_downloadWallMediaFromPosts: fb_downloadWallMediaFromPosts, textToSpeech: textToSpeech, diff --git a/scripts/insta_getAllUserMedia.js b/scripts/insta_getAllUserMedia.js index 5c53014e..98a80f44 100644 --- a/scripts/insta_getAllUserMedia.js +++ b/scripts/insta_getAllUserMedia.js @@ -64,8 +64,12 @@ export default { } } console.log(all_urls); - setLoadingText(`Đang tải xuống ... (${all_urls.length} link)`); - downloadData(all_urls.join("\n"), user_id, ".txt"); + if (!all_urls?.length) { + alert("Không tìm được link ảnh/video nào."); + } else { + setLoadingText(`Đang tải xuống ... (${all_urls.length} link)`); + downloadData(all_urls.join("\n"), user_id, ".txt"); + } } catch (e) { alert("ERROR: " + e); } finally { diff --git a/scripts/insta_getToken.js b/scripts/insta_getToken.js deleted file mode 100644 index 86c29021..00000000 --- a/scripts/insta_getToken.js +++ /dev/null @@ -1,40 +0,0 @@ -export default { - icon: "https://www.instagram.com/favicon.ico", - name: { - en: "Get token insta", - vi: "Lấy token insta", - }, - description: { - en: "Get instagram access token", - vi: "Lấy instagram access token", - }, - onClick: function () { - try { - const encoded = document.cookie - .split("; ") - ?.find((_) => _.startsWith("fbsr")) - ?.split(".")[1]; - if (encoded) { - const decoded = JSON.parse(atob(encoded)); - console.log(decoded); - prompt("Access token: ", decoded.oauth_token); - } else { - alert( - "Không tìm thấy thông tin access token trong cookie!\nBạn đã đăng nhập instagram chưa??" - ); - } - } catch (e) { - alert("Lỗi: " + e.toString()); - } - }, -}; - -function backup() { - // https://greasyfork.org/en/scripts/14755-instagram-reloaded - function get_csrf_token() { - return document.body.innerHTML - .match(/\"csrf_token\":(?:"[^"]*"|^[^"]*$)/)[0] - .replace(/\"/g, "") - .split(":")[1]; - } -} diff --git a/scripts/insta_storySaver.js b/scripts/insta_storySaver.js deleted file mode 100644 index 1f4fc578..00000000 --- a/scripts/insta_storySaver.js +++ /dev/null @@ -1,119 +0,0 @@ -export default { - icon: "https://lh3.googleusercontent.com/e8gqesNOLhN-0xivFcaAlwGaoftfxEJcZXcXJ1F2bhoqrozs3mCYgLhPC0qJ9izdGYRnHwfXegimH9fjj3IBwlby9ZA=w128-h128-e365-rj-sc0x00ffffff", - name: { - en: "Download watching Instagram Story", - vi: "Tải Story Instagram đang xem", - }, - description: { - en: "Download instagram story that you are watching", - vi: "Tải instagram story bạn đang xem", - }, - - onClick: function () { - // Source code extracted from: https://chrome.google.com/webstore/detail/story-saver/mafcolokinicfdmlidhaebadidhdehpk - - let videos = document.querySelectorAll("video"); - let videoUrl = null; - for (var i = videos.length - 1; i >= 0; i--) { - if (videos[i].offsetHeight === 0) continue; - var reactKey = ""; - let keys = Object.keys(videos[i]); - for (var j = 0; j < keys.length; j++) { - if (keys[j].indexOf("__reactFiber") != -1) { - reactKey = keys[j].split("__reactFiber")[1]; - break; - } - } - try { - videoUrl = - videos[i].parentElement.parentElement.parentElement.parentElement[ - "__reactProps" + reactKey - ].children.props.children.props.implementations[0].data.hdSrc; - } catch (e) {} - if (videoUrl == null) { - try { - videoUrl = - videos[i].parentElement.parentElement.parentElement.parentElement[ - "__reactProps" + reactKey - ].children[0].props.children.props.implementations[1].data.hdSrc; - } catch (e) {} - } - if (videoUrl == null) { - try { - videoUrl = - videos[i].parentElement.parentElement.parentElement.parentElement[ - "__reactProps" + reactKey - ].children.props.children.props.implementations[0].data.sdSrc; - } catch (e) {} - } - if (videoUrl == null) { - try { - videoUrl = - videos[i].parentElement.parentElement.parentElement.parentElement[ - "__reactProps" + reactKey - ].children[0].props.children.props.implementations[1].data.sdSrc; - } catch (e) {} - } - if (videoUrl == null) { - try { - videoUrl = - videos[i].parentElement.parentElement.parentElement.parentElement[ - "__reactProps" + reactKey - ].children.props.children.props.implementations[1].data.hdSrc; - } catch (e) {} - } - if (videoUrl == null) { - try { - videoUrl = - videos[i].parentElement.parentElement.parentElement.parentElement[ - "__reactProps" + reactKey - ].children.props.children.props.implementations[1].data.sdSrc; - } catch (e) {} - } - if (videoUrl == null) { - try { - videoUrl = - videos[i]["__reactFiber" + reactKey].return.stateNode.props - .videoData.$1.hd_src; - } catch (e) {} - } - if (videoUrl == null) { - try { - videoUrl = - videos[i]["__reactFiber" + reactKey].return.stateNode.props - .videoData.$1.sd_src; - } catch (e) {} - } - if (videoUrl != null) break; - } - - if (videoUrl) window.open(videoUrl); - else alert("Không tìm thấy instagram story nào trong trang web."); - }, -}; - -function backup() { - // https://greasyfork.org/en/scripts/404535-ig-helper/code - async function getStories(userId) { - let url = `https://www.instagram.com/graphql/query/?query_hash=15463e8449a83d3d60b06be7e90627c7&variables=%7B%22reel_ids%22:%5B%22${userId}%22%5D,%22precomposed_overlay%22:false%7D`; - let res = await fetch(url); - let json = await res.json(); - return json; - } - - async function getUserId(username) { - let url = `https://www.instagram.com/web/search/topsearch/?query=${username}`; - let res = await feetch(url); - let json = await res.json(); - return json.users[0]; - } - - // postPath example: /p/CixHwr6AxZ9/ - async function getBlobMedia(postPath) { - let postShortCode = postPath.substring(3, postPath.length - 1); - let url = `https://www.instagram.com/graphql/query/?query_hash=2c4c2e343a8f64c625ba02b2aa12c7f8&variables=%7B%22shortcode%22:%22${postShortCode}%22}`; - let res = await fetch(url); - let json = await res.json(); - return json.data; - } -} diff --git a/scripts/jsonformatter.js b/scripts/jsonformatter.js deleted file mode 100644 index ae3d8310..00000000 --- a/scripts/jsonformatter.js +++ /dev/null @@ -1,36 +0,0 @@ -export default { - icon: "https://jsonformatter.org/img/favicon.png", - name: { - en: "JSON formatter", - vi: "JSON formatter", - }, - description: { - en: "Open web tool for beautify JSON in new tab", - vi: "Mở công cụ làm đẹp JSON trong tab mới", - }, - onClickExtension: () => window.open("https://jsonformatter.org/"), -}; - -async function backup() { - // try { - // let url = "https://jsonformatter.org"; - // let strObj = prompt("Nhập object json muốn làm đẹp:", ""); - // if (strObj != null) { - // let [err, obj] = strObjToObject(strObj); - // if (err) throw err; - // alert(obj); - // await setLocalStorage(url, "index", JSON.stringify(obj)); - // window.open(url); - // } - // } catch (e) { - // alert("Lỗi: " + e); - // } - // ===================================== - // https://stackoverflow.com/a/32357610/11898496 - // var win = window.open("https://jsonformatter.org"); - // win.focus(); - // win.addEventListener("load", () => { - // win.localStorage.setItem("index", "{'abc':1}"); - // win.alert("yep"); - // }); -} diff --git a/scripts/pixaiart.js b/scripts/pixaiart.js deleted file mode 100644 index 7e891d55..00000000 --- a/scripts/pixaiart.js +++ /dev/null @@ -1,15 +0,0 @@ -export default { - icon: "https://pixai.art/favicon.ico", - name: { - en: "PixAI.Art", - vi: "PixAI.Art", - }, - description: { - en: "AI character engine", - vi: "Trình tạo nhân vật AI", - }, - - onClickExtension: () => { - window.open("https://pixai.art/"); - }, -}; diff --git a/scripts/playgroundai.js b/scripts/playgroundai.js deleted file mode 100644 index 4c0231ec..00000000 --- a/scripts/playgroundai.js +++ /dev/null @@ -1,15 +0,0 @@ -export default { - icon: "https://playgroundai.com/favicon.ico", - name: { - en: "PlaygroundAI - Art AI", - vi: "PlaygroundAI - Art AI ", - }, - description: { - en: "Free-to-use online AI image creator. Use it to create art, social media posts, presentations, posters, videos, logos and more.", - vi: "Trình tạo hình ảnh AI trực tuyến miễn phí.", - }, - - onClickExtension: () => { - window.open("https://playgroundai.com"); - }, -}; diff --git a/scripts/skybox_blockadelabs.js b/scripts/skybox_blockadelabs.js deleted file mode 100644 index 5d0e73c6..00000000 --- a/scripts/skybox_blockadelabs.js +++ /dev/null @@ -1,15 +0,0 @@ -export default { - icon: "https://skybox.blockadelabs.com/favicon.png", - name: { - en: "Skybox - Blockade Labs", - vi: "Skybox - Blockade Labs", - }, - description: { - en: "360 AI Image Generator, panoramic imagery and skyboxes.", - vi: "Trình tạo ảnh 360 độ bằng AI", - }, - - onClickExtension: () => { - window.open("https://skybox.blockadelabs.com/"); - }, -}; diff --git a/scripts/stable_diffusion_baseten.js b/scripts/stable_diffusion_baseten.js deleted file mode 100644 index 9117b089..00000000 --- a/scripts/stable_diffusion_baseten.js +++ /dev/null @@ -1,15 +0,0 @@ -export default { - icon: "https://app.baseten.co/favicon.ico", - name: { - en: "Stable Diffusion - Baseten", - vi: "Stable Diffusion - Baseten", - }, - description: { - en: "Stable Diffusion Demo on Baseten", - vi: "Trình tạo ảnh AI sử dụng modal Stable Diffusion", - }, - - onClickExtension: () => { - window.open("https://app.baseten.co/apps/VBlnMVP/operator_views/nBrd8zP"); - }, -}; diff --git a/scripts/stable_diffusion_demo.js b/scripts/stable_diffusion_demo.js deleted file mode 100644 index 60fb8f53..00000000 --- a/scripts/stable_diffusion_demo.js +++ /dev/null @@ -1,33 +0,0 @@ -export default { - icon: "https://stabilityai-stable-diffusion-1.hf.space/favicon.ico", - name: { - en: "Stable Diffusion - Gradio", - vi: "Stable Diffusion - Gradio", - }, - description: { - en: "Stable Diffusion Demo on Gradio", - vi: "Trình tạo ảnh AI sử dụng modal Stable Diffusion", - }, - - onClickExtension: () => { - let choice = prompt( - "Enter your choice:\n" + - " 1: stable diffusion 1\n" + - " 2: stable diffusion 2.1\n" + - " 3: stable diffusion - image variations\n", - "2" - ); - - if (choice == null) return; - - if (choice == "1") - window.open("https://stabilityai-stable-diffusion-1.hf.space/"); - else if (choice == "2") - window.open("https://stabilityai-stable-diffusion.hf.space/"); - else if (choice == "3") - window.open( - "https://lambdalabs-stable-diffusion-image-variation-54db9c0.hf.space/" - ); - else alert("Invalid version"); - }, -}; diff --git a/scripts/textToQRCode.html b/scripts/textToQRCode.html deleted file mode 100644 index 77f04d66..00000000 --- a/scripts/textToQRCode.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - Text to QRCode - Useful script - - - - - - - -
    -
    - - - - - diff --git a/scripts/textToQRCode.js b/scripts/textToQRCode.js deleted file mode 100644 index 973fd460..00000000 --- a/scripts/textToQRCode.js +++ /dev/null @@ -1,23 +0,0 @@ -import { popupCenter } from "./helpers/utils.js"; - -export default { - icon: ``, - name: { - en: "Text to QR Code", - vi: "Chuyển chữ thành QRCode", - }, - description: { - en: "Convert text to QR Code", - vi: "Nhập vào chữ và nhận về QRCode tương ứng", - }, - - onClickExtension: function () { - let text = prompt("Enter text / Nhập chữ:", ""); - if (text === null) return; - - popupCenter({ - url: chrome.runtime.getURL("/scripts/textToQRCode.html?text=" + text), - title: "Text To QRCode", - }); - }, -}; diff --git a/scripts/textToQRCode_main.js b/scripts/textToQRCode_main.js deleted file mode 100644 index 0c8b70c1..00000000 --- a/scripts/textToQRCode_main.js +++ /dev/null @@ -1,19 +0,0 @@ -window.onload = () => { - let input = document.getElementById("text"); - let qrcode = new QRCode("qrcode"); - - let textFromUrl = new URLSearchParams(window.location.search).get("text"); - if (textFromUrl) { - qrcode.makeCode(textFromUrl); - input.value = textFromUrl; - } - - input.addEventListener("keyup", (event) => { - qrcode.makeCode(event.target.value); - }); - input.addEventListener("blur", (event) => { - qrcode.makeCode(event.target.value); - }); - - window.resizeTo(400, 400); -}; diff --git a/scripts/transfer_sh.js b/scripts/transfer_sh.js deleted file mode 100644 index c417688e..00000000 --- a/scripts/transfer_sh.js +++ /dev/null @@ -1,219 +0,0 @@ -export default { - icon: "https://transfer.sh/favicon.ico", - name: { - en: "Transfer.sh - Share file faster", - vi: "Transfer.sh - Chia sẻ file nhanh", - }, - description: { - en: "Upload file and get URL to share", - vi: "Tải file lên và nhận về link để chia sẻ", - }, - - onClickExtension: function () { - // https://transfer.sh - - // x-url-delete: https://transfer.sh/xtRy1u/Screenshot%20%281%29.png/Fpg96TDmuH5x - - // https://stackoverflow.com/a/52757538/11898496 - function selectFile(contentType, multiple) { - return new Promise((resolve) => { - let input = document.createElement("input"); - input.type = "file"; - input.multiple = multiple; - if (contentType) input.accept = contentType; - - input.onchange = (_) => { - let files = Array.from(input.files); - if (multiple) resolve(files); - else resolve(files[0]); - }; - - input.click(); - }); - } - - // https://stackoverflow.com/a/69400632/11898496 - function uploadFileWithProgress(url, file, onProgress) { - const xhr = new XMLHttpRequest(); - return { - cancel: xhr.abort, - startUpload: () => - new Promise((resolve) => { - xhr.upload.addEventListener("progress", (event) => { - if (event.lengthComputable) { - let { loaded, total } = event; - let percent = loaded / total; - onProgress?.({ percent, loaded, total }); - } - }); - xhr.onreadystatechange = () => { - if (xhr.readyState === xhr.HEADERS_RECEIVED) { - const urlDelete = xhr.getResponseHeader("x-url-delete"); - resolve(urlDelete); - } - }; - xhr.open("PUT", url, true); - xhr.send(file); - }), - }; - } - - // - //

    HOẶC

    - // Mở transfer.sh trong tab mới - - (async () => { - let html = /*html*/ `
    - -
    - -

    Transfer.sh

    - -
    -

    Đang tải lên...

    -

    File name

    - - -
    - -
    - - -
    `; - - let div = document.createElement("div"); - div.innerHTML = html; - document.body.appendChild(div); - - const containerDiv = document.querySelector(".transfer-sh-container"); - const status = containerDiv.querySelector(".status"); - const fileInfo = containerDiv.querySelector(".file-info"); - const progressDiv = containerDiv.querySelector("progress"); - const loader = containerDiv.querySelector(".loader"); - const resultDiv = containerDiv.querySelector(".result"); - const closeBtn = containerDiv.querySelector("#close-btn"); - - closeBtn.addEventListener("click", function () { - div.remove(); - }); - - // containerDiv.style.display = "flex"; - // return; - - let file = await selectFile(); - console.log(file); - - if (file) { - let url = "https://transfer.sh/" + file.name; - let { startUpload, cancel } = uploadFileWithProgress( - url, - file, - ({ percent, loaded, total }) => { - progressDiv.value = percent * 100; - - let uploadDone = percent == 1; - status.innerText = uploadDone - ? `Đang tạo link...` - : `Đang tải lên...`; - - let kb = uploadDone - ? ~~(file.size / 1e3) + "Kb" - : `${~~(loaded / 1e3)}/${~~(total / 1e3)}Kb`; - fileInfo.innerText = `${file.name}\n(${kb})`; - } - ); - - fileInfo.innerText = `${file.name}\n(${~~(file.size / 1e3)}Kb)`; - containerDiv.style.display = "flex"; - loader.style.display = "block"; - closeBtn.addEventListener("click", cancel); - - let urlDelete = await startUpload(); - status.innerText = `Tải lên hoàn tất`; - loader.style.display = "none"; - - if (urlDelete) { - let url = urlDelete.slice(0, urlDelete.lastIndexOf("/")); - let deletionToken = urlDelete.slice(urlDelete.lastIndexOf("/") + 1); - let downloadZipUrl = url + ".zip"; - let downloadTarGzUrl = url + ".tar.gz"; - - resultDiv.innerHTML = /*html*/ `
    - URL:
    - Delete token:
    - Download .zip
    - Download .tar.gz -
    `; - } else { - alert( - "Lỗi\n\nBạn có thể mở trang web sau để upload file:", - "https://transfer.sh/" - ); - } - } - })(); - }, -}; - -function backup() { - function uploadFile(url, file) { - fetch(url, { - method: "PUT", - body: file, - }) - .then((res) => { - console.log(res); - console.log(res.headers.get("x-url-delete")); - - alert("sucecss"); - }) - .catch((e) => { - alert("ERROR " + e); - }); - } -} diff --git a/scripts/webToQRCode.js b/scripts/webToQRCode.js deleted file mode 100644 index 70276963..00000000 --- a/scripts/webToQRCode.js +++ /dev/null @@ -1,27 +0,0 @@ -import { getCurrentTab, popupCenter } from "./helpers/utils.js"; - -export default { - icon: ``, - name: { - en: "URL to QR Code", - vi: "Lấy QRCode cho web hiện tại", - }, - description: { - en: "Convert current website URL to QR Code", - vi: "Chuyển URL của trang web sang QR Code", - }, - - onClickExtension: async function () { - let tab = await getCurrentTab(); - let url = tab.url; - if (!url) { - alert("Không tìm thấy url web hiện tại"); - return; - } - - popupCenter({ - url: chrome.runtime.getURL("/scripts/textToQRCode.html?text=" + url), - title: "Text To QRCode", - }); - }, -}; From 5ec5d24eb972cc65eb67548e2d4a31fbd70ec6c9 Mon Sep 17 00:00:00 2001 From: HoangTran <99.hoangtran@gmail.com> Date: Wed, 3 Apr 2024 01:54:01 +0700 Subject: [PATCH 39/40] fix isolated world for ufs_global_webpage_context --- manifest.json | 20 ++++++++++++------- .../scripts/ufs_global_webpage_context.js | 5 ----- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/manifest.json b/manifest.json index be6218f4..d019ab57 100644 --- a/manifest.json +++ b/manifest.json @@ -29,21 +29,27 @@ "content_scripts": [ { "matches": [""], - "js": [ - "scripts/content-scripts/document_start.js", - "scripts/content-scripts/scripts/ufs_global_webpage_context.js" - ], - "run_at": "document_start" + "js": ["scripts/content-scripts/scripts/ufs_global_webpage_context.js"], + "run_at": "document_start", + "world": "MAIN" + }, + { + "matches": [""], + "js": ["scripts/content-scripts/document_start.js"], + "run_at": "document_start", + "world": "ISOLATED" }, { "matches": [""], "js": ["scripts/content-scripts/document_idle.js"], - "run_at": "document_idle" + "run_at": "document_idle", + "world": "ISOLATED" }, { "matches": [""], "js": ["scripts/content-scripts/document_end.js"], - "run_at": "document_end" + "run_at": "document_end", + "world": "ISOLATED" } ], "web_accessible_resources": [ diff --git a/scripts/content-scripts/scripts/ufs_global_webpage_context.js b/scripts/content-scripts/scripts/ufs_global_webpage_context.js index 4734d82e..4152d5e9 100644 --- a/scripts/content-scripts/scripts/ufs_global_webpage_context.js +++ b/scripts/content-scripts/scripts/ufs_global_webpage_context.js @@ -1346,8 +1346,3 @@ window.UsefulScriptsUtils = UsefulScriptsUtils; // Chrome pre-34 if (!Element.prototype.matches) Element.prototype.matches = Element.prototype.webkitMatchesSelector; - -// https://mmazzarolo.com/blog/2022-08-25-simple-colored-logging-for-javascript-clis/ -window.console.success = (...args) => console.log("\x1b[32m✔\x1b[0m", ...args); -window.console.failure = (...args) => - console.error("\x1b[31mx\x1b[0m", ...args); From 2edde7ae106067c45b527f3d353e28264153c306 Mon Sep 17 00:00:00 2001 From: HoangTran <99.hoangtran@gmail.com> Date: Wed, 3 Apr 2024 01:55:03 +0700 Subject: [PATCH 40/40] remove dev features --- popup/tabs.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/popup/tabs.js b/popup/tabs.js index 15585a51..c8d97238 100644 --- a/popup/tabs.js +++ b/popup/tabs.js @@ -28,7 +28,7 @@ const tabs = [ { ...CATEGORY.search, scripts: [ - s._test, + // s._test, s.search_userscript, s.whatFont, s.similarWeb, @@ -166,7 +166,7 @@ const tabs = [ { ...CATEGORY.youtube, scripts: [ - s.youtube_localDownloader, + // s.youtube_localDownloader, s.youtube_downloadVideo, s.pictureInPicture, s.youtube_toggleLight,