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 });