index.js 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795
  1. import Awesome from '../background/awesome.js';
  2. const DEV_TOOLS_MY_TOOLS = 'DEV-TOOLS:MY-TOOLS';
  3. const TOOL_NAME_TPL = 'DYNAMIC_TOOL:#TOOL-NAME#';
  4. const TOOL_CONTENT_SCRIPT_TPL = 'DYNAMIC_TOOL:CS:#TOOL-NAME#';
  5. const TOOL_CONTENT_SCRIPT_CSS_TPL = 'DYNAMIC_TOOL:CS:CSS:#TOOL-NAME#';
  6. let editor = null;
  7. new Vue({
  8. el: '#pageContainer',
  9. data: {
  10. showGivenIcons: false,
  11. myTools: {},
  12. showEditorFlag: false,
  13. showNewToolForm: false,
  14. updateUrlMode: false,
  15. givenIconList: [],
  16. model: {},
  17. demo: {
  18. name: 'hello-world',
  19. files: ['fh-config.js', 'index.html', 'index.js', 'index.css', 'content-script.js']
  20. }
  21. },
  22. mounted: function () {
  23. // 本地没安装过demo,就强制再更新一遍
  24. this.getContentFromLocal(this.demo.name, 'index.html').then(content => {
  25. if(content) {
  26. this.getToolConfigs();
  27. }else{
  28. this.loadDemo().then(() => this.getToolConfigs());
  29. }
  30. });
  31. },
  32. methods: {
  33. toggleEditor(show, toolName) {
  34. this.showEditorFlag = show;
  35. if (show && toolName) {
  36. let toolObj = this.myTools[toolName] || {};
  37. this.model = {
  38. tool: toolName,
  39. name: toolObj.name
  40. };
  41. this.getToolFilesFromLocal(toolName).then(files => {
  42. this.model.files = files;
  43. if (!editor) {
  44. editor = CodeMirror.fromTextArea(this.$refs.txtEditor, {
  45. mode: 'javascript',
  46. lineNumbers: true,
  47. matchBrackets: true,
  48. styleActiveLine: true,
  49. lineWrapping: true
  50. });
  51. editor.on('change', (editor, changes) => {
  52. let result = this.saveContentToLocal(this.model.tool, this.model.editingFile, editor.getValue());
  53. if (this.model.editingFile === 'fh-config.js' && result) {
  54. result.contentScriptJs && !this.model.files.includes('content-script.js') && this.model.files.push('content-script.js');
  55. result.contentScriptCss && !this.model.files.includes('content-script.css') && this.model.files.push('content-script.css');
  56. this.$forceUpdate();
  57. }
  58. });
  59. editor.on('keydown', (editor, event) => {
  60. if (event.metaKey || event.ctrlKey) {
  61. if (event.code === 'KeyS') {
  62. this.toast('当前代码是自动保存的,无需Ctrl+S手动保存!');
  63. event.preventDefault();
  64. event.stopPropagation();
  65. return false;
  66. }
  67. }
  68. });
  69. }
  70. this.$nextTick(() => this.editFile(toolName, files[0]));
  71. });
  72. }
  73. },
  74. editFile(toolName, fileName) {
  75. let editorMode = {
  76. css: 'text/css',
  77. js: {name: 'javascript', json: true},
  78. html: 'htmlmixed'
  79. };
  80. let mode = editorMode[/\.(js|css|html)$/.exec(fileName)[1]];
  81. editor.setOption('mode', mode);
  82. this.model.editingFile = fileName;
  83. this.getContentFromLocal(toolName, fileName).then(content => {
  84. editor.setValue(content);
  85. editor.focus();
  86. this.$forceUpdate();
  87. });
  88. },
  89. importFile(toolName) {
  90. let fileInput = document.createElement('input');
  91. fileInput.type = 'file';
  92. fileInput.multiple = 'multiple';
  93. fileInput.accept = 'text/javascript,text/css,text/html';
  94. fileInput.style.cssText = 'position:absolute;top:-100px;left:-100px';
  95. fileInput.addEventListener('change', (evt) => {
  96. Array.prototype.slice.call(fileInput.files).forEach(file => {
  97. let reader = new FileReader();
  98. reader.onload = (evt) => {
  99. if (this.model.files.includes(file.name)) {
  100. if (!confirm(`文件 ${file.name} 已经存在,是否需要覆盖?`)) {
  101. return false;
  102. }
  103. } else {
  104. this.model.files.push(file.name);
  105. }
  106. this.saveContentToLocal(toolName, file.name, evt.target.result);
  107. this.editFile(toolName, file.name);
  108. };
  109. reader.readAsText(file);
  110. })
  111. }, false);
  112. document.body.appendChild(fileInput);
  113. fileInput.click();
  114. window.setTimeout(() => fileInput.remove(), 3000);
  115. },
  116. createFile(toolName) {
  117. let fileName = prompt('请输入你要创建的文件名!注意,只能是 *.html 、*.js 、*.css 类型的文件!').trim();
  118. let result = /^[\w\-_\.]+\.(html|js|css)$/.exec(fileName);
  119. if (!result) {
  120. return alert('文件格式不正确!创建文件失败!');
  121. }
  122. if (this.model.files.includes(fileName)) {
  123. return alert(`文件 ${fileName} 已经存在!`);
  124. }
  125. this.model.files.push(fileName);
  126. this.saveContentToLocal(toolName, fileName, '');
  127. this.editFile(toolName, fileName);
  128. },
  129. deleteFile(toolName, fileName, event) {
  130. event.preventDefault();
  131. event.stopPropagation();
  132. if (['fh-config.js', 'index.html'].includes(fileName)) {
  133. return alert(`文件 ${fileName} 不允许被删除!`);
  134. }
  135. if (confirm(`确定要删除文件 ${fileName} 吗?此操作不可撤销,请三思!`)) {
  136. this.model.files.splice(this.model.files.indexOf(fileName), 1);
  137. this.$forceUpdate();
  138. let key = '';
  139. switch (fileName) {
  140. case 'index.html':
  141. key = TOOL_NAME_TPL.replace('#TOOL-NAME#', toolName);
  142. break;
  143. case 'content-script.js':
  144. key = TOOL_CONTENT_SCRIPT_TPL.replace('#TOOL-NAME#', toolName);
  145. break;
  146. case 'content-script.css':
  147. key = TOOL_CONTENT_SCRIPT_CSS_TPL.replace('#TOOL-NAME#', toolName);
  148. break;
  149. default:
  150. key = fileName.startsWith(`../${toolName}/`) ? fileName : `../${toolName}/${fileName}`;
  151. }
  152. Awesome.StorageMgr.remove(key);
  153. }
  154. },
  155. addNewTool(localOrUrl) {
  156. this.showNewToolForm = true;
  157. this.updateUrlMode = localOrUrl === 'url';
  158. if (this.updateUrlMode) {
  159. this.toast('请务必载入自己的Web工具服务!PS:请尽量别把它当成网站原内容的爬取工具,因为别人的网站你爬取过来也不一定完全能运行!');
  160. }
  161. },
  162. newToolAction(event) {
  163. let toolId = this.$refs.toolId.value;
  164. let toolName = this.$refs.toolName.value;
  165. let toolIcon = this.$refs.toolIcon.value;
  166. let contentScript = this.$refs.hasContentScript.checked;
  167. let noPage = this.$refs.noPage.checked;
  168. let updateUrl = '';
  169. if (this.updateUrlMode) {
  170. updateUrl = this.$refs.updateUrl.value;
  171. if (updateUrl.indexOf('baidufe.com') > -1 || updateUrl.indexOf('fehelper.com') > -1) {
  172. return this.toast('如果你是要安装FeHelper官网的工具,请到插件配置页直接安装!');
  173. }
  174. }
  175. if (this.myTools[toolId]) {
  176. this.toast(`ID为 ${toolId} 的工具在本地已存在,请重新命名!`);
  177. event.preventDefault();
  178. return false;
  179. }
  180. // 关闭Form表单
  181. this.showNewToolForm = false;
  182. // 创建文件列表
  183. let files = ['fh-config.js'];
  184. (contentScript || noPage) && files.push('content-script.js');
  185. // 本地创建的模式,需要用模板来初始化
  186. if (!this.updateUrlMode) {
  187. files.push('index.html');
  188. if (!noPage) {
  189. files.push('index.css');
  190. files.push('index.js');
  191. }
  192. }
  193. // 初始化的文件内容,需要进行存储
  194. files.forEach(file => {
  195. let content = FileTpl[file].replace(/#toolName#/gm, toolId)
  196. .replace(/#toolFullName#/gm, toolName)
  197. .replace(/#toolIcon#/gm, toolIcon)
  198. .replace(/#updateUrl#/gm, updateUrl)
  199. .replace(/#contentScript#/gm, !!contentScript || !!noPage)
  200. .replace(/#noPage#/gm, !!noPage)
  201. .replace(/#toolNameLower#/gm, toolId.replace(/[\-_]/g, ''));
  202. if (noPage && file === 'content-script.js') {
  203. content += '\n\n' + FileTpl['noPage.js'].replace(/#toolName#/gm, toolId)
  204. .replace(/#toolNameLower#/gm, toolId.replace(/[\-_]/g, ''));
  205. }
  206. this.saveContentToLocal(toolId, file, content);
  207. });
  208. if (this.updateUrlMode) {
  209. // 远程下载并安装工具
  210. this.loadRemoteTool(toolId, updateUrl, this.toast).then(progress => {
  211. this.toast(progress);
  212. this.toggleEditor(true, toolId);
  213. this.toast('工具创建成功!现在可以进行实时编辑了!');
  214. });
  215. } else {
  216. this.toggleEditor(true, toolId);
  217. this.toast('工具创建成功!现在可以进行实时编辑了!');
  218. }
  219. event.preventDefault();
  220. return false;
  221. },
  222. loadRemoteTool(toolName, updateUrl, fnProgress) {
  223. return new Promise((resolve, reject) => {
  224. fnProgress && fnProgress('开始下载...');
  225. fetch(updateUrl).then(resp => resp.text()).then(html => {
  226. let result = this.htmlTplEncode(toolName, html, updateUrl);
  227. html = result.html;
  228. let files = result.jsCss;
  229. // 获取所有网络文件的总个数,以便于计算进度
  230. let total = eval(Object.values(files).map(a => a.length).join('+')) + 1;
  231. let loaded = 1;
  232. fnProgress && fnProgress(Math.floor(100 * loaded / total) + '%');
  233. (async () => {
  234. let toolObj = this.myTools[toolName];
  235. for (let t in files) {
  236. for (let f = 0; f < files[t].length; f++) {
  237. let fs = files[t][f];
  238. // script-block内识别出来的代码,直接保存
  239. if (t === 'js' && fs[0].indexOf('fh-script-block.js') > -1) {
  240. this.saveContentToLocal(toolName, fs[0], fs[2]);
  241. continue;
  242. }
  243. await fetch(fs[2]).then(resp => resp.text()).then(txt => {
  244. this.saveContentToLocal(toolName, fs[0], txt);
  245. // 保存content-script / background-script
  246. if (toolObj.contentScriptJs && fs[0].indexOf(toolName + '/content-script.js') !== -1) {
  247. this.saveContentToLocal(toolName, 'content-script.js', txt);
  248. // 存储content-script.css文件内容
  249. if (toolObj.contentScriptCss) {
  250. fetch(fs[2].replace('content-script.js', 'content-script.css')).then(resp => resp.text()).then(css => {
  251. this.saveContentToLocal(toolName, 'content-script.css', css);
  252. });
  253. }
  254. }
  255. fnProgress && fnProgress(Math.floor(100 * ++loaded / total) + '%');
  256. });
  257. }
  258. }
  259. // 全部下载完成!
  260. resolve && resolve('100%');
  261. })();
  262. this.saveContentToLocal(toolName, 'index.html', html);
  263. }).catch(e => {
  264. this.delToolConfigs(toolName);
  265. fnProgress && fnProgress(`糟糕,下载出错,工具远程安装失败!${e.toString()}`);
  266. });
  267. });
  268. },
  269. loadDemo() {
  270. let demoName = this.demo.name;
  271. let files = this.demo.files;
  272. let site = '.';
  273. if (window.chrome && chrome.runtime && chrome.runtime.getURL) {
  274. site = chrome.runtime.getURL('devtools');
  275. }
  276. let arrPromise = files.map(file => fetch(`${site}/${demoName}/${file}`).then(resp => resp.text()));
  277. return Promise.all(arrPromise).then(contents => {
  278. // fh-config.js
  279. let json = JSON.parse(contents[0]);
  280. this.addToolConfigs(json);
  281. // index.html
  282. let result = this.htmlTplEncode(demoName, contents[1]);
  283. this.saveContentToLocal(demoName, files[1], result.html, true);
  284. // 其他文件
  285. for (let i = 2; i < contents.length; i++) {
  286. this.saveContentToLocal(demoName, files[i], contents[i]);
  287. }
  288. this.toast('工具更新成功!');
  289. });
  290. },
  291. downloadTool(tool) {
  292. let toolName = tool || this.demo.name;
  293. this.getToolFilesFromLocal(toolName).then(files => {
  294. let arrPromise = files.map(file => this.getContentFromLocal(toolName, file));
  295. Promise.all(arrPromise).then(contents => {
  296. let zipper = new JSZip();
  297. let zipPkg = zipper.folder(toolName);
  298. files.forEach((file, index) => zipPkg.file(file, contents[index]));
  299. zipper.generateAsync({type: "blob"})
  300. .then(function (content) {
  301. let elA = document.createElement('a');
  302. elA.style.cssText = 'position:absolute;top:-1000px;left:-10000px;';
  303. elA.setAttribute('download', `${toolName}.zip`);
  304. elA.href = URL.createObjectURL(new Blob([content], {type: 'application/octet-stream'}));
  305. document.body.appendChild(elA);
  306. elA.click();
  307. });
  308. });
  309. });
  310. },
  311. downloadDemo() {
  312. // 本地没安装过demo,就强制再更新一遍
  313. this.getContentFromLocal(this.demo.name, 'index.html').then(content => {
  314. if(content) {
  315. this.downloadTool(this.demo.name);
  316. }else{
  317. this.loadDemo().then(() => this.downloadTool(this.demo.name));
  318. }
  319. });
  320. },
  321. upgrade(tool, urlMode) {
  322. if (tool === this.demo.name) {
  323. this.loadDemo();
  324. } else if (urlMode) {
  325. // 远程下载并安装工具
  326. this.loadRemoteTool(tool, this.myTools[tool].updateUrl, this.toast).then(progress => {
  327. this.toast(progress);
  328. this.toast('工具更新完成!');
  329. });
  330. } else {
  331. this.loadTool(true, tool);
  332. }
  333. },
  334. loadTool(upgradeMode, upgradeToolName) {
  335. let Model = (function () {
  336. zip.useWebWorkers = false;
  337. return {
  338. getEntries: function (file, onend) {
  339. zip.createReader(new zip.BlobReader(file), function (zipReader) {
  340. zipReader.getEntries(onend);
  341. }, function (e) {
  342. console.log(e);
  343. });
  344. },
  345. getEntryFile: function (entry, onend, onprogress) {
  346. entry.getData(new zip.TextWriter(), function (text) {
  347. onend(text);
  348. }, onprogress);
  349. }
  350. };
  351. })();
  352. let fileInput = document.createElement('input');
  353. fileInput.type = 'file';
  354. fileInput.accept = 'application/zip';
  355. fileInput.style.cssText = 'position:absolute;top:-100px;left:-100px';
  356. fileInput.addEventListener('change', (evt) => {
  357. let toolName = fileInput.files[0].name.replace('.zip', '');
  358. if (upgradeMode && upgradeToolName !== toolName) {
  359. return this.toast(`请确保上传${upgradeToolName}.zip进行更新!`);
  360. }
  361. Model.getEntries(fileInput.files[0], (entries) => {
  362. entries = entries.filter(entry => !entry.directory && /\.(html|js|css)$/.test(entry.filename));
  363. let reg = /(fh-config\.js|index\.html|content-script\.(js|css))$/;
  364. let entPart1 = entries.filter(en => reg.test(en.filename));
  365. let entPart2 = entries.filter(en => !reg.test(en.filename));
  366. entPart1.forEach((entry) => {
  367. Model.getEntryFile(entry, (fileContent) => {
  368. let fileName = entry.filename.split('/').pop();
  369. try {
  370. if (fileName === `fh-config.js`) {
  371. let json = JSON.parse(fileContent);
  372. this.addToolConfigs(json);
  373. } else if (fileName === 'index.html') {
  374. let result = this.htmlTplEncode(toolName, fileContent);
  375. this.saveContentToLocal(toolName, fileName, result.html, true);
  376. // 所有被引用的静态文件都在这里进行遍历
  377. entPart2.forEach(jcEntry => {
  378. Model.getEntryFile(jcEntry, jcContent => {
  379. Object.keys(result.jsCss).forEach(tp => {
  380. result.jsCss[tp].some(file => {
  381. if (file[0].indexOf(jcEntry.filename) > -1) {
  382. this.saveContentToLocal(toolName, file[0].replace(`../${toolName}/`, ''), jcContent);
  383. return true;
  384. }
  385. });
  386. });
  387. });
  388. });
  389. } else if (['content-script.js', 'content-script.css'].includes(fileName)) {
  390. this.saveContentToLocal(toolName, fileName, fileContent);
  391. }
  392. } catch (err) {
  393. this.toast(`${fileName} 文件发生错误:${err.message}`);
  394. }
  395. });
  396. });
  397. this.toast('工具更新成功!');
  398. });
  399. }, false);
  400. document.body.appendChild(fileInput);
  401. fileInput.click();
  402. window.setTimeout(() => fileInput.remove(), 3000);
  403. },
  404. getToolConfigs() {
  405. return Awesome.StorageMgr.get(DEV_TOOLS_MY_TOOLS).then(data => {
  406. this.myTools = JSON.parse(data || localStorage.getItem(DEV_TOOLS_MY_TOOLS) || '{}');
  407. Object.keys(this.myTools).forEach(t => {
  408. if(this.myTools[t].menuConfig) {
  409. this.myTools.icon = this.myTools[t].menuConfig[0].icon;
  410. delete this.myTools[t].menuConfig;
  411. }
  412. if(this.myTools[t].contentScript) {
  413. this.myTools[t].contentScriptJs = this.myTools[t].contentScript;
  414. delete this.myTools[t].contentScript;
  415. }
  416. if(!this.myTools[t].icon) {
  417. this.myTools[t].icon = '◆';
  418. }
  419. });
  420. this.setToolConfigs();
  421. });
  422. },
  423. setToolConfigs() {
  424. Awesome.StorageMgr.set(DEV_TOOLS_MY_TOOLS,JSON.stringify(this.myTools));
  425. },
  426. addToolConfigs(configs) {
  427. this.getToolConfigs().then(() => {
  428. Object.keys(configs).forEach(key => {
  429. let config = configs[key];
  430. this.myTools[key] = {
  431. _devTool: true,
  432. _enable: this.myTools[key] && this.myTools[key]._enable,
  433. name: config.name,
  434. tips: config.tips,
  435. icon: config.icon,
  436. noPage: !!config.noPage,
  437. contentScriptJs: !!config.contentScriptJs || !!config.contentScript,
  438. contentScriptCss: !!config.contentScriptCss,
  439. updateUrl: config.updateUrl || null
  440. }
  441. });
  442. this.setToolConfigs();
  443. });
  444. },
  445. delToolConfigs(tools) {
  446. // 先删除文件
  447. [].concat(tools).forEach(tool => {
  448. this.getToolFilesFromLocal(tool).then(files => {
  449. Awesome.StorageMgr.remove(files.map(file =>
  450. file.startsWith(`../${tool}`) ? file : `../${tool}/${file}` ));
  451. });
  452. // 删模板等
  453. let removeItems = [
  454. TOOL_NAME_TPL.replace('#TOOL-NAME#', tool),
  455. TOOL_CONTENT_SCRIPT_TPL.replace('#TOOL-NAME#', tool),
  456. TOOL_CONTENT_SCRIPT_CSS_TPL.replace('#TOOL-NAME#', tool)
  457. ];
  458. Awesome.StorageMgr.remove(removeItems);
  459. });
  460. // 再删配置
  461. [].concat(tools).forEach(tool => {
  462. delete this.myTools[tool];
  463. });
  464. this.setToolConfigs();
  465. this.$forceUpdate();
  466. this.toast('工具删除成功!');
  467. },
  468. toggleToolEnableStatus(tool) {
  469. this.myTools[tool]._enable = !this.myTools[tool]._enable;
  470. this.setToolConfigs();
  471. this.$forceUpdate();
  472. },
  473. getToolFilesFromLocal(toolName) {
  474. return new Promise(resolve => {
  475. let files = ['fh-config.js', 'index.html'];
  476. let toolObj = this.myTools[toolName];
  477. toolObj.contentScriptJs && files.push('content-script.js');
  478. toolObj.contentScriptCss && files.push('content-script.css');
  479. chrome.storage.local.get(null, allDatas => {
  480. let fs = Object.keys(allDatas).filter(key => String(key).startsWith(`../${toolName}/`));
  481. files = files.concat(fs);
  482. resolve(files.map(f => f.replace(`../${toolName}/`, '')));
  483. });
  484. });
  485. },
  486. saveContentToLocal(toolName, fileName, content, htmlDone) {
  487. if (fileName === 'fh-config.js') {
  488. try {
  489. let json = JSON.parse(content);
  490. this.addToolConfigs(json);
  491. this.$forceUpdate();
  492. return json[toolName];
  493. } catch (e) {
  494. return null;
  495. }
  496. }
  497. let key = '';
  498. switch (fileName) {
  499. case 'index.html':
  500. key = TOOL_NAME_TPL.replace('#TOOL-NAME#', toolName);
  501. if (!htmlDone) {
  502. let result = this.htmlTplEncode(toolName, content);
  503. content = result.html;
  504. }
  505. break;
  506. case 'content-script.js':
  507. key = TOOL_CONTENT_SCRIPT_TPL.replace('#TOOL-NAME#', toolName);
  508. break;
  509. case 'content-script.css':
  510. key = TOOL_CONTENT_SCRIPT_CSS_TPL.replace('#TOOL-NAME#', toolName);
  511. break;
  512. default:
  513. key = fileName.startsWith(`../${toolName}/`) ? fileName : `../${toolName}/${fileName}`;
  514. }
  515. Awesome.StorageMgr.set(key,content);
  516. },
  517. getContentFromLocal(toolName, fileName) {
  518. return new Promise((resolve, reject) => {
  519. if (fileName === 'fh-config.js') {
  520. let counter = 0;
  521. let config = {};
  522. config[toolName] = this.myTools[toolName];
  523. ['_devTool', '_enable'].forEach(k => delete config[toolName][k]);
  524. delete config[toolName].menuConfig;
  525. let jsonText = JSON.stringify(config,null,4);
  526. resolve(jsonText);
  527. } else {
  528. let key = '';
  529. switch (fileName) {
  530. case 'index.html':
  531. key = TOOL_NAME_TPL.replace('#TOOL-NAME#', toolName);
  532. break;
  533. case 'content-script.js':
  534. key = TOOL_CONTENT_SCRIPT_TPL.replace('#TOOL-NAME#', toolName);
  535. break;
  536. case 'content-script.css':
  537. key = TOOL_CONTENT_SCRIPT_CSS_TPL.replace('#TOOL-NAME#', toolName);
  538. break;
  539. default:
  540. key = fileName.startsWith(`../${toolName}/`) ? fileName : `../${toolName}/${fileName}`;
  541. }
  542. // 获取到的数据需要做二次加工
  543. let _update = (content) => {
  544. content = content || '';
  545. if (fileName === 'index.html') {
  546. content = this.htmlTplDecode(toolName, content);
  547. }
  548. // 如果noPage为true,但content-script.js中还没有window.xxxNoPage定义的话,就自动加一个
  549. else if (fileName === 'content-script.js' && this.myTools[toolName].noPage) {
  550. if (content.indexOf(`window.${toolName.replace(/[\-_]/g, '')}NoPage`) === -1) {
  551. content += '\n\n' + FileTpl['noPage.js'].replace(/#toolName#/gm, toolName)
  552. .replace(/#toolNameLower#/gm, toolName.replace(/[\-_]/g, ''));
  553. this.saveContentToLocal(toolName, fileName, content);
  554. }
  555. }
  556. return content
  557. };
  558. Awesome.StorageMgr.get(key).then(data => {
  559. resolve(_update(data));
  560. });
  561. }
  562. });
  563. },
  564. htmlTplEncode(toolName, html, updateUrl) {
  565. let jsReg = /<script[^>]*src=['"]([^'"]+)['"][^>]*>\s*?<\/[^>]*script>/igm;
  566. let csReg = /<link\s+[^>]*[^>]*href=['"]([^'"]+)['"][^>]*[^>]*>/igm;
  567. let scriptBlockReg = /<script(?:[^>]+|(?!src=))*>([^<]+|<(?!\/script>))+<\/script>/gim;
  568. let files = {};
  569. [csReg, jsReg].forEach(reg => {
  570. html = html.replace(reg, (tag, src) => {
  571. let tagName = /<script/.test(tag) ? 'js' : 'css';
  572. if (tagName === 'css' && !/stylesheet/i.test(tag)) {
  573. return tag;
  574. }
  575. // 这里必须保留带有md5戳的原地址,要不然会有cdn缓存,更新会失败
  576. let originSrc = src;
  577. // 这个src是携带了Query的,用于直接存储到html中
  578. let withQuerySrc = src;
  579. // src 去query处理,用于Storage存储,避免过多冗余key出现
  580. if (src.indexOf('?') !== -1) {
  581. let x = src.split('?');
  582. x.pop();
  583. src = x.join('');
  584. }
  585. if (!/^\./.test(src)) {
  586. src = `../${toolName}/${src}`;
  587. withQuerySrc = `../${toolName}/${withQuerySrc}`;
  588. }
  589. // 存储静态文件的内容
  590. let filePath = originSrc;
  591. if (!/^(http(s)?:)?\/\//.test(originSrc) && updateUrl) {
  592. filePath = new URL(originSrc, updateUrl).href;
  593. }
  594. files[tagName] = files[tagName] || [];
  595. files[tagName].push([src, withQuerySrc, filePath]);
  596. return '';
  597. });
  598. });
  599. // 识别所有无src属性的script标签
  600. let blockCodes = [];
  601. html = html.replace(scriptBlockReg, (tag, codes) => {
  602. codes = codes.trim();
  603. codes.length && blockCodes.push(codes);
  604. return '';
  605. });
  606. if (blockCodes.length) {
  607. let blockName = `../${toolName}/fh-script-block.js`;
  608. files.js = files.js || [];
  609. files.js.push([blockName, blockName, blockCodes.join(';\n\n')]);
  610. }
  611. // 如果是updateURL模式,需要替换所有相对链接为绝对链接,包括:img、a
  612. if (updateUrl) {
  613. let relativePathRegexp = /^(http:\/\/|https:\/\/|\/\/)[^\s'"]+/igm;
  614. let imgReg = /<img[^>]*src=['"]([^'"]+)['"][^>]*>(\s*?<\/[^>]*img>)?/igm;
  615. let aReg = /<a[^>]*href=['"]([^'"]+)['"][^>]*>(\s*?<\/[^>]*a>)?/igm;
  616. html = html.replace(imgReg, (tag, link) => {
  617. if (!relativePathRegexp.test(link)) {
  618. tag = tag.replace(/src=['"]([^'"]+)['"]/igm, () => ` src="${new URL(link, updateUrl).href}"`);
  619. }
  620. return tag;
  621. }).replace(aReg, (tag, link) => {
  622. if (!relativePathRegexp.test(link)) {
  623. tag = tag.replace(/href=['"]([^'"]+)['"]/igm, () => ` href="${new URL(link, updateUrl).href}"`);
  624. }
  625. return tag;
  626. });
  627. }
  628. html = html.replace('static/img/favicon.ico', 'static/img/fe-16.png') // 替换favicon
  629. .replace(/<\/body>/, () => { // 将静态文件添加到页面最底部
  630. return Object.keys(files).map(t => {
  631. return `<dynamic data-type="${t}" data-source="${files[t].map(f => f[1]).join(',')}"></dynamic>`;
  632. }).join('') + '</body>';
  633. });
  634. return {
  635. html: html,
  636. jsCss: files
  637. };
  638. },
  639. htmlTplDecode(toolName, html) {
  640. let reg = /<dynamic\s+data\-type="(js|css)"\s+data\-source=['"]([^'"]+)['"][^>]*>\s*?<\/[^>]*dynamic>/igm;
  641. return html.replace(reg, (frag, tag, list) => {
  642. list = list.split(',');
  643. if (tag === 'js') {
  644. return list.map(src => `<script src="${src.replace(`../${toolName}/`, '')}"></script>`).join('');
  645. } else {
  646. return list.map(href => `<link rel="stylesheet" type="text/css" href="${href.replace(`../${toolName}/`, '')}" />`).join('');
  647. }
  648. });
  649. },
  650. givenIcons(forceClose) {
  651. if (!this.givenIconList.length) {
  652. this.givenIconList = FileTpl['given-icons'].replace(/\s/gm, '').split('');
  653. }
  654. if (forceClose) {
  655. this.showGivenIcons = false;
  656. } else {
  657. this.showGivenIcons = !this.showGivenIcons;
  658. }
  659. this.$forceUpdate();
  660. },
  661. selectIcon(icon) {
  662. if (this.showNewToolForm) {
  663. this.$refs.toolIcon.value = icon;
  664. this.givenIcons(true);
  665. } else {
  666. this.copyToClipboard(icon);
  667. this.toast(`图标 ${icon} 复制成功,随处粘贴可用!`);
  668. }
  669. },
  670. toast(content) {
  671. window.clearTimeout(window.feHelperAlertMsgTid);
  672. let elAlertMsg = document.querySelector("#fehelper_alertmsg");
  673. if (!elAlertMsg) {
  674. let elWrapper = document.createElement('div');
  675. elWrapper.innerHTML = '<div id="fehelper_alertmsg">' + content + '</div>';
  676. elAlertMsg = elWrapper.childNodes[0];
  677. document.body.appendChild(elAlertMsg);
  678. } else {
  679. elAlertMsg.innerHTML = content;
  680. elAlertMsg.style.display = 'block';
  681. }
  682. window.feHelperAlertMsgTid = window.setTimeout(function () {
  683. elAlertMsg.style.display = 'none';
  684. }, 3000);
  685. },
  686. copyToClipboard(text) {
  687. let input = document.createElement('textarea');
  688. input.style.position = 'fixed';
  689. input.style.opacity = 0;
  690. input.value = text;
  691. document.body.appendChild(input);
  692. input.select();
  693. document.execCommand('Copy');
  694. document.body.removeChild(input);
  695. },
  696. fhDeveloperDoc() {
  697. window.open(`https://github.com/zxlie/FeHelper/blob/master/README_NEW.md#%E5%85%ADopen-api`);
  698. }
  699. }
  700. });