ui.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. /*
  2. * Copyright 2018 Gildas Lormeau
  3. * contact : gildas.lormeau <at> gmail.com
  4. *
  5. * This file is part of SingleFile.
  6. *
  7. * SingleFile is free software: you can redistribute it and/or modify
  8. * it under the terms of the GNU Lesser General Public License as published by
  9. * the Free Software Foundation, either version 3 of the License, or
  10. * (at your option) any later version.
  11. *
  12. * SingleFile is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU Lesser General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU Lesser General Public License
  18. * along with SingleFile. If not, see <http://www.gnu.org/licenses/>.
  19. */
  20. /* global browser, singlefile */
  21. singlefile.ui = (() => {
  22. const DEFAULT_ICON_PATH = "/extension/ui/resources/icon_16.png";
  23. const WAIT_ICON_PATH_PREFIX = "/extension/ui/resources/icon_16_wait";
  24. const DEFAULT_TITLE = "Save page with SingleFile";
  25. const DEFAULT_COLOR = [2, 147, 20, 255];
  26. const BADGE_PROPERTIES = [{ name: "text", browserActionMethod: "setBadgeText" }, { name: "color", browserActionMethod: "setBadgeBackgroundColor" }, { name: "title", browserActionMethod: "setTitle" }, { name: "path", browserActionMethod: "setIcon" }];
  27. const FORBIDDEN_URLS = ["https://chrome.google.com", "https://addons.mozilla.org"];
  28. const MENU_ID_SAVE_PAGE = "save-page";
  29. const MENU_ID_SAVE_SELECTED = "save-selected";
  30. const MENU_ID_SAVE_FRAME = "save-frame";
  31. const MENU_ID_SAVE_SELECTED_TABS = "save-selected-tabs";
  32. const MENU_ID_SAVE_UNPINNED_TABS = "save-unpinned-tabs";
  33. const MENU_ID_SAVE_ALL_TABS = "save-tabs";
  34. const MENU_ID_AUTO_SAVE_DISABLED = "auto-save-disabled";
  35. const MENU_ID_AUTO_SAVE_TAB = "auto-save-tab";
  36. const MENU_ID_AUTO_SAVE_UNPINNED = "auto-save-unpinned";
  37. const MENU_ID_AUTO_SAVE_ALL = "auto-save-all";
  38. let tabsData;
  39. browser.runtime.onInstalled.addListener(refreshContextMenu);
  40. if (browser.menus && browser.menus.onClicked) {
  41. browser.menus.onClicked.addListener(async (event, tab) => {
  42. if (event.menuItemId == MENU_ID_SAVE_PAGE) {
  43. processTab(tab);
  44. }
  45. if (event.menuItemId == MENU_ID_SAVE_SELECTED) {
  46. processTab(tab, { selected: true });
  47. }
  48. if (event.menuItemId == MENU_ID_SAVE_FRAME) {
  49. processTab(tab, { frameId: event.frameId });
  50. }
  51. if (event.menuItemId == MENU_ID_SAVE_SELECTED_TABS) {
  52. const tabs = await browser.tabs.query({ currentWindow: true, highlighted: true });
  53. tabs.forEach(tab => isAllowedURL(tab.url) && processTab(tab));
  54. }
  55. if (event.menuItemId == MENU_ID_SAVE_UNPINNED_TABS) {
  56. const tabs = await browser.tabs.query({ currentWindow: true, pinned: false });
  57. tabs.forEach(tab => isAllowedURL(tab.url) && processTab(tab));
  58. }
  59. if (event.menuItemId == MENU_ID_SAVE_ALL_TABS) {
  60. const tabs = await browser.tabs.query({ currentWindow: true });
  61. tabs.forEach(tab => isAllowedURL(tab.url) && processTab(tab));
  62. }
  63. if (event.menuItemId == MENU_ID_AUTO_SAVE_TAB) {
  64. const tabsData = await getTabsData();
  65. if (!tabsData[tab.id]) {
  66. tabsData[tab.id] = {};
  67. }
  68. tabsData[tab.id].autoSave = event.checked;
  69. await browser.storage.local.set({ tabsData });
  70. }
  71. if (event.menuItemId == MENU_ID_AUTO_SAVE_DISABLED) {
  72. const tabsData = await getTabsData();
  73. Object.keys(tabsData).forEach(tabId => tabsData[tabId].autoSave = false);
  74. tabsData.autoSaveUnpinned = tabsData.autoSaveAll = false;
  75. await browser.storage.local.set({ tabsData });
  76. }
  77. if (event.menuItemId == MENU_ID_AUTO_SAVE_ALL) {
  78. const tabsData = await getTabsData();
  79. tabsData.autoSaveAll = event.checked;
  80. await browser.storage.local.set({ tabsData });
  81. }
  82. if (event.menuItemId == MENU_ID_AUTO_SAVE_UNPINNED) {
  83. const tabsData = await getTabsData();
  84. tabsData.autoSaveUnpinned = event.checked;
  85. await browser.storage.local.set({ tabsData });
  86. }
  87. });
  88. }
  89. browser.browserAction.onClicked.addListener(async tab => {
  90. if (isAllowedURL(tab.url)) {
  91. const tabs = await browser.tabs.query({ currentWindow: true, highlighted: true });
  92. if (!tabs.length) {
  93. processTab(tab);
  94. } else {
  95. tabs.forEach(tab => isAllowedURL(tab.url) && processTab(tab));
  96. }
  97. }
  98. });
  99. browser.tabs.onActivated.addListener(async activeInfo => {
  100. const tab = await browser.tabs.get(activeInfo.tabId);
  101. await refreshContextMenuState(tab);
  102. onTabActivated(tab);
  103. });
  104. browser.tabs.onCreated.addListener(async tab => {
  105. await refreshContextMenuState(tab);
  106. onTabActivated(tab);
  107. });
  108. browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
  109. const tabsData = await getTabsData();
  110. if (tabsData[tab.id] && tabsData[tab.id].autoSave || tabsData.autoSaveAll || (tabsData.autoSaveUnpinned && !tab.pinned)) {
  111. if (changeInfo.status == "complete") {
  112. processTab(tab, { autoSave: true });
  113. }
  114. }
  115. onTabActivated(tab);
  116. });
  117. browser.tabs.onRemoved.addListener(tabId => onTabRemoved(tabId));
  118. browser.runtime.onMessage.addListener((request, sender) => {
  119. if (request.processProgress && request.maxIndex) {
  120. onTabProgress(sender.tab.id, request.index, request.maxIndex);
  121. }
  122. if (request.processEnd) {
  123. onTabEnd(sender.tab.id);
  124. }
  125. if (request.processError) {
  126. if (request.error) {
  127. console.error("Initialization error", request.error); // eslint-disable-line no-console
  128. }
  129. onTabError(sender.tab.id);
  130. }
  131. });
  132. return { update: refreshContextMenu };
  133. async function refreshContextMenu() {
  134. const config = await singlefile.config.get();
  135. if (browser.menus && browser.menus.removeAll && browser.menus.create) {
  136. if (config.contextMenuEnabled) {
  137. await browser.menus.removeAll();
  138. browser.menus.create({
  139. id: MENU_ID_SAVE_PAGE,
  140. contexts: ["page"],
  141. title: DEFAULT_TITLE
  142. });
  143. browser.menus.create({
  144. id: "separator-1",
  145. contexts: ["all"],
  146. type: "separator"
  147. });
  148. browser.menus.create({
  149. id: MENU_ID_SAVE_SELECTED,
  150. contexts: ["selection"],
  151. title: "Save selection"
  152. });
  153. browser.menus.create({
  154. id: MENU_ID_SAVE_FRAME,
  155. contexts: ["frame"],
  156. title: "Save frame"
  157. });
  158. browser.menus.create({
  159. id: MENU_ID_SAVE_SELECTED_TABS,
  160. contexts: ["page"],
  161. title: "Save selected tabs"
  162. });
  163. browser.menus.create({
  164. id: MENU_ID_SAVE_UNPINNED_TABS,
  165. contexts: ["page"],
  166. title: "Save unpinned tabs"
  167. });
  168. browser.menus.create({
  169. id: MENU_ID_SAVE_ALL_TABS,
  170. contexts: ["page"],
  171. title: "Save all tabs"
  172. });
  173. browser.menus.create({
  174. id: "separator-2",
  175. contexts: ["all"],
  176. type: "separator"
  177. });
  178. browser.menus.create({
  179. id: MENU_ID_AUTO_SAVE_DISABLED,
  180. type: "radio",
  181. title: "Disable Auto-save",
  182. contexts: ["all"],
  183. checked: true
  184. });
  185. browser.menus.create({
  186. id: MENU_ID_AUTO_SAVE_TAB,
  187. type: "radio",
  188. title: "Auto-save this tab",
  189. contexts: ["all"],
  190. checked: false
  191. });
  192. browser.menus.create({
  193. id: MENU_ID_AUTO_SAVE_UNPINNED,
  194. type: "radio",
  195. title: "Auto-save unpinned tabs",
  196. contexts: ["all"],
  197. checked: false
  198. });
  199. browser.menus.create({
  200. id: MENU_ID_AUTO_SAVE_ALL,
  201. type: "radio",
  202. title: "Auto-save all tabs",
  203. contexts: ["all"],
  204. checked: false
  205. });
  206. } else {
  207. await browser.menus.removeAll();
  208. }
  209. }
  210. }
  211. async function refreshContextMenuState(tab) {
  212. const tabsData = await getTabsData();
  213. if (browser.menus && browser.menus.update) {
  214. await browser.menus.update(MENU_ID_AUTO_SAVE_DISABLED, { checked: !tabsData[tab.id] || !tabsData[tab.id].autoSave });
  215. await browser.menus.update(MENU_ID_AUTO_SAVE_TAB, { checked: tabsData[tab.id] && tabsData[tab.id].autoSave });
  216. await browser.menus.update(MENU_ID_AUTO_SAVE_UNPINNED, { checked: tabsData.autoSaveUnpinned });
  217. await browser.menus.update(MENU_ID_AUTO_SAVE_ALL, { checked: tabsData.autoSaveAll });
  218. }
  219. }
  220. async function processTab(tab, options) {
  221. const tabId = tab.id;
  222. try {
  223. refreshBadge(tabId, {
  224. id: tabId,
  225. text: "...",
  226. color: DEFAULT_COLOR,
  227. title: "Initializing SingleFile (1/2)",
  228. path: DEFAULT_ICON_PATH,
  229. progress: -1,
  230. barProgress: -1
  231. });
  232. await singlefile.core.processTab(tab, options);
  233. refreshBadge(tabId, {
  234. id: tabId,
  235. text: "...",
  236. color: [4, 229, 36, 255],
  237. title: "Initializing SingleFile (2/2)",
  238. path: DEFAULT_ICON_PATH,
  239. progress: -1,
  240. barProgress: -1
  241. });
  242. } catch (error) {
  243. if (error) {
  244. onTabError(tabId);
  245. } else {
  246. refreshBadge(tabId, {
  247. id: tabId,
  248. text: "↻",
  249. color: [255, 141, 1, 255],
  250. title: "Reload the page",
  251. path: DEFAULT_ICON_PATH,
  252. progress: -1,
  253. barProgress: -1
  254. });
  255. }
  256. }
  257. }
  258. function onTabError(tabId) {
  259. refreshBadge(tabId, {
  260. text: "ERR",
  261. color: [229, 4, 12, 255],
  262. title: DEFAULT_TITLE,
  263. path: DEFAULT_ICON_PATH,
  264. progress: -1,
  265. barProgress: -1
  266. });
  267. }
  268. async function onTabEnd(tabId) {
  269. const tabsData = await getTabsData();
  270. refreshBadge(tabId, {
  271. text: "OK",
  272. color: tabsData.autoSaveAll || tabsData.autoSaveUnpinned || tabsData[tabId].autoSave ? [255, 141, 1, 255] : [4, 229, 36, 255],
  273. title: DEFAULT_TITLE,
  274. path: DEFAULT_ICON_PATH,
  275. progress: -1,
  276. barProgress: -1
  277. });
  278. }
  279. function onTabProgress(tabId, index, maxIndex) {
  280. const progress = Math.max(Math.min(20, Math.floor((index / maxIndex) * 20)), 0);
  281. const barProgress = Math.floor((index / maxIndex) * 8);
  282. refreshBadge(tabId, {
  283. progress,
  284. text: "",
  285. title: "Save progress: " + (progress * 5) + "%",
  286. color: [4, 229, 36, 255],
  287. barProgress,
  288. path: WAIT_ICON_PATH_PREFIX + barProgress + ".png"
  289. });
  290. }
  291. async function onTabRemoved() {
  292. const tabsData = await getTabsData();
  293. await cleanupTabsData();
  294. await browser.storage.local.set({ tabsData });
  295. }
  296. function onTabActivated(tab) {
  297. if (isAllowedURL(tab.url) && browser.browserAction && browser.browserAction.enable && browser.browserAction.disable) {
  298. if (isAllowedURL(tab.url)) {
  299. browser.browserAction.enable(tab.id);
  300. if (browser.runtime.lastError) {
  301. /* ignored */
  302. }
  303. } else {
  304. browser.browserAction.disable(tab.id);
  305. if (browser.runtime.lastError) {
  306. /* ignored */
  307. }
  308. }
  309. }
  310. }
  311. function isAllowedURL(url) {
  312. return url && (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("file://")) && !FORBIDDEN_URLS.find(storeUrl => url.startsWith(storeUrl));
  313. }
  314. async function refreshBadge(tabId, tabData) {
  315. const tabsData = await getTabsData();
  316. if (!tabsData[tabId]) {
  317. tabsData[tabId] = {};
  318. }
  319. if (!tabsData[tabId].pendingRefresh) {
  320. tabsData[tabId].pendingRefresh = Promise.resolve();
  321. }
  322. tabsData[tabId].pendingRefresh = tabsData[tabId].pendingRefresh.then(() => refreshBadgeAsync(tabId, tabData));
  323. await tabsData[tabId].pendingRefresh;
  324. }
  325. async function refreshBadgeAsync(tabId, tabData, lastTabData) {
  326. for (let property of BADGE_PROPERTIES) {
  327. await refreshBadgeProperty(tabId, property.name, property.browserActionMethod, tabData, lastTabData);
  328. }
  329. }
  330. async function refreshBadgeProperty(tabId, property, browserActionMethod, tabData) {
  331. const value = tabData[property];
  332. const browserActionParameter = { tabId };
  333. if (browser.browserAction && browser.browserAction[browserActionMethod]) {
  334. browserActionParameter[property] = value;
  335. if (!tabsData[tabId]) {
  336. tabsData[tabId] = {};
  337. }
  338. if (!tabsData[tabId].badge) {
  339. tabsData[tabId].badge = {};
  340. }
  341. if (JSON.stringify(tabsData[tabId].badge[browserActionMethod]) != JSON.stringify(value)) {
  342. tabsData[tabId].badge[browserActionMethod] = value;
  343. await browser.browserAction[browserActionMethod](browserActionParameter);
  344. }
  345. }
  346. }
  347. async function getTabsData() {
  348. if (tabsData) {
  349. return tabsData;
  350. } else {
  351. const config = await browser.storage.local.get();
  352. tabsData = config.tabsData;
  353. await cleanupTabsData();
  354. return tabsData;
  355. }
  356. }
  357. async function cleanupTabsData() {
  358. if (tabsData) {
  359. const tabs = await browser.tabs.query({});
  360. Object.keys(tabsData).filter(tabId => !tabs.find(tab => tab.id == tabId)).forEach(tabId => delete tabsData[tabId]);
  361. await browser.storage.local.set({ tabsData });
  362. }
  363. }
  364. })();