index.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576
  1. /**
  2. * 网页涂鸦油猴:可以针对任何网页进行任何涂鸦
  3. * @author zhaoxianlie
  4. */
  5. Date.prototype.format = function (pattern) {
  6. let pad = function (source, length) {
  7. let pre = "",
  8. negative = (source < 0),
  9. string = String(Math.abs(source));
  10. if (string.length < length) {
  11. pre = (new Array(length - string.length + 1)).join('0');
  12. }
  13. return (negative ? "-" : "") + pre + string;
  14. };
  15. if ('string' !== typeof pattern) {
  16. return this.toString();
  17. }
  18. let replacer = function (patternPart, result) {
  19. pattern = pattern.replace(patternPart, result);
  20. };
  21. let year = this.getFullYear(),
  22. month = this.getMonth() + 1,
  23. date2 = this.getDate(),
  24. hours = this.getHours(),
  25. minutes = this.getMinutes(),
  26. seconds = this.getSeconds(),
  27. milliSec = this.getMilliseconds();
  28. replacer(/yyyy/g, pad(year, 4));
  29. replacer(/yy/g, pad(parseInt(year.toString().slice(2), 10), 2));
  30. replacer(/MM/g, pad(month, 2));
  31. replacer(/M/g, month);
  32. replacer(/dd/g, pad(date2, 2));
  33. replacer(/d/g, date2);
  34. replacer(/HH/g, pad(hours, 2));
  35. replacer(/H/g, hours);
  36. replacer(/hh/g, pad(hours % 12, 2));
  37. replacer(/h/g, hours % 12);
  38. replacer(/mm/g, pad(minutes, 2));
  39. replacer(/m/g, minutes);
  40. replacer(/ss/g, pad(seconds, 2));
  41. replacer(/s/g, seconds);
  42. replacer(/SSS/g, pad(milliSec, 3));
  43. replacer(/S/g, milliSec);
  44. return pattern;
  45. };
  46. // 本地开发用,简单模拟实现chrome.storage.local
  47. if (new URL(location.href).protocol.startsWith('http')) {
  48. window.chrome = window.chrome || {};
  49. window.chrome.storage = {
  50. local: {
  51. get(key, callback) {
  52. let obj = [];
  53. [].concat(key).forEach(k => {
  54. obj[k] = localStorage.getItem(k);
  55. });
  56. callback && callback(obj);
  57. },
  58. set(obj, callback) {
  59. Object.keys(obj).forEach(key => localStorage.setItem(key, obj[key]));
  60. callback && callback();
  61. }
  62. }
  63. };
  64. }
  65. !RegExp.prototype.toJSON && Object.defineProperty(RegExp.prototype, "toJSON", {
  66. value: RegExp.prototype.toString
  67. });
  68. ////////////////////////////////////////////////////////////////////////////////////////////
  69. let editor = null;
  70. const PAGE_MONKEY_LOCAL_STORAGE_KEY = 'PAGE-MODIFIER-LOCAL-STORAGE-KEY';
  71. new Vue({
  72. el: '#pageContainer',
  73. data: {
  74. editing: false,
  75. editCM: {},
  76. editWithUI: true,
  77. cachedMonkeys: []
  78. },
  79. mounted: function () {
  80. this.editCM = this.getANewCM();
  81. // 退出的时候检测是否有未保存的数据
  82. window.onbeforeunload = (e) => {
  83. if (this.editCM.unSaved) {
  84. (e || window.event).returnValue = '当前还有未保存的数据,确定要离开么?';
  85. }
  86. };
  87. // 初始化获取数据
  88. this.getPageMonkeyConfigs((cmList) => {
  89. this.cachedMonkeys = (cmList || []).filter(cm => cm.mName && cm.mPattern);
  90. });
  91. this.loadPatchHotfix();
  92. },
  93. methods: {
  94. loadPatchHotfix() {
  95. // 页面加载时自动获取并注入页面的补丁
  96. chrome.runtime.sendMessage({
  97. type: 'fh-dynamic-any-thing',
  98. thing: 'fh-get-tool-patch',
  99. toolName: 'page-monkey'
  100. }, patch => {
  101. if (patch) {
  102. if (patch.css) {
  103. const style = document.createElement('style');
  104. style.textContent = patch.css;
  105. document.head.appendChild(style);
  106. }
  107. if (patch.js) {
  108. try {
  109. if (window.evalCore && window.evalCore.getEvalInstance) {
  110. window.evalCore.getEvalInstance(window)(patch.js);
  111. }
  112. } catch (e) {
  113. console.error('page-monkey补丁JS执行失败', e);
  114. }
  115. }
  116. }
  117. });
  118. },
  119. getPageMonkeyConfigs: function (callback) {
  120. chrome.storage.local.get(PAGE_MONKEY_LOCAL_STORAGE_KEY, (resps) => {
  121. let cacheMonkeys, storageMode = false;
  122. if (!resps || !resps[PAGE_MONKEY_LOCAL_STORAGE_KEY]) {
  123. cacheMonkeys = localStorage.getItem(PAGE_MONKEY_LOCAL_STORAGE_KEY) || '[]';
  124. storageMode = true;
  125. } else {
  126. cacheMonkeys = resps[PAGE_MONKEY_LOCAL_STORAGE_KEY] || '[]';
  127. }
  128. callback && callback(JSON.parse(cacheMonkeys));
  129. // 本地存储的内容,需要全部迁移到chrome.storage.local中,以确保unlimitedStorage
  130. if (storageMode) {
  131. let storageData = {};
  132. storageData[PAGE_MONKEY_LOCAL_STORAGE_KEY] = cacheMonkeys;
  133. chrome.storage.local.set(storageData);
  134. }
  135. });
  136. },
  137. /**
  138. * 存储 网页涂鸦油猴 的配置
  139. * @param monkeys
  140. * @param callback
  141. * @private
  142. */
  143. savePageMonkeyConfigs: function (monkeys, callback) {
  144. let storageData = {};
  145. storageData[PAGE_MONKEY_LOCAL_STORAGE_KEY] = JSON.stringify(monkeys);
  146. chrome.storage.local.set(storageData);
  147. callback && callback();
  148. },
  149. getANewCM: function () {
  150. return {
  151. id: 'mf_' + new Date() * 1,
  152. mName: '',
  153. mPattern: '',
  154. mScript: '',
  155. mRefresh: 0,
  156. mDisabled: false,
  157. mRequireJs: '',
  158. mUpdatedAt: new Date().format('yyyy-MM-dd HH:mm:ss')
  159. };
  160. },
  161. initEditor() {
  162. if (!editor) {
  163. // 编辑器初始化
  164. editor = CodeMirror.fromTextArea(this.$refs.mScript, {
  165. mode: "text/javascript",
  166. lineNumbers: true,
  167. matchBrackets: true,
  168. styleActiveLine: true,
  169. lineWrapping: true
  170. });
  171. editor.on('keydown', (editor, event) => {
  172. if (event.metaKey || event.ctrlKey) {
  173. if (event.code === 'KeyS') {
  174. this.saveMonkey();
  175. event.preventDefault();
  176. event.stopPropagation();
  177. return false;
  178. }
  179. }
  180. });
  181. }
  182. },
  183. toggleEditMode() {
  184. let unSaved = this.editCM.unSaved;
  185. if (this.editWithUI) { // UI界面模式
  186. editor.setValue(this.deflateMonkey(this.getEditMonkey()));
  187. } else { // 纯代码编辑模式
  188. let curMonkey = this.inflateMonkey(editor.getValue());
  189. this.editCM = {...curMonkey};
  190. this.$nextTick(() => {
  191. editor.setValue(this.editCM.mScript);
  192. })
  193. }
  194. this.editCM.unSaved = unSaved;
  195. this.editWithUI = !this.editWithUI;
  196. },
  197. createMonkey: function () {
  198. this.editing = true;
  199. this.editCM.unSaved = true;
  200. this.editCM = this.getANewCM();
  201. this.initEditor();
  202. this.$nextTick(() => {
  203. editor.setValue(window.MonkeyNewGuide);
  204. });
  205. },
  206. selectMonkey: function (cm) {
  207. this.editing = true;
  208. // 把数据呈现到编辑面板
  209. this.editCM = cm;
  210. this.initEditor();
  211. this.$nextTick(() => {
  212. editor.setValue(this.editWithUI ? cm.mScript : this.deflateMonkey(cm));
  213. });
  214. },
  215. getEditMonkey() {
  216. if (this.editWithUI) {
  217. this.editCM.mScript = editor && editor.getValue() || this.editCM.mScript;
  218. this.editCM.mUpdatedAt = new Date().format('yyyy-MM-dd HH:mm:ss');
  219. this.editCM.mDisabled = !!this.editCM.mDisabled;
  220. return this.editCM;
  221. } else {
  222. return this.inflateMonkey(editor.getValue());
  223. }
  224. },
  225. saveMonkey: function () {
  226. let curMonkey = this.getEditMonkey();
  227. let found = this.cachedMonkeys.some((cm, index) => {
  228. if (cm.id === curMonkey.id) {
  229. this.cachedMonkeys[index] = curMonkey;
  230. return true;
  231. }
  232. });
  233. if (!found && curMonkey.mName && curMonkey.mPattern) this.cachedMonkeys.push(curMonkey);
  234. this.savePageMonkeyConfigs(this.cachedMonkeys, () => {
  235. this.editCM.unSaved = false;
  236. this.toast('恭喜,您的操作已成功并生效!');
  237. });
  238. },
  239. closeEditor() {
  240. if (this.editCM.unSaved) {
  241. if (confirm('检测到当前猴子有修改,是否先保存?')) {
  242. this.savePageMonkeyConfigs(this.cachedMonkeys, () => {
  243. this.toast('恭喜,您的操作已成功并生效!');
  244. });
  245. }
  246. }
  247. this.editing = false;
  248. this.editCM.unSaved = false;
  249. },
  250. loadMonkeys(monkeys) {
  251. if (monkeys && Array.isArray(monkeys) && monkeys.length) {
  252. let keys = 'mName,mPattern,mRefresh,mScript,mDisabled,mRequireJs,mUpdatedAt'.split(',');
  253. let result = monkeys.filter(item => {
  254. if (typeof item === 'object') {
  255. Object.keys(item).forEach(k => {
  256. !keys.includes(k) && k !== 'id' && delete(item[k]);
  257. });
  258. if (!item.mUpdatedAt) {
  259. item.mUpdatedAt = new Date().format('yyyy-MM-dd HH:mm:ss');
  260. }
  261. if (Object.keys(item).length) {
  262. return true;
  263. }
  264. }
  265. return false;
  266. });
  267. if (result.length) {
  268. let merge = null;
  269. // 配置合并,如果有重复的,则弹框确认
  270. result.forEach(r => {
  271. let found = this.cachedMonkeys.some(cm => {
  272. if (r.id === cm.id || r.mName === cm.mName || r.mPattern === cm.mPattern) {
  273. if (merge === null) {
  274. merge = confirm('发现有相同名称或规则的油猴,是否选择覆盖?');
  275. }
  276. if (merge) {
  277. keys.forEach(k => {
  278. cm[k] = r[k];
  279. });
  280. cm.mUpdatedAt = new Date().format('yyyy-MM-dd HH:mm:ss');
  281. }
  282. return true;
  283. }
  284. });
  285. if (!found || merge === false) {
  286. let newCm = {...r};
  287. newCm.id = this.getANewCM().id;
  288. newCm.mUpdatedAt = new Date().format('yyyy-MM-dd HH:mm:ss');
  289. this.cachedMonkeys.push(newCm);
  290. }
  291. });
  292. this.savePageMonkeyConfigs(this.cachedMonkeys, () => {
  293. this.toast('恭喜,您的操作已成功并生效!');
  294. });
  295. }
  296. }
  297. },
  298. // 将Monkey内容打平后输出
  299. deflateMonkey(monkey) {
  300. let strPad = (str, place) => {
  301. return String(str).padEnd(20, place || ' ');
  302. };
  303. let jsCode = [];
  304. // 注释头部分
  305. jsCode.push('// ==FeHelperMonkey==');
  306. jsCode.push(strPad('// @reminder') + '请不要删除这部分代码注释,这是FeHelper油猴脚本能正常工作的基本条件!当然,你可以按需修改这里的内容!');
  307. jsCode.push(strPad('// @id') + monkey.id);
  308. jsCode.push(strPad('// @name') + monkey.mName);
  309. jsCode.push(strPad('// @url-pattern') + monkey.mPattern);
  310. jsCode.push(strPad('// @enable') + !monkey.mDisabled);
  311. let jsFiles = (monkey.mRequireJs || '').split(/[\s,,]+/).filter(js => js.length);
  312. jsFiles = Array.from(new Set(jsFiles));
  313. jsFiles.forEach(js => {
  314. jsCode.push(strPad('// @require-js') + js);
  315. });
  316. if (!jsFiles.length) {
  317. jsCode.push(strPad('// @require-js'));
  318. }
  319. jsCode.push(strPad('// @auto-refresh') + monkey.mRefresh);
  320. jsCode.push(strPad('// @updated') + monkey.mUpdatedAt);
  321. jsCode.push('// ==/FeHelperMonkey==\n\n');
  322. // 代码部分
  323. jsCode.push(monkey.mScript);
  324. // 输出
  325. return jsCode.join('\n');
  326. },
  327. // 从一个js文件内容中,提取并解析为monkey对象
  328. inflateMonkey(jsCode) {
  329. if (jsCode.indexOf('// ==FeHelperMonkey==') !== 0) throw new Error('wrong file header');
  330. if (jsCode.indexOf('// ==/FeHelperMonkey==') === -1) throw new Error('wrong file header');
  331. let [comments, scripts] = jsCode.split('// ==/FeHelperMonkey==');
  332. let monkey = this.getANewCM();
  333. monkey.mScript = (scripts || '').trim();
  334. comments.split('\n').forEach(cmt => {
  335. if (cmt.startsWith('// @id')) {
  336. monkey.id = cmt.split('// @id')[1].trim();
  337. } else if (cmt.startsWith('// @name')) {
  338. monkey.mName = cmt.split('// @name')[1].trim();
  339. } else if (cmt.startsWith('// @url-pattern')) {
  340. monkey.mPattern = cmt.split('// @url-pattern')[1].trim();
  341. } else if (cmt.startsWith('// @enable')) {
  342. monkey.mDisabled = cmt.split('// @enable')[1].trim() === 'false';
  343. } else if (cmt.startsWith('// @auto-refresh')) {
  344. monkey.mRefresh = parseInt(cmt.split('// @auto-refresh')[1].trim());
  345. } else if (cmt.startsWith('// @updated')) {
  346. monkey.mUpdatedAt = cmt.split('// @updated')[1].trim();
  347. } else if (cmt.startsWith('// @require-js')) {
  348. let jsFiles = (monkey.mRequireJs || '').split(/[\s,,]+/).filter(js => js.length);
  349. jsFiles = Array.from(new Set(jsFiles));
  350. jsFiles.push(cmt.split('// @require-js')[1].trim());
  351. monkey.mRequireJs = jsFiles.join(',');
  352. }
  353. });
  354. if (!monkey.mName || !monkey.mPattern) {
  355. throw new Error('wrong file format,no name or url-pattern');
  356. }
  357. return monkey;
  358. },
  359. // 导入配置
  360. importMonkey: function () {
  361. let that = this;
  362. let fileInput = document.getElementById('fileInput');
  363. if (!fileInput) {
  364. fileInput = document.createElement('input');
  365. fileInput.id = 'fileInput';
  366. fileInput.type = 'file';
  367. fileInput.accept = 'application/json,text/javascript';
  368. fileInput.style.cssText = 'position:relative;top:-1000px;left:-1000px;';
  369. fileInput.onchange = (event) => {
  370. let reader = new FileReader();
  371. reader.readAsText(fileInput.files[0], 'utf-8');
  372. reader.onload = (evt) => {
  373. let content = evt.target.result;
  374. if (/\.js$/.test(fileInput.files[0].name)) {
  375. // 新版本,导出的是一个js文件,直接读取
  376. try {
  377. let monkey = this.inflateMonkey(content);
  378. this.loadMonkeys([monkey]);
  379. } catch (e) {
  380. this.toast('当前选择的js文件不符合FeHelper Monkey脚本文件格式!');
  381. }
  382. } else if (/\.json$/.test(fileInput.files[0].name)) {
  383. // 老版本,导出的是json格式,做向下兼容
  384. try {
  385. // 过滤掉文件头部所有注释,然后转化成json
  386. let list = JSON.parse(content.replace(/^\/\*[^\*]*\*\//, ''));
  387. this.loadMonkeys(list);
  388. } catch (e) {
  389. this.toast('当前选择的JSON配置文件格式不正确!');
  390. }
  391. }
  392. };
  393. };
  394. document.body.appendChild(fileInput);
  395. }
  396. fileInput.click();
  397. },
  398. // 导出配置
  399. exportMonkey: function (theCM) {
  400. let exportHandler = monkey => {
  401. let blob = new Blob([this.deflateMonkey(monkey)], {type: 'application/octet-stream'});
  402. if (typeof chrome === 'undefined' || !chrome.permissions) {
  403. let aLink = document.getElementById('btnDownloadMonkey');
  404. if (!aLink) {
  405. aLink = document.createElement('a');
  406. aLink.setAttribute('id', 'btnDownloadMonkey');
  407. aLink.style.cssText = 'position:absolute;top:-1000px;left:-1000px';
  408. document.body.appendChild(aLink);
  409. }
  410. aLink.setAttribute('download', `FhMonkey-${monkey.mName}.js`);
  411. aLink.setAttribute('href', URL.createObjectURL(blob));
  412. aLink.click();
  413. } else {
  414. chrome.permissions.request({
  415. permissions: ['downloads']
  416. }, (granted) => {
  417. if (granted) {
  418. chrome.downloads.download({
  419. url: URL.createObjectURL(blob),
  420. saveAs: true,
  421. conflictAction: 'overwrite',
  422. filename: `FhMonkey-${monkey.mName}.js`
  423. });
  424. } else {
  425. this.toast('必须接受授权,才能正常导出!');
  426. }
  427. });
  428. }
  429. };
  430. exportHandler(theCM);
  431. },
  432. // 清空油猴
  433. removeMonkey: function (theCM) {
  434. if (confirm('你确定要删除所有的油猴吗,此操作不可撤销!')) {
  435. if (theCM) {
  436. this.cachedMonkeys = this.cachedMonkeys.filter(cm => {
  437. return cm.id !== theCM.id;
  438. });
  439. } else {
  440. this.cachedMonkeys = [];
  441. }
  442. this.savePageMonkeyConfigs(this.cachedMonkeys, () => {
  443. this.toast('恭喜,您的操作已成功并生效!');
  444. });
  445. }
  446. },
  447. // 停用油猴
  448. disableMonkey: function (theCM) {
  449. if (theCM) {
  450. this.cachedMonkeys.some(cm => {
  451. if (cm.id === theCM.id) {
  452. cm.mDisabled = !theCM.mDisabled;
  453. cm.mUpdatedAt = new Date().format('yyyy-MM-dd HH:mm:ss');
  454. return true;
  455. }
  456. });
  457. this.savePageMonkeyConfigs(this.cachedMonkeys, () => {
  458. this.toast(`猴子「 ${theCM.mName} 」相关配置已修改成功!`);
  459. });
  460. } else {
  461. if (confirm('停用油猴后,可单独编辑启用;是否继续此操作?')) {
  462. this.cachedMonkeys.forEach(cm => {
  463. cm.mDisabled = true;
  464. cm.mUpdatedAt = new Date().format('yyyy-MM-dd HH:mm:ss');
  465. });
  466. this.savePageMonkeyConfigs(this.cachedMonkeys, () => {
  467. this.toast('所有猴子均已停用!');
  468. });
  469. }
  470. }
  471. },
  472. // 引入Demo
  473. loadDemo() {
  474. if(confirm('郑重声明:这个Demo是在你打开百度网站时,自动搜索FeHelper,这就是一个用于演示油猴的示例!!!' +
  475. '所以,请记得体验完以后自行停用这个Demo!!!!!!!要不然,你一定会误会作者耍流氓,那作者就真的心凉了。。。')) {
  476. this.loadMonkeys(MonkeyTpl);
  477. }
  478. },
  479. /**
  480. * 自动消失的Alert弹窗
  481. * @param content
  482. */
  483. toast(content) {
  484. window.clearTimeout(window.feHelperAlertMsgTid);
  485. let elAlertMsg = document.querySelector("#fehelper_alertmsg");
  486. if (!elAlertMsg) {
  487. let elWrapper = document.createElement('div');
  488. elWrapper.innerHTML = '<div id="fehelper_alertmsg">' + content + '</div>';
  489. elAlertMsg = elWrapper.childNodes[0];
  490. document.body.appendChild(elAlertMsg);
  491. } else {
  492. elAlertMsg.innerHTML = content;
  493. elAlertMsg.style.display = 'block';
  494. }
  495. window.feHelperAlertMsgTid = window.setTimeout(function () {
  496. elAlertMsg.style.display = 'none';
  497. }, 1000);
  498. },
  499. openDonateModal: function(event) {
  500. event.preventDefault();
  501. event.stopPropagation();
  502. chrome.runtime.sendMessage({
  503. type: 'fh-dynamic-any-thing',
  504. thing: 'open-donate-modal',
  505. params: { toolName: 'page-monkey' }
  506. });
  507. },
  508. openOptionsPage: function(event) {
  509. event.preventDefault();
  510. event.stopPropagation();
  511. chrome.runtime.openOptionsPage();
  512. }
  513. }
  514. });