index.js 21 KB

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