File indexing completed on 2024-04-28 05:32:50
0001 /* 0002 Copyright (C) 2019 Kai Uwe Broulik <kde@privat.broulik.de> 0003 0004 This program is free software; you can redistribute it and/or 0005 modify it under the terms of the GNU General Public License as 0006 published by the Free Software Foundation; either version 3 of 0007 the License, or (at your option) any later version. 0008 0009 This program is distributed in the hope that it will be useful, 0010 but WITHOUT ANY WARRANTY; without even the implied warranty of 0011 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 0012 GNU General Public License for more details. 0013 0014 You should have received a copy of the GNU General Public License 0015 along with this program. If not, see <http://www.gnu.org/licenses/>. 0016 */ 0017 0018 const purposeShareMenuId = "purpose_share"; 0019 let hasPurposeMenu = false; 0020 let hasPurposeTabMenu = false; 0021 0022 // Stores <notification id, share url> so that when you click the finished 0023 // notification it will open the URL 0024 let purposeNotificationUrls = {}; 0025 0026 function purposeShare(data) { 0027 return new Promise((resolve, reject) => { 0028 sendPortMessageWithReply("purpose", "share", {data}).then((reply) => { 0029 if (!reply.success) { 0030 if (!["BUSY", "CANCELED", "INVALID_ARGUMENT"].includes(reply.errorCode) 0031 && reply.errorCode !== 1 /*ERR_USER_CANCELED*/) { 0032 chrome.notifications.create(null, { 0033 type: "basic", 0034 title: chrome.i18n.getMessage("purpose_share_failed_title"), 0035 message: chrome.i18n.getMessage("purpose_share_failed_text", 0036 reply.errorMessage || chrome.i18n.getMessage("general_error_unknown")), 0037 iconUrl: "icons/document-share-failed.png" 0038 }); 0039 } 0040 0041 reject(); 0042 return; 0043 } 0044 0045 let url = reply.response.url; 0046 if (url) { 0047 chrome.notifications.create(null, { 0048 type: "basic", 0049 title: chrome.i18n.getMessage("purpose_share_finished_title"), 0050 message: chrome.i18n.getMessage("purpose_share_finished_text", url), 0051 iconUrl: "icons/document-share.png" 0052 }, (notificationId) => { 0053 if (chrome.runtime.lastError) { 0054 return; 0055 } 0056 0057 purposeNotificationUrls[notificationId] = url; 0058 }); 0059 } 0060 0061 resolve(); 0062 }); 0063 }); 0064 } 0065 0066 function checkPurposeEnabled() { 0067 return Promise.all([ 0068 sendPortMessageWithReply("settings", "getSubsystemStatus"), 0069 SettingsUtils.get() 0070 ]).then((result) => { 0071 0072 const subsystemStatus = result[0]; 0073 const settings = result[1]; 0074 0075 // HACK Unfortunately I removed the loaded/unloaded signals for plugins 0076 // so we can't reliably know on settings change whether a module is enabled 0077 // sending settings is also legacy done without a reply we could wait for. 0078 // Instead, check whether the module is known and enabled in settings, 0079 // which should be close enough, since purpose plugin also has no additional 0080 // dependencies that could make it fail to load. 0081 return subsystemStatus.hasOwnProperty("purpose") 0082 && settings.purpose && settings.purpose.enabled; 0083 }); 0084 } 0085 0086 function updatePurposeMenu() { 0087 checkPurposeEnabled().then((enabled) => { 0088 let props = { 0089 id: purposeShareMenuId, 0090 contexts: ["link", "page", "image", "audio", "video", "selection"], 0091 title: chrome.i18n.getMessage("purpose_share") 0092 }; 0093 0094 if (IS_FIREFOX) { 0095 props.icons = { 0096 "16": "icons/document-share-symbolic.svg" 0097 } 0098 } 0099 0100 if (enabled && !hasPurposeMenu) { 0101 chrome.contextMenus.create(props, () => { 0102 const error = chrome.runtime.lastError; 0103 if (error) { 0104 console.warn("Error creating purpose context menu", error.message); 0105 return; 0106 } 0107 hasPurposeMenu = true; 0108 }); 0109 } else if (!enabled && hasPurposeMenu) { 0110 chrome.contextMenus.remove(props.id, () => { 0111 const error = chrome.runtime.lastError; 0112 if (error) { 0113 console.warn("Error removing purpose context menu", error.message); 0114 return; 0115 } 0116 hasPurposeMenu = false; 0117 }); 0118 } 0119 0120 // Entry on a tab in the tab bar (Firefox) 0121 props.id += "_tab"; 0122 if (IS_FIREFOX && enabled && !hasPurposeTabMenu) { 0123 props.contexts = ["tab"]; 0124 // TODO restrict patterns also for generic menu (however, needs a split like KDE Connect does). 0125 props.documentUrlPatterns = ["http://*/*", "https://*/*"]; 0126 0127 chrome.contextMenus.create(props, () => { 0128 if (!chrome.runtime.lastError) { 0129 hasPurposeTabMenu = true; 0130 } 0131 }); 0132 } else if (!enabled && hasPurposeTabMenu) { 0133 chrome.contextMenus.remove(props.id, () => { 0134 if (!chorme.runtime.lastError) { 0135 hasPurposeTabMenu = false; 0136 } 0137 }); 0138 } 0139 }); 0140 } 0141 0142 chrome.contextMenus.onClicked.addListener((info) => { 0143 if (!info.menuItemId.startsWith(purposeShareMenuId)) { 0144 return; 0145 } 0146 0147 let url = info.linkUrl || info.srcUrl || info.pageUrl; 0148 let selection = info.selectionText; 0149 if (!url && !selection) { 0150 return; 0151 } 0152 0153 let shareData = {}; 0154 if (selection) { 0155 shareData.text = selection; 0156 } else if (url) { 0157 shareData.url = url; 0158 if (info.linkText && info.linkText != url) { 0159 shareData.title = info.linkText; 0160 } 0161 } 0162 0163 // We probably shared the current page, add its title to shareData 0164 new Promise((resolve, reject) => { 0165 if (!info.linkUrl && !info.srcUrl && info.pageUrl) { 0166 let pageUrlWithoutHash = new URL(info.pageUrl); 0167 // chrome.tabs.query url does not match URL hash. 0168 pageUrlWithoutHash.hash = ""; 0169 0170 chrome.tabs.query({ 0171 // more correct would probably be currentWindow + activeTab 0172 url: pageUrlWithoutHash.href 0173 }, (tabs) => { 0174 for (let tab of tabs) { 0175 if (tab.url === info.pageUrl) { 0176 return resolve(tab.title); 0177 } 0178 } 0179 resolve(""); 0180 }); 0181 return; 0182 } 0183 0184 resolve(""); 0185 }).then((title) => { 0186 if (title) { 0187 shareData.title = title; 0188 } 0189 0190 purposeShare(shareData); 0191 }); 0192 }); 0193 0194 SettingsUtils.onChanged().addListener((delta) => { 0195 if (delta.purpose) { 0196 updatePurposeMenu(); 0197 } 0198 }); 0199 0200 addRuntimeCallback("purpose", "share", (message, sender, action) => { 0201 return purposeShare(message); 0202 }); 0203 0204 chrome.notifications.onClicked.addListener((notificationId) => { 0205 const url = purposeNotificationUrls[notificationId]; 0206 if (url) { 0207 chrome.tabs.create({url}); 0208 } 0209 }); 0210 0211 chrome.notifications.onClosed.addListener((notificationId) => { 0212 delete purposeNotificationUrls[notificationId]; 0213 });