ui-menu.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. /*
  2. * Copyright 2010-2019 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, URL */
  21. singlefile.ui.menu = (() => {
  22. const menus = browser.menus || browser.contextMenus;
  23. const BROWSER_MENUS_API_SUPPORTED = menus && menus.onClicked && menus.create && menus.update && menus.removeAll;
  24. const MENU_ID_SAVE_PAGE = "save-page";
  25. const MENU_ID_SELECT_PROFILE = "select-profile";
  26. const MENU_ID_SELECT_PROFILE_PREFIX = "select-profile-";
  27. const MENU_ID_ASSOCIATE_WITH_PROFILE = "associate-with-profile";
  28. const MENU_ID_ASSOCIATE_WITH_PROFILE_PREFIX = "associate-with-profile-";
  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 = "auto-save";
  35. const MENU_ID_AUTO_SAVE_DISABLED = "auto-save-disabled";
  36. const MENU_ID_AUTO_SAVE_TAB = "auto-save-tab";
  37. const MENU_ID_AUTO_SAVE_UNPINNED = "auto-save-unpinned";
  38. const MENU_ID_AUTO_SAVE_ALL = "auto-save-all";
  39. let profileIndexes = new Map();
  40. initialize();
  41. browser.tabs.onActivated.addListener(async activeInfo => {
  42. const tab = await browser.tabs.get(activeInfo.tabId);
  43. await refreshTab(tab);
  44. });
  45. browser.tabs.onCreated.addListener(refreshTab);
  46. return {
  47. refresh
  48. };
  49. async function refresh(tab) {
  50. const [profiles, tabsData] = await Promise.all([singlefile.config.getProfiles(), singlefile.tabsData.get()]);
  51. const options = await singlefile.config.getOptions(tabsData.profileName, tab && tab.url, true);
  52. if (BROWSER_MENUS_API_SUPPORTED) {
  53. const pageContextsEnabled = ["page", "frame", "image", "link", "video", "audio"];
  54. const defaultContextsDisabled = ["browser_action"];
  55. const defaultContextsEnabled = defaultContextsDisabled.concat(...pageContextsEnabled);
  56. const defaultContexts = options.contextMenuEnabled ? defaultContextsEnabled : defaultContextsDisabled;
  57. await menus.removeAll();
  58. if (options.contextMenuEnabled) {
  59. menus.create({
  60. id: MENU_ID_SAVE_PAGE,
  61. contexts: pageContextsEnabled,
  62. title: browser.i18n.getMessage("menuSavePage")
  63. });
  64. }
  65. if (options.contextMenuEnabled) {
  66. menus.create({
  67. id: "separator-1",
  68. contexts: pageContextsEnabled,
  69. type: "separator"
  70. });
  71. }
  72. menus.create({
  73. id: MENU_ID_SAVE_SELECTED,
  74. contexts: defaultContexts,
  75. title: browser.i18n.getMessage("menuSaveSelection")
  76. });
  77. if (options.contextMenuEnabled) {
  78. menus.create({
  79. id: MENU_ID_SAVE_FRAME,
  80. contexts: ["frame"],
  81. title: browser.i18n.getMessage("menuSaveFrame")
  82. });
  83. menus.create({
  84. id: MENU_ID_SAVE_SELECTED_TABS,
  85. contexts: pageContextsEnabled,
  86. title: browser.i18n.getMessage("menuSaveSelectedTabs")
  87. });
  88. }
  89. menus.create({
  90. id: MENU_ID_SAVE_UNPINNED_TABS,
  91. contexts: defaultContexts,
  92. title: browser.i18n.getMessage("menuUnpinnedTabs")
  93. });
  94. menus.create({
  95. id: MENU_ID_SAVE_ALL_TABS,
  96. contexts: defaultContexts,
  97. title: browser.i18n.getMessage("menuAllTabs")
  98. });
  99. if (options.contextMenuEnabled) {
  100. menus.create({
  101. id: "separator-2",
  102. contexts: pageContextsEnabled,
  103. type: "separator"
  104. });
  105. }
  106. if (Object.keys(profiles).length > 1) {
  107. menus.create({
  108. id: MENU_ID_SELECT_PROFILE,
  109. title: browser.i18n.getMessage("menuSelectProfile"),
  110. contexts: defaultContexts,
  111. });
  112. menus.create({
  113. id: MENU_ID_SELECT_PROFILE_PREFIX + "default",
  114. type: "radio",
  115. contexts: defaultContexts,
  116. title: browser.i18n.getMessage("profileDefaultSettings"),
  117. checked: !tabsData.profileName || tabsData.profileName == singlefile.config.DEFAULT_PROFILE_NAME,
  118. parentId: MENU_ID_SELECT_PROFILE
  119. });
  120. menus.create({
  121. id: MENU_ID_ASSOCIATE_WITH_PROFILE,
  122. title: browser.i18n.getMessage("menuCreateDomainRule"),
  123. contexts: defaultContexts,
  124. });
  125. let rule;
  126. if (tab && tab.url) {
  127. rule = await singlefile.config.getRule(tab.url);
  128. }
  129. menus.create({
  130. id: MENU_ID_ASSOCIATE_WITH_PROFILE_PREFIX + "default",
  131. type: "radio",
  132. contexts: defaultContexts,
  133. title: browser.i18n.getMessage("profileDefaultSettings"),
  134. checked: !rule || rule.profile == singlefile.config.DEFAULT_PROFILE_NAME,
  135. parentId: MENU_ID_ASSOCIATE_WITH_PROFILE
  136. });
  137. profileIndexes = new Map();
  138. Object.keys(profiles).forEach((profileName, profileIndex) => {
  139. if (profileName != singlefile.config.DEFAULT_PROFILE_NAME) {
  140. menus.create({
  141. id: MENU_ID_SELECT_PROFILE_PREFIX + profileIndex,
  142. type: "radio",
  143. contexts: defaultContexts,
  144. title: profileName,
  145. checked: options.profileName == profileName,
  146. parentId: MENU_ID_SELECT_PROFILE
  147. });
  148. menus.create({
  149. id: MENU_ID_ASSOCIATE_WITH_PROFILE_PREFIX + profileIndex,
  150. type: "radio",
  151. contexts: defaultContexts,
  152. title: profileName,
  153. checked: rule && rule.profile == profileName,
  154. parentId: MENU_ID_ASSOCIATE_WITH_PROFILE
  155. });
  156. profileIndexes.set(profileName, profileIndex);
  157. }
  158. });
  159. menus.create({
  160. id: "separator-3",
  161. contexts: defaultContexts,
  162. type: "separator"
  163. });
  164. }
  165. menus.create({
  166. id: MENU_ID_AUTO_SAVE,
  167. contexts: defaultContexts,
  168. title: browser.i18n.getMessage("menuAutoSave")
  169. });
  170. menus.create({
  171. id: MENU_ID_AUTO_SAVE_DISABLED,
  172. type: "radio",
  173. title: browser.i18n.getMessage("menuAutoSaveDisabled"),
  174. contexts: defaultContexts,
  175. checked: true,
  176. parentId: MENU_ID_AUTO_SAVE
  177. });
  178. menus.create({
  179. id: MENU_ID_AUTO_SAVE_TAB,
  180. type: "radio",
  181. title: browser.i18n.getMessage("menuAutoSaveTab"),
  182. contexts: defaultContexts,
  183. checked: false,
  184. parentId: MENU_ID_AUTO_SAVE
  185. });
  186. menus.create({
  187. id: MENU_ID_AUTO_SAVE_UNPINNED,
  188. type: "radio",
  189. title: browser.i18n.getMessage("menuAutoSaveUnpinnedTabs"),
  190. contexts: defaultContexts,
  191. checked: false,
  192. parentId: MENU_ID_AUTO_SAVE
  193. });
  194. menus.create({
  195. id: MENU_ID_AUTO_SAVE_ALL,
  196. type: "radio",
  197. title: browser.i18n.getMessage("menuAutoSaveAllTabs"),
  198. contexts: defaultContexts,
  199. checked: false,
  200. parentId: MENU_ID_AUTO_SAVE
  201. });
  202. }
  203. }
  204. async function initialize() {
  205. if (BROWSER_MENUS_API_SUPPORTED) {
  206. refresh();
  207. menus.onClicked.addListener(async (event, tab) => {
  208. if (event.menuItemId == MENU_ID_SAVE_PAGE) {
  209. singlefile.ui.saveTab(tab);
  210. }
  211. if (event.menuItemId == MENU_ID_SAVE_SELECTED) {
  212. singlefile.ui.saveTab(tab, { selected: true });
  213. }
  214. if (event.menuItemId == MENU_ID_SAVE_FRAME) {
  215. singlefile.ui.saveTab(tab, { frameId: event.frameId });
  216. }
  217. if (event.menuItemId == MENU_ID_SAVE_SELECTED_TABS) {
  218. const tabs = await browser.tabs.query({ currentWindow: true, highlighted: true });
  219. tabs.forEach(tab => singlefile.ui.isAllowedURL(tab.url) && singlefile.ui.saveTab(tab));
  220. }
  221. if (event.menuItemId == MENU_ID_SAVE_UNPINNED_TABS) {
  222. const tabs = await browser.tabs.query({ currentWindow: true, pinned: false });
  223. tabs.forEach(tab => singlefile.ui.isAllowedURL(tab.url) && singlefile.ui.saveTab(tab));
  224. }
  225. if (event.menuItemId == MENU_ID_SAVE_ALL_TABS) {
  226. const tabs = await browser.tabs.query({ currentWindow: true });
  227. tabs.forEach(tab => singlefile.ui.isAllowedURL(tab.url) && singlefile.ui.saveTab(tab));
  228. }
  229. if (event.menuItemId == MENU_ID_AUTO_SAVE_TAB) {
  230. const tabsData = await singlefile.tabsData.get();
  231. if (!tabsData[tab.id]) {
  232. tabsData[tab.id] = {};
  233. }
  234. tabsData[tab.id].autoSave = event.checked;
  235. await singlefile.tabsData.set(tabsData);
  236. refreshExternalComponents(tab.id, { autoSave: true });
  237. }
  238. if (event.menuItemId == MENU_ID_AUTO_SAVE_DISABLED) {
  239. const tabsData = await singlefile.tabsData.get();
  240. Object.keys(tabsData).forEach(tabId => tabsData[tabId].autoSave = false);
  241. tabsData.autoSaveUnpinned = tabsData.autoSaveAll = false;
  242. await singlefile.tabsData.set(tabsData);
  243. refreshExternalComponents(tab.id, { autoSave: false });
  244. }
  245. if (event.menuItemId == MENU_ID_AUTO_SAVE_ALL) {
  246. const tabsData = await singlefile.tabsData.get();
  247. tabsData.autoSaveAll = event.checked;
  248. await singlefile.tabsData.set(tabsData);
  249. refreshExternalComponents(tab.id, { autoSave: true });
  250. }
  251. if (event.menuItemId == MENU_ID_AUTO_SAVE_UNPINNED) {
  252. const tabsData = await singlefile.tabsData.get();
  253. tabsData.autoSaveUnpinned = event.checked;
  254. await singlefile.tabsData.set(tabsData);
  255. refreshExternalComponents(tab.id, { autoSave: true });
  256. }
  257. if (event.menuItemId.startsWith(MENU_ID_SELECT_PROFILE_PREFIX)) {
  258. const [profiles, tabsData] = await Promise.all([singlefile.config.getProfiles(), singlefile.tabsData.get()]);
  259. const profileId = event.menuItemId.split(MENU_ID_SELECT_PROFILE_PREFIX)[1];
  260. if (profileId == "default") {
  261. tabsData.profileName = singlefile.config.DEFAULT_PROFILE_NAME;
  262. } else {
  263. const profileIndex = Number(profileId);
  264. tabsData.profileName = Object.keys(profiles)[profileIndex];
  265. }
  266. await singlefile.tabsData.set(tabsData);
  267. refresh();
  268. refreshExternalComponents(tab.id, { autoSave: tabsData.autoSaveAll || tabsData.autoSaveUnpinned || (tabsData[tab.id] && tabsData[tab.id].autoSave) });
  269. }
  270. if (event.menuItemId.startsWith(MENU_ID_ASSOCIATE_WITH_PROFILE_PREFIX)) {
  271. const [profiles, rule] = await Promise.all([singlefile.config.getProfiles(), singlefile.config.getRule(tab.url)]);
  272. const profileId = event.menuItemId.split(MENU_ID_ASSOCIATE_WITH_PROFILE_PREFIX)[1];
  273. let profileName;
  274. if (profileId == "default") {
  275. profileName = singlefile.config.DEFAULT_PROFILE_NAME;
  276. } else {
  277. const profileIndex = Number(profileId);
  278. profileName = Object.keys(profiles)[profileIndex];
  279. }
  280. if (rule) {
  281. await singlefile.config.updateRule(rule.url, rule.url, profileName, profileName);
  282. } else {
  283. await menus.update(MENU_ID_ASSOCIATE_WITH_PROFILE, { title: browser.i18n.getMessage("menuUpdateRule") });
  284. await singlefile.config.addRule(new URL(tab.url).hostname, profileName, profileName);
  285. }
  286. }
  287. });
  288. const tabs = await browser.tabs.query({});
  289. tabs.forEach(tab => refreshTab(tab));
  290. }
  291. }
  292. async function refreshExternalComponents(tabId) {
  293. await singlefile.autosave.refresh();
  294. singlefile.ui.button.refresh(tabId);
  295. }
  296. async function refreshTab(tab) {
  297. if (BROWSER_MENUS_API_SUPPORTED) {
  298. const tabsData = await singlefile.tabsData.get();
  299. const promises = [];
  300. try {
  301. const disabled = Boolean(!tabsData[tab.id] || !tabsData[tab.id].autoSave);
  302. promises.push(menus.update(MENU_ID_AUTO_SAVE_DISABLED, { checked: disabled }));
  303. promises.push(menus.update(MENU_ID_AUTO_SAVE_TAB, { checked: !disabled }));
  304. promises.push(menus.update(MENU_ID_AUTO_SAVE_UNPINNED, { checked: Boolean(tabsData.autoSaveUnpinned) }));
  305. promises.push(menus.update(MENU_ID_AUTO_SAVE_ALL, { checked: Boolean(tabsData.autoSaveAll) }));
  306. if (tab && tab.url) {
  307. let selectedEntryId = MENU_ID_ASSOCIATE_WITH_PROFILE_PREFIX + "default";
  308. let title = browser.i18n.getMessage("menuCreateDomainRule");
  309. const [profiles, rule] = await Promise.all([singlefile.config.getProfiles(), singlefile.config.getRule(tab.url)]);
  310. if (rule) {
  311. const profileIndex = profileIndexes.get(rule.profile);
  312. if (profileIndex) {
  313. selectedEntryId = MENU_ID_ASSOCIATE_WITH_PROFILE_PREFIX + profileIndex;
  314. title = browser.i18n.getMessage("menuUpdateRule");
  315. }
  316. }
  317. Object.keys(profiles).forEach((profileName, profileIndex) => {
  318. if (profileName == singlefile.config.DEFAULT_PROFILE_NAME) {
  319. promises.push(menus.update(MENU_ID_ASSOCIATE_WITH_PROFILE_PREFIX + "default", { checked: selectedEntryId == MENU_ID_ASSOCIATE_WITH_PROFILE_PREFIX + "default" }));
  320. } else {
  321. promises.push(menus.update(MENU_ID_ASSOCIATE_WITH_PROFILE_PREFIX + profileIndex, { checked: selectedEntryId == MENU_ID_ASSOCIATE_WITH_PROFILE_PREFIX + profileIndex }));
  322. }
  323. });
  324. promises.push(menus.update(MENU_ID_ASSOCIATE_WITH_PROFILE, { title }));
  325. }
  326. await Promise.all(promises);
  327. } catch (error) {
  328. /* ignored */
  329. }
  330. }
  331. }
  332. })();