awesome.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. /**
  2. * 工具更新
  3. * @type {{download}}
  4. */
  5. import toolMap from './tools.js';
  6. let Awesome = (() => {
  7. let manifest = chrome.runtime ? chrome.runtime.getManifest() : {};
  8. const SERVER_SITE = manifest.homepage_url;
  9. const URL_TOOL_TPL = `${SERVER_SITE}/#TOOL-NAME#/index.html`;
  10. const TOOL_NAME_TPL = 'DYNAMIC_TOOL:#TOOL-NAME#';
  11. const TOOL_CONTENT_SCRIPT_TPL = 'DYNAMIC_TOOL:CS:#TOOL-NAME#';
  12. const TOOL_CONTENT_SCRIPT_CSS_TPL = 'DYNAMIC_TOOL:CS:CSS:#TOOL-NAME#';
  13. const TOOL_MENU_TPL = 'DYNAMIC_MENU:#TOOL-NAME#';
  14. /**
  15. * 管理本地存储
  16. */
  17. let StorageMgr = (() => {
  18. // 获取chrome.storage.local中的内容,返回Promise,可直接await
  19. let get = keyArr => {
  20. return new Promise((resolve, reject) => {
  21. chrome.storage.local.get(keyArr, result => {
  22. resolve(typeof keyArr === 'string' ? result[keyArr] : result);
  23. });
  24. });
  25. };
  26. let set = (items, values) => {
  27. return new Promise((resolve, reject) => {
  28. if (typeof items === 'string') {
  29. let tmp = {};
  30. tmp[items] = values;
  31. items = tmp;
  32. }
  33. chrome.storage.local.set(items, () => {
  34. resolve();
  35. });
  36. });
  37. };
  38. let remove = keyArr => {
  39. return new Promise((resolve, reject) => {
  40. keyArr = [].concat(keyArr);
  41. chrome.storage.local.remove(keyArr, () => {
  42. resolve();
  43. });
  44. });
  45. };
  46. return {get, set, remove};
  47. })();
  48. /**
  49. * 检测工具是否已被成功安装
  50. * @param toolName 工具名称
  51. * @param detectMenu 是否进一步检测Menu的设置情况
  52. * @returns {Promise}
  53. */
  54. let detectInstall = (toolName, detectMenu) => {
  55. let menuKey = TOOL_MENU_TPL.replace('#TOOL-NAME#', toolName);
  56. let toolKey = TOOL_NAME_TPL.replace('#TOOL-NAME#', toolName);
  57. return Promise.all([StorageMgr.get(toolKey), StorageMgr.get(menuKey)]).then(values => {
  58. let toolInstalled = !!values[0];
  59. // 系统预置的功能,是强制 installed 状态的
  60. if(toolMap[toolName] && toolMap[toolName].systemInstalled) {
  61. toolInstalled = true;
  62. }
  63. if (detectMenu) {
  64. return toolInstalled && String(values[1]) === '1';
  65. }
  66. return toolInstalled;
  67. });
  68. };
  69. let log = (txt) => {
  70. // console.log(String(new Date(new Date() * 1 - (new Date().getTimezoneOffset()) * 60 * 1000).toJSON()).replace(/T/i, ' ').replace(/Z/i, '') + '>', txt);
  71. };
  72. /**
  73. * 安装/更新工具,支持显示安装进度
  74. * @param toolName
  75. * @param fnProgress
  76. * @returns {Promise<any>}
  77. */
  78. let install = (toolName, fnProgress) => {
  79. return new Promise((resolve, reject) => {
  80. // 存储html文件
  81. StorageMgr.set(TOOL_NAME_TPL.replace('#TOOL-NAME#', toolName), new Date().getTime());
  82. log(toolName + '工具html模板安装/更新成功!');
  83. resolve();
  84. });
  85. };
  86. let offLoad = (toolName) => {
  87. let items = [];
  88. items.push(TOOL_NAME_TPL.replace('#TOOL-NAME#', toolName));
  89. items.push(TOOL_CONTENT_SCRIPT_TPL.replace('#TOOL-NAME#', toolName));
  90. items.push(TOOL_CONTENT_SCRIPT_CSS_TPL.replace('#TOOL-NAME#', toolName));
  91. // 删除所有静态文件
  92. chrome.storage.local.get(null, allDatas => {
  93. if (allDatas) {
  94. StorageMgr.remove(Object.keys(allDatas).filter(key => String(key).startsWith(`../${toolName}/`)));
  95. }
  96. });
  97. log(toolName + ' 卸载成功!');
  98. return StorageMgr.remove(items);
  99. };
  100. /**
  101. * 有些工具其实已经卸载过了,但是本地还有冗余的静态文件,都需要统一清理一遍
  102. */
  103. let gcLocalFiles = () => getAllTools().then(tools => {
  104. if (!tools) return;
  105. Object.keys(tools).forEach(tool => {
  106. if (!tools[tool] || !tools[tool]._devTool && !tools[tool].installed) {
  107. offLoad(tool);
  108. }
  109. });
  110. });
  111. let getAllTools = async () => {
  112. // 获取本地开发的插件,也拼接进来
  113. try {
  114. const DEV_TOOLS_MY_TOOLS = 'DEV-TOOLS:MY-TOOLS';
  115. let _tools = await StorageMgr.get(DEV_TOOLS_MY_TOOLS);
  116. let localDevTools = JSON.parse(_tools || '{}');
  117. Object.keys(localDevTools).forEach(tool => {
  118. toolMap[tool] = localDevTools[tool];
  119. });
  120. } catch (e) {
  121. }
  122. let tools = Object.keys(toolMap);
  123. let promises = [];
  124. tools.forEach(tool => {
  125. promises = promises.concat([detectInstall(tool), detectInstall(tool, true)])
  126. });
  127. return Promise.all(promises).then(values => {
  128. (values || []).forEach((v, i) => {
  129. let tool = tools[Math.floor(i / 2)];
  130. let key = i % 2 === 0 ? 'installed' : 'menu';
  131. toolMap[tool][key] = v;
  132. // 本地工具,还需要看是否处于开启状态
  133. if (toolMap[tool].hasOwnProperty('_devTool')) {
  134. toolMap[tool][key] = toolMap[tool][key] && toolMap[tool]._enable;
  135. }
  136. });
  137. return toolMap;
  138. });
  139. };
  140. /**
  141. * 检查看本地已安装过哪些工具 - 性能优化版本
  142. * @returns {Promise}
  143. */
  144. let getInstalledTools = async () => {
  145. try {
  146. // 一次性获取所有存储数据,避免多次访问
  147. const allStorageData = await new Promise((resolve, reject) => {
  148. chrome.storage.local.get(null, result => {
  149. resolve(result || {});
  150. });
  151. });
  152. // 获取本地开发的插件
  153. const DEV_TOOLS_MY_TOOLS = 'DEV-TOOLS:MY-TOOLS';
  154. let localDevTools = {};
  155. try {
  156. localDevTools = JSON.parse(allStorageData[DEV_TOOLS_MY_TOOLS] || '{}');
  157. Object.keys(localDevTools).forEach(tool => {
  158. toolMap[tool] = localDevTools[tool];
  159. });
  160. } catch (e) {
  161. // 忽略解析错误
  162. }
  163. let installedTools = {};
  164. // 遍历所有工具,从存储数据中检查安装状态
  165. Object.keys(toolMap).forEach(toolName => {
  166. const toolKey = TOOL_NAME_TPL.replace('#TOOL-NAME#', toolName);
  167. const menuKey = TOOL_MENU_TPL.replace('#TOOL-NAME#', toolName);
  168. // 检查工具是否已安装
  169. let toolInstalled = !!allStorageData[toolKey];
  170. // 系统预置的功能,是强制 installed 状态的
  171. if (toolMap[toolName] && toolMap[toolName].systemInstalled) {
  172. toolInstalled = true;
  173. }
  174. // 检查菜单状态
  175. let menuInstalled = String(allStorageData[menuKey]) === '1';
  176. // 本地工具,还需要看是否处于开启状态
  177. if (toolMap[toolName].hasOwnProperty('_devTool')) {
  178. toolInstalled = toolInstalled && toolMap[toolName]._enable;
  179. menuInstalled = menuInstalled && toolMap[toolName]._enable;
  180. }
  181. // 只收集已安装的工具
  182. if (toolInstalled) {
  183. installedTools[toolName] = {
  184. ...toolMap[toolName],
  185. installed: true,
  186. menu: menuInstalled,
  187. installTime: parseInt(allStorageData[toolKey]) || 0
  188. };
  189. }
  190. });
  191. // 按安装时间排序
  192. const sortedToolNames = Object.keys(installedTools).sort((a, b) => {
  193. return installedTools[a].installTime - installedTools[b].installTime;
  194. });
  195. let sortedToolMap = {};
  196. sortedToolNames.forEach(toolName => {
  197. sortedToolMap[toolName] = installedTools[toolName];
  198. });
  199. return sortedToolMap;
  200. } catch (error) {
  201. console.error('getInstalledTools error:', error);
  202. // 发生错误时返回空对象,避免popup完全无法加载
  203. return {};
  204. }
  205. };
  206. /**
  207. * 获取工具的content-script
  208. * @param toolName
  209. * @param cssMode
  210. */
  211. let getContentScript = (toolName, cssMode) => {
  212. return StorageMgr.get(cssMode ? TOOL_CONTENT_SCRIPT_CSS_TPL.replace('#TOOL-NAME#', toolName)
  213. : TOOL_CONTENT_SCRIPT_TPL.replace('#TOOL-NAME#', toolName));
  214. };
  215. /**
  216. * 获取工具的html模板
  217. * @param toolName
  218. * @returns {*}
  219. */
  220. let getToolTpl = (toolName) => StorageMgr.get(TOOL_NAME_TPL.replace('#TOOL-NAME#', toolName));
  221. /**
  222. * 从服务器检查,看本地已安装的工具,有哪些又已经升级过了
  223. * @param tool
  224. */
  225. let checkUpgrade = (tool) => {
  226. let getOnline = (toolName) => fetch(URL_TOOL_TPL.replace('#TOOL-NAME#', toolName)).then(resp => resp.text());
  227. let getOffline = (toolName) => StorageMgr.get(TOOL_NAME_TPL.replace('#TOOL-NAME#', toolName));
  228. return Promise.all([getOnline(tool), getOffline(tool)]).then(values => {
  229. let onlineData = _tplHandler(tool, values[0]);
  230. let local = values[1];
  231. return local !== onlineData.html;
  232. });
  233. };
  234. /**
  235. * 管理右键菜单
  236. * @param toolName
  237. * @param action 具体动作install/offload/get
  238. * @returns {Promise<any>}
  239. */
  240. let menuMgr = (toolName, action) => {
  241. let menuKey = TOOL_MENU_TPL.replace('#TOOL-NAME#', toolName);
  242. switch (action) {
  243. case 'get':
  244. return StorageMgr.get(menuKey);
  245. case 'offload':
  246. // 必须用setItem模式,而不是removeItem,要处理 0/1/null三种结果
  247. log(toolName + ' 卸载成功!');
  248. return StorageMgr.set(menuKey, 0);
  249. case 'install':
  250. log(toolName + ' 安装成功!');
  251. return StorageMgr.set(menuKey, 1);
  252. }
  253. };
  254. /**
  255. * 采集客户端信息并发送给background
  256. */
  257. let collectAndSendClientInfo = () => {
  258. try {
  259. const nav = navigator;
  260. const screenInfo = window.screen;
  261. const lang = nav.language || nav.userLanguage || '';
  262. const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || '';
  263. const ua = nav.userAgent;
  264. const platform = nav.platform;
  265. const vendor = nav.vendor;
  266. const colorDepth = screenInfo.colorDepth;
  267. const screenWidth = screenInfo.width;
  268. const screenHeight = screenInfo.height;
  269. const deviceMemory = nav.deviceMemory || '';
  270. const hardwareConcurrency = nav.hardwareConcurrency || '';
  271. const connection = nav.connection || nav.mozConnection || nav.webkitConnection || {};
  272. const screenOrientation = screenInfo.orientation ? screenInfo.orientation.type : '';
  273. const touchSupport = ('ontouchstart' in window) || (nav.maxTouchPoints > 0);
  274. let memoryJSHeapSize = '';
  275. if (window.performance && window.performance.memory) {
  276. memoryJSHeapSize = window.performance.memory.jsHeapSizeLimit;
  277. }
  278. const clientInfo = {
  279. language: lang,
  280. timezone,
  281. userAgent: ua,
  282. platform,
  283. vendor,
  284. colorDepth,
  285. screenWidth,
  286. screenHeight,
  287. deviceMemory,
  288. hardwareConcurrency,
  289. networkType: connection.effectiveType || '',
  290. downlink: connection.downlink || '',
  291. rtt: connection.rtt || '',
  292. online: nav.onLine,
  293. touchSupport,
  294. cookieEnabled: nav.cookieEnabled,
  295. doNotTrack: nav.doNotTrack,
  296. appVersion: nav.appVersion,
  297. appName: nav.appName,
  298. product: nav.product,
  299. vendorSub: nav.vendorSub,
  300. screenOrientation,
  301. memoryJSHeapSize
  302. };
  303. chrome.runtime.sendMessage({ type: 'clientInfo', data: clientInfo });
  304. } catch (e) {
  305. // 忽略采集异常
  306. }
  307. };
  308. return {
  309. StorageMgr,
  310. detectInstall,
  311. install,
  312. offLoad,
  313. getInstalledTools,
  314. menuMgr,
  315. checkUpgrade,
  316. getContentScript,
  317. getToolTpl,
  318. gcLocalFiles,
  319. getAllTools,
  320. collectAndSendClientInfo
  321. }
  322. })();
  323. export default Awesome;