diff --git a/extension/action_popup.js b/extension/action_popup.js
index 1711a40d..8c77d6f1 100644
--- a/extension/action_popup.js
+++ b/extension/action_popup.js
@@ -1,271 +1,571 @@
/*
Copyright (C) 2019 Kai Uwe Broulik
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 3 of
the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
class TabUtils {
// Gets the currently viewed tab
static getCurrentTab() {
return new Promise((resolve, reject) => {
chrome.tabs.query({
active: true,
currentWindow: true
}, (tabs) => {
const error = chrome.runtime.lastError;
if (error) {
return reject(error.message);
}
const tab = tabs[0];
if (!tab) {
return reject("NO_TAB");
}
resolve(tab);
});
});
}
// Gets the URLs of the currently viewed tab including all of its iframes
static getCurrentTabFramesUrls() {
return new Promise((resolve, reject) => {
TabUtils.getCurrentTab().then((tab) => {
chrome.tabs.executeScript({
allFrames: true, // so we also catch iframe videos
code: `window.location.href`,
runAt: "document_start"
}, (result) => {
const error = chrome.runtime.lastError;
if (error) {
return reject(error.message);
}
resolve(result);
});
});
});
}
};
class MPrisBlocker {
getAllowed() {
return new Promise((resolve, reject) => {
Promise.all([
SettingsUtils.get(),
TabUtils.getCurrentTabFramesUrls()
]).then((result) => {
const settings = result[0];
const currentUrls = result[1];
const mprisSettings = settings.mpris;
if (!mprisSettings.enabled) {
return reject("MPRIS_DISABLED");
}
if (!currentUrls) { // can this happen?
return reject("NO_URLS");
}
const origins = currentUrls.map((url) => {
try {
return new URL(url).origin;
} catch (e) {
console.warn("Invalid url", url);
return "";
}
}).filter((origin) => {
return !!origin;
});
if (origins.length === 0) {
return reject("NO_ORIGINS");
}
const uniqueOrigins = [...new Set(origins)];
const websiteSettings = mprisSettings.websiteSettings || {};
let response = {
origins: {},
mprisSettings
};
for (const origin of uniqueOrigins) {
let allowed = true;
if (typeof MPRIS_WEBSITE_SETTINGS[origin] === "boolean") {
allowed = MPRIS_WEBSITE_SETTINGS[origin];
}
if (typeof websiteSettings[origin] === "boolean") {
allowed = websiteSettings[origin];
}
response.origins[origin] = allowed;
}
resolve(response);
}, reject);
});
}
setAllowed(origin, allowed) {
return SettingsUtils.get().then((settings) => {
const mprisSettings = settings.mpris;
if (!mprisSettings.enabled) {
return reject("MPRIS_DISABLED");
}
let websiteSettings = mprisSettings.websiteSettings || {};
let implicitAllowed = true;
if (typeof MPRIS_WEBSITE_SETTINGS[origin] === "boolean") {
implicitAllowed = MPRIS_WEBSITE_SETTINGS[origin];
}
if (allowed !== implicitAllowed) {
websiteSettings[origin] = allowed;
} else {
delete websiteSettings[origin];
}
mprisSettings.websiteSettings = websiteSettings;
return SettingsUtils.set({
mpris: mprisSettings
});
});
}
};
+class Itinerary {
+ extract() {
+ TabUtils.getCurrentTab().then((tab) => {
+ // Extract currently viewed PDF, e.g. if vieweing boarding pass PDF in internal PDF viewer
+ if (new URL(tab.url).pathname.toLowerCase().endsWith(".pdf")) {
+ const reader = new BlobReader(tab.url);
+ reader.type = BlobReader.READ_TYPE_DATA_URL;
+ reader.read().then((result) => {
+ // Limit from ExtractorEngine, plus 30% overhead of being base64 encoded
+ if (result.length > 4000000 * 1.3) {
+ return;
+ }
+
+ this._doExtract("Pdf", result);
+ });
+ return;
+ }
+
+ // Extract website HTML
+ chrome.tabs.executeScript({
+ allFrames: false,
+ code: `document.documentElement.outerHTML`,
+ runAt: "document_end"
+ }, (result) => {
+ if (chrome.runtime.lastError) {
+ return;
+ }
+
+ result = result[0];
+ if (!result) {
+ return;
+ }
+
+ this._doExtract("Html", result);
+ });
+ });
+ }
+
+ _doExtract(type, data) {
+ return new Promise((resolve, reject) => {
+ sendMessage("itinerary", "extract", {
+ type,
+ data
+ }).then((result) => {
+ if (!result.success) {
+ // TODO print error?
+ return;
+ }
+
+ this.buildUi(result.data);
+ });
+ });
+ }
+
+ buildUi(result) {
+ if (!result || result.length === 0) {
+ return;
+ }
+
+ const foundTypes = result.map((item) => {
+ return item["@type"];
+ });
+
+ // is joined together as a string so I don't always have to expand the Array in the developer console
+ console.log("Itinerary extractor found types:", foundTypes.join(",").substr(0,100));
+
+ const filteredResult = result.filter((item) => {
+ return SUPPORTED_ITINERARY_TYPES.includes(item["@type"]);
+ });
+
+ // Likely an overview page, not much practical use
+ if (filteredResult.length > MAXIMUM_ITINERARY_TYPE_OCCURRENCES) {
+ return;
+ }
+
+ // HACK prefer the result with most keys, likely to have more useful data
+ // Ideally KItinerary filtered out obvious garbage like a Restaurant with just a name and nothing else
+ filteredResult.sort((a, b) => {
+ return Object.keys(b).length - Object.keys(a).length;
+ });
+
+ const data = filteredResult[0];
+ if (!data) {
+ return;
+ }
+
+ // There's some content, hide dummy placeholder
+ document.getElementById("dummy-main").classList.add("hidden");
+
+ const infoElement = document.querySelector(`.itinerary-info[data-itinerary-context*='${data["@type"]}'`);
+ infoElement.classList.remove("hidden");
+
+ // All of this below is pretty ridiculous and should be replaced by a proper HTML templating engine :)
+ const elements = infoElement.querySelectorAll("[data-prop]");
+ for (const element of elements) {
+ const keys = element.dataset.prop.split(",");
+
+ const values = keys.map((key) => {
+ const segments = key.split(".");
+ let depth = 0;
+ let value = data;
+
+ while (depth < segments.length) {
+ const key = segments[depth];
+ value = value[key];
+ ++depth;
+ if (!value) {
+ return undefined;
+ }
+ }
+
+ return value;
+ });
+ const value = values[0];
+
+ if (!values.some((value) => {
+ return !!value;
+ })) {
+ element.classList.add("hidden");
+ continue;
+ }
+
+ switch (element.tagName) {
+ case "IMG":
+ element.src = value;
+ break;
+ default:
+ const formatter = element.dataset.formatter;
+ switch (formatter) {
+
+ case "daterange":
+ // TODO check if there's only one date
+ const now = new Date();
+ const startDate = new Date(values[0]);
+ const endDate = new Date(values[1]);
+
+ // TODO pretty format when end on same as start, no year if same and current etc etc etc
+
+ const formattedDates = values.map((value) => {
+ return new Date(value).toLocaleString(navigator.language, {
+ weekday: 'short', year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'
+ });
+ });
+
+ element.innerText = `${formattedDates[0]} to ${formattedDates[1]}`;
+
+
+ /*let startDateFormat = {
+ weekday: "short"
+ day: "numeric",
+ month: "short"
+ };
+ let endDateFormat = {
+
+ };
+
+ if (startDate.getFullYear() !== endDate.getFullYear()
+ || startDate.getFullYear() !== now.getFullYear()
+ || endDate.getFullYear() !== now.getFullYear()) {
+ dateFormat.year = "numeric";
+ }
+
+ if (startDate.getHours() !== 0 || startDate.getMinutes() !== 0 || startDate.getSeconds() !== 0
+ || endDate.getHours() !== 0 || endDate.getMinutes() !== 0 || endDate.getSeconds !== 0) {
+ dateFormat.hour = "2-digit";
+ dateFormat.minute = "2-digit";
+ }
+
+
+ // TODO handle if there's only one date
+ element.innerText = `${formattedDates[0]} to ${formattedDates[1]}`;*/
+ break;
+
+ case "place":
+ const address = value.address;
+ switch (address["@type"]) {
+ case "PostalAddress":
+ // TODO geo url or OSM or whatever
+ // FIXME
+ element.innerText = `${address.streetAddress}, ${address.postalCode}, ${address.addressLocality} (${address.addressCountry})`;
+ break;
+ }
+
+ break;
+ case "address":
+ switch (value["@type"]) {
+ case "PostalAddress":
+ // TODO geo url or OSM or whatever
+ // FIXME
+ element.innerText = `${value.streetAddress}\n${value.postalCode}, ${value.addressLocality} (${value.addressCountry})`;
+ break;
+ }
+
+ break;
+
+ case "phonelink": {
+ let link = document.createElement("a");
+ link.href = "tel:" + value;
+ link.target = "_blank";
+ link.innerText = value; // TODO pretty format phone number
+ link.addEventListener("click", (e) => {
+ e.preventDefault();
+ alert("Call " + link.href);
+ });
+ // TODO clear element before
+ element.appendChild(link);
+ break;
+ }
+
+ case "emaillink": {
+ let link = document.createElement("a");
+ link.href = "mailto:" + value;
+ link.target = "_blank";
+ link.innerText = value;
+ element.appendChild(link);
+ break;
+ }
+
+ case "background-image":
+ let backgroundUrl = value;
+ if (backgroundUrl.startsWith("//")) {
+ // FIXME resolve properly based on current website URL!
+ backgroundUrl = "https:" + backgroundUrl;
+ }
+ element.style.backgroundImage = `url('${backgroundUrl}')`;
+ break;
+
+ case "noop":
+ break;
+ default:
+ element.innerText = value;
+ }
+ }
+ }
+
+ const outOfContextElements = infoElement.querySelectorAll("[data-context]:not([data-context=" + data["@type"]);
+ outOfContextElements.forEach((element) => {
+ element.classList.add("hidden");
+ });
+
+ const actionsListElement = document.getElementById("itinerary-info-actions");
+
+ const actions = data.potentialAction || [];
+ for (const action of actions) {
+ let listItem = document.createElement("li");
+
+ let link = document.createElement("a");
+
+ let text = action.result && action.result.name;
+ if (!text) {
+ switch (action["@type"]) {
+ case "ReserveAction":
+ switch (data["@type"]) {
+ case "FoodEstablishment":
+ case "Restaurant":
+ // TODO rest
+ text = "Reserve a Table"; // FIXME i18n
+ break;
+ }
+
+ break;
+ }
+ }
+
+ link.innerText = text;
+
+ link.href = action.target;
+ link.target = "_blank";
+
+ listItem.appendChild(link);
+
+ actionsListElement.appendChild(listItem);
+ }
+
+ document.querySelectorAll("[data-action]").forEach((element) => {
+ element.addEventListener("click", (e) => {
+ e.preventDefault();
+
+ const action = element.dataset.action;
+
+ switch (action) {
+ case "add-to-itinerary":
+
+ /*sendMessage("itinerary", "run", {
+ path: ""
+ });*/
+
+ break;
+ }
+ });
+ });
+ }
+};
+
document.addEventListener("DOMContentLoaded", () => {
sendMessage("browserAction", "getStatus").then((status) => {
switch (status.portStatus) {
case "UNSUPPORTED_OS":
document.getElementById("unsupported_os_error").classList.remove("hidden");
break;
case "STARTUP_FAILED":
document.getElementById("startup_error").classList.remove("hidden");
break;
default:
document.getElementById("main").classList.remove("hidden");
let errorText = status.portLastErrorMessage;
if (errorText === "UNKNOWN") {
errorText = chrome.i18n.getMessage("general_error_unknown");
}
if (errorText) {
document.getElementById("runtime_error_text").innerText = errorText;
document.getElementById("runtime_error").classList.remove("hidden");
// There's some content, hide dummy placeholder
document.getElementById("dummy-main").classList.add("hidden");
}
break;
}
// HACK so the extension can tell we closed, see "browserAction" "ready" callback in extension.js
chrome.runtime.onConnect.addListener((port) => {
if (port.name !== "browserActionPort") {
return;
}
// do we need to do something with the port here?
});
sendMessage("browserAction", "ready");
});
// MPris blocker checkboxes
const blocker = new MPrisBlocker();
blocker.getAllowed().then((result) => {
const origins = result.origins;
if (Object.entries(origins).length === 0) { // "isEmpty"
return;
}
// To keep media controls setting from always showing up, only show them, if:
// - There is actually a player anywhere on this tab
// or, since when mpris is disabled, there are never any players
// - when media controls are disabled for any origin on this tab
new Promise((resolve, reject) => {
for (let origin in origins) {
if (origins[origin] === false) {
return resolve("HAS_BLOCKED");
}
}
TabUtils.getCurrentTab().then((tab) => {
return sendMessage("mpris", "hasTabPlayer", {
tabId: tab.id
});
}).then((playerIds) => {
if (playerIds.length > 0) {
return resolve("HAS_PLAYER");
}
reject("NO_PLAYER_NO_BLOCKED");
});
}).then(() => {
// There's some content, hide dummy placeholder
document.getElementById("dummy-main").classList.add("hidden");
let blacklistInfoElement = document.querySelector(".mpris-blacklist-info");
blacklistInfoElement.classList.remove("hidden");
let originsListElement = blacklistInfoElement.querySelector("ul.mpris-blacklist-origins");
for (const origin in origins) {
const originAllowed = origins[origin];
let blockListElement = document.createElement("li");
let labelElement = document.createElement("label");
labelElement.innerText = origin;
let checkboxElement = document.createElement("input");
checkboxElement.type = "checkbox";
checkboxElement.checked = (originAllowed === true);
checkboxElement.addEventListener("click", (e) => {
// Let us handle (un)checking the checkbox when setAllowed succeeds
e.preventDefault();
const allowed = checkboxElement.checked;
blocker.setAllowed(origin, allowed).then(() => {
checkboxElement.checked = allowed;
}, (err) => {
console.warn("Failed to change media controls settings:", err);
});
});
labelElement.insertBefore(checkboxElement, labelElement.firstChild);
blockListElement.appendChild(labelElement);
originsListElement.appendChild(blockListElement);
}
}, (err) => {
console.log("Not showing media controls settings because", err);
});
}, (err) => {
console.warn("Failed to check for whether media controls are blocked", err);
});
+
+ sendMessage("settings", "getSubsystemStatus").then((status) => {
+ if (status && status.itinerary && status.itinerary.loaded) {
+ new Itinerary().extract();
+ }
+ });
});
diff --git a/extension/constants.js b/extension/constants.js
index 798d3eda..1541119a 100644
--- a/extension/constants.js
+++ b/extension/constants.js
@@ -1,54 +1,61 @@
/*
Copyright (C) 2017 Kai Uwe Broulik
Copyright (C) 2018 David Edmundson
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 3 of
the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
DEFAULT_EXTENSION_SETTINGS = {
mpris: {
enabled: true,
websiteSettings: {}
},
mprisMediaSessions: {
enabled: true
},
kdeconnect: {
enabled: true
},
downloads: {
enabled: true,
saveOriginUrl: false
},
tabsrunner: {
enabled: true
},
purpose: {
enabled: true
},
+ itinerary: {
+ enabled: true
+ },
breezeScrollBars: {
// this breaks pages in interesting ways, disable by default
enabled: false
}
};
IS_FIREFOX = (typeof InstallTrigger !== "undefined"); // heh.
// NOTE if you change this, make sure to adjust the error message shown in action_popup.html
SUPPORTED_PLATFORMS = ["linux", "openbsd", "freebsd"];
// Default MPRIS settings for websites
const MPRIS_WEBSITE_SETTINGS = {
//"https://www.example.com": false
};
+
+// Supported schema.org types for itinerary extraction, sorted by preferences
+const SUPPORTED_ITINERARY_TYPES = ["Event", "FoodEstablishment", "LodgingBusiness", "FlightReservation", "TrainReservation"];
+const MAXIMUM_ITINERARY_TYPE_OCCURRENCES = 15;
diff --git a/extension/extension-itinerary-quick-extractor.js b/extension/extension-itinerary-quick-extractor.js
new file mode 100644
index 00000000..559757a2
--- /dev/null
+++ b/extension/extension-itinerary-quick-extractor.js
@@ -0,0 +1,64 @@
+/*
+ Copyright (C) 2019 Kai Uwe Broulik
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; either version 3 of
+ the License, or (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+ */
+
+(function() {
+
+ // This is an Object so we can count the number of items ofr any given type
+ // If any type comes up suspiciously often, we assume it's an overview page
+ // and of no practical use.
+ let foundTypes = {};
+
+ const schemaOrgTags = document.querySelectorAll("[itemtype*='schema.org']");
+
+ schemaOrgTags.forEach((tag) => {
+ let type = tag.getAttribute("itemtype");
+ // Using https for an URI is wrong but some websites do that...
+ type = type.replace(/^https?:\/\/s?schema.org\//, "");
+ if (!type) {
+ return; // continue
+ }
+
+ foundTypes[type] = (foundTypes[type] || 0) + 1;
+ });
+
+ const jsonLdScriptTags = document.querySelectorAll("script[type='application/ld+json']");
+ jsonLdScriptTags.forEach((tag) => {
+ try {
+ let json = JSON.parse(tag.innerText);
+ if (json) {
+ if (!Array.isArray(json)) { // turn it into an array so we can use same codepath below
+ json = [json];
+ }
+
+ json.forEach((item) => {
+ const type = item["@type"];
+ if (!type) {
+ return; // continue
+ }
+
+ foundTypes[type] = (foundTypes[type] || 0) + 1;
+ });
+ }
+ } catch (e) {
+ console.warn("Failed to parse json ld in", tag, e);
+ return;
+ }
+ });
+
+ return foundTypes;
+
+})();
diff --git a/extension/extension-itinerary.js b/extension/extension-itinerary.js
new file mode 100644
index 00000000..b8cb88bf
--- /dev/null
+++ b/extension/extension-itinerary.js
@@ -0,0 +1,208 @@
+/*
+ Copyright (C) 2019 Kai Uwe Broulik
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; either version 3 of
+ the License, or (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+ */
+
+const badgeIcons = {
+ "Event": "📅", // "Calendar" Emoji
+ "LodgingBusiness": "🏨", // "Hotel" Emoji, looks more like a hospital to me
+ "FoodEstablishment": "🍽" // "Fork and Knive With Plate" Emoji
+ // TODO FlightReservation
+};
+
+const genericTypes = {
+ "Event": [
+ new RegExp(".*Event$"),
+ "CourseInstance",
+ "EventSeries",
+ "Festival"
+ ],
+ "LodgingBusiness": [
+ "BedAndBreakfast",
+ "Campground",
+ "Hostel",
+ "Hotel",
+ "Motel",
+ "Resort"
+ ],
+ "FoodEstablishment": [
+ "Bakery",
+ "BarOrPub",
+ "Brewery",
+ "CafeOrCoffeeShop",
+ "Distillery",
+ "FastFoodRestaurant",
+ "IceCreamShop",
+ "Restaurant",
+ "Winery"
+ ]
+};
+
+function generalizeType(type) {
+ for (const genericType in genericTypes) {
+ const candidates = genericTypes[genericType];
+
+ for (const candidate of candidates) {
+ if (typeof candidate === "string" && type === candidate) {
+ return genericType;
+ }
+
+ if (candidate instanceof RegExp && type.match(candidate)) {
+ return genericType;
+ }
+ }
+ }
+
+ return type;
+}
+
+let itineraryTabCache = {};
+
+let itineraryEnabled = undefined;
+
+SettingsUtils.onChanged().addListener((delta) => {
+ // Check again next time
+ if (delta.itinerary) {
+ itineraryEnabled = undefined;
+ }
+});
+
+// This is a basic check for whether there is anything worth extracting
+// the actual recursive extraction of structured data happens in the content
+// script on demand once the browser action is actually opened
+function checkForStructuredData() {
+ // Check whether itinerary is enabled and loaded and supported
+ // Only do so once until settings are changed, to avoid continuous getSubsystemStatus queries
+ new Promise((resolve, reject) => {
+ if (itineraryEnabled !== undefined) {
+ return resolve(itineraryEnabled);
+ }
+
+ sendPortMessageWithReply("settings", "getSubsystemStatus").then((status) => {
+ const itinerary = status.itinerary;
+ resolve(itinerary && itinerary.loaded && itinerary.extractorFound);
+ });
+ }).then((enabled) => {
+ itineraryEnabled = enabled;
+
+ if (!enabled) {
+ return;
+ }
+
+ chrome.tabs.query({
+ currentWindow: true,
+ active: true
+ }, (tabs) => {
+ if (chrome.runtime.lastError) {
+ return;
+ }
+
+ const tab = tabs[0];
+ if (!tab) {
+ return;
+ }
+
+ // We set the browser action specifically on the tab, so if we checked a tab once
+ // we don't need to do it again. However, if the tab navigated to a different page,
+ // we invalidated the cache and will check again.
+ if (itineraryTabCache[tab.id]) {
+ return;
+ }
+
+ chrome.tabs.executeScript(tab.id, {
+ frameId: 0, // main frame
+ file: "extension-itinerary-quick-extractor.js"
+ }, (result) => {
+ const error = chrome.runtime.lastError;
+ if (error) {
+ return;
+ }
+
+ result = result[0] || {};
+ itineraryTabCache[tab.id] = result;
+
+ // Generalize types and add specific occurrences to the general type
+ Object.keys(result).forEach((type) => {
+ const genericType = generalizeType(type);
+
+ if (genericType !== type) {
+ result[genericType] = (result[genericType] || 0) + result[type];
+ delete result[type];
+ }
+ });
+
+ let possibleIcon = "";
+ // Should we have a priority order here?
+ for (const type of SUPPORTED_ITINERARY_TYPES) {
+ const count = result[type];
+ if (!count) {
+ continue;
+ }
+
+ // Probably an overview page, no practical use for us
+ if (count > MAXIMUM_ITINERARY_TYPE_OCCURRENCES) {
+ console.log(`Found too many (${count}) occurrences of ${type}, skipping`);
+ // Should we really still carry on after this?
+ continue;
+ }
+
+ possibleIcon = badgeIcons[type];
+ break;
+ }
+
+ // We don't need to unset the icon as we set it on a specific tab and navigating
+ // away to a different page will clear it automatically.
+ // More importantly, this keeps us from clearing an error icon.
+ if (!possibleIcon) {
+ return;
+ }
+
+ chrome.browserAction.setBadgeText({
+ text: possibleIcon,
+ tabId: tab.id
+ });
+
+ chrome.browserAction.setBadgeBackgroundColor({
+ color: "#4d4d4d", // Breeze "black" icon color
+ tabId: tab.id
+ });
+ });
+
+ });
+ });
+}
+
+chrome.tabs.onActivated.addListener(checkForStructuredData);
+chrome.windows.onFocusChanged.addListener(checkForStructuredData);
+
+chrome.tabs.onRemoved.addListener((tabId) => {
+ delete itineraryTabCache[tabId];
+});
+
+chrome.tabs.onUpdated.addListener((tabId, info, tab) => {
+ if (info.status === "complete") {
+ delete itineraryTabCache[tabId];
+
+ checkForStructuredData();
+ }
+});
+
+addRuntimeCallback("itinerary", "extract", (message) => {
+ return new Promise((resolve, reject) => {
+ sendPortMessageWithReply("itinerary", "extract", message).then((reply) => {
+ resolve(reply);
+ });
+ });
+});
diff --git a/extension/icons/airplane.svg b/extension/icons/airplane.svg
new file mode 100644
index 00000000..8e67ebdd
--- /dev/null
+++ b/extension/icons/airplane.svg
@@ -0,0 +1,79 @@
+
+
+
+
diff --git a/extension/manifest.json b/extension/manifest.json
index 842038ef..e8b4f85f 100644
--- a/extension/manifest.json
+++ b/extension/manifest.json
@@ -1,74 +1,75 @@
{
"manifest_version": 2,
"name": "Plasma Integration",
"short_name": "Plasma",
"description": "Provides better integration with the KDE Plasma 5 desktop.",
"version": "1.6.1",
"default_locale": "en",
"author": "Kai Uwe Broulik ",
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1Wz6JigIdPBxPJvYrnjuKM3X4YEaUHgyOW2TM1G1Maxook1HO8vLFGhBR7g3jEQ7Yw9PTa6ZAa8J/I9a/1EQz0Ssx2+m3R9SvJfdKSCrCth8nwMpB8V8I5EhiUoW57kM0v9r/18Buem5cBIo0EnSAVCbmNG95R5jtg62P4+LNGEwHk5L7RSblXUN2hhUwXty4A98EXD6T2Pnpu1m8nRF2B1t5AcspSMV5ECnL9x8kT5bup1gJo0FGTz6C+1huDNRaI3OY1YDWyvCVRFXlAhuFERTW6siMdghK++kYM43H7cvJMFYnVVuffD9WNRsUCI9V0SrqYbPre/2nEl+8VUXVwIDAQAB",
"icons": {
"16": "icons/plasma-16.png",
"32": "icons/plasma-32.png",
"48": "icons/plasma-48.png",
"128": "icons/plasma-128.png"
},
"background": {
"scripts": [
"constants.js",
"utils.js",
"extension-utils.js",
"extension-kdeconnect.js",
"extension-mpris.js",
"extension-downloads.js",
"extension-tabsrunner.js",
"extension-purpose.js",
+ "extension-itinerary.js",
"extension.js"
],
"persistent": false
},
"browser_action": {
"browser_style": true,
"default_popup": "action_popup.html"
},
"content_scripts": [
{
"matches": ["*://*/*"],
"js": ["constants.js", "utils.js", "content-utils.js", "content-script.js"],
"run_at":"document_start",
"all_frames": true,
"match_about_blank": true
}
],
"options_ui": {
"page": "options.html",
"chrome_style": true
},
"permissions": [
"nativeMessaging",
"notifications",
"storage",
"downloads",
"tabs",
"",
"contextMenus"
],
"applications": {
"gecko": {
"id": "plasma-browser-integration@kde.org",
"strict_min_version": "50.0"
}
},
"optional_permissions": [ "webRequest" ]
}
diff --git a/extension/options.html b/extension/options.html
index 541f184f..adb8d520 100644
--- a/extension/options.html
+++ b/extension/options.html
@@ -1,118 +1,125 @@
diff --git a/extension/options.js b/extension/options.js
index 33c70704..4beadca2 100644
--- a/extension/options.js
+++ b/extension/options.js
@@ -1,264 +1,272 @@
/*
Copyright (C) 2017 Kai Uwe Broulik
Copyright (C) 2018 David Edmundson
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 3 of
the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
function tabClicked(tabbar, tabbutton) {
tabbar.buttons.forEach(function (button) {
var tablink = button.dataset.tabLink
var tabtarget = document.querySelector("[data-tab-id=" + tablink + "]");
if (tabbutton == button) {
button.classList.add("active");
tabtarget.classList.add("active");
} else {
button.classList.remove("active");
tabtarget.classList.remove("active");
}
});
}
function loadSettings() {
SettingsUtils.get().then((items) => {
for (let key in items) {
if (!items.hasOwnProperty(key)) {
continue;
}
let controls = document.querySelectorAll("[data-extension=" + key + "]");
for (let control of controls) {
let settingsKey = control.dataset.settingsKey;
if (!settingsKey) {
console.warn("Invalid settings key in", control, "cannot load this");
continue;
}
let value = items[key][settingsKey]
if (control.type === "checkbox") {
control.checked = !!value;
} else {
if (value === true) {
control.value = "TRUE";
} else if (value === false) {
control.value = "FALSE";
} else {
control.value = value;
}
}
updateDependencies(control, key, settingsKey);
control.addEventListener("change", () => {
let saveFailureInfoElement = document.getElementById("save-failure-info");
saveFailureInfoElement.classList.add("hidden");
updateDependencies(control, key, settingsKey);
saveSettings((error) => {
if (error) {
saveFailureInfoElement.classList.remove("hidden");
let saveMessageElement = document.getElementById("save-message");
try {
saveMessageElement.innerText = chrome.i18n.getMessage("options_save_failed");
} catch (e) {
// When the extension is reloaded, any call to extension APIs throws, make sure we show at least some form of error
saveMessageElement.innerText = "Saving settings failed (" + (error || e) + ")";
}
return;
}
});
});
}
}
});
}
function saveSettings(cb) {
var settings = {};
let controls = document.querySelectorAll("[data-extension]");
for (let control of controls) {
let extension = control.dataset.extension;
if (!DEFAULT_EXTENSION_SETTINGS.hasOwnProperty(extension)) {
console.warn("Cannot save settings for extension", extension, "which isn't in DEFAULT_EXTENSION_SETTINGS");
continue;
}
let settingsKey = control.dataset.settingsKey;
if (!settingsKey) {
console.warn("Invalid settings key in", control, "cannot save this");
continue;
}
if (!settings[extension]) {
settings[extension] = {};
}
if (!DEFAULT_EXTENSION_SETTINGS[extension].hasOwnProperty(settingsKey)) {
console.warn("Cannot save settings key", settingsKey, "in extension", extension, "which isn't in DEFAULT_EXTENSION_SETTINGS");
continue;
}
if (control.type === "checkbox") {
settings[extension][settingsKey] = control.checked;
} else {
let value = control.value;
if (value === "TRUE") {
value = true;
} else if (value === "FALSE") {
value = false;
}
settings[extension][settingsKey] = value;
}
}
SettingsUtils.set(settings).then(() => {
cb();
}, (err) => {
cb(err);
});
}
function updateDependencies(control, extension, settingsKey) {
// Update all depending controls
let value = control.type === "checkbox" ? control.checked : control.value;
if (value === true) {
value = "TRUE";
} else if (value === false) {
value = "FALSE";
}
let dependencies = document.querySelectorAll("[data-depends-extension=" + extension + "][data-depends-settings-key=" + settingsKey + "]");
for (let dependency of dependencies) {
dependency.disabled = (value != dependency.dataset.dependsSettingsValue);
}
}
document.addEventListener("DOMContentLoaded", function () {
// poor man's tab widget :)
document.querySelectorAll(".tabbar").forEach(function (tabbar) {
tabbar.buttons = [];
tabbar.querySelectorAll("[data-tab-link]").forEach(function (button) {
var tablink = button.dataset.tabLink
var tabtarget = document.querySelector("[data-tab-id=" + tablink + "]");
button.addEventListener("click", function (event) {
tabClicked(tabbar, button);
event.preventDefault();
});
tabbar.buttons.push(button);
// start with the one tab page that is active
if (tabtarget.classList.contains("active")) {
tabClicked(tabbar, button);
}
});
});
if (IS_FIREFOX) {
document.querySelectorAll("[data-not-show-in=firefox]").forEach(function (item) {
item.style.display = "none";
});
}
// check whether the platform is supported before loading and activating settings
chrome.runtime.getPlatformInfo(function (info) {
if (!SUPPORTED_PLATFORMS.includes(info.os)) {
document.body.classList.add("os-not-supported");
return;
}
loadSettings();
// When getSubsystemStatus fails we assume it's an old host without any of the new features
// for which we added the requires-extension attributes. Disable all of them initially
// and then have the supported ones enabled below.
document.querySelectorAll("[data-requires-extension]").forEach((item) => {
item.classList.add("not-supported", "by-host");
});
sendMessage("settings", "getSubsystemStatus").then((status) => {
document.querySelectorAll("[data-requires-extension]").forEach((item) => {
let requiresExtension = item.dataset.requiresExtension;
if (requiresExtension && !status.hasOwnProperty(requiresExtension)) {
console.log("Extension", requiresExtension, "is not supported by this version of the host");
return; // continue
}
let requiresMinimumVersion = Number(item.dataset.requiresExtensionVersionMinimum);
if (requiresMinimumVersion) {
let runningVersion = status[requiresExtension].version;
if (runningVersion < requiresMinimumVersion) {
console.log("Extension", requiresExtension, "of version", requiresMinimumVersion, "is required but only", runningVersion, "is present in the host");
return; // continue
}
}
item.classList.remove("not-supported", "by-host");
});
+
+ // If itinerary is supported in the host but the extractor is not installed, show a hint
+ const itinerary = status.itinerary;
+ if (itinerary && !itinerary.extractorFound) {
+ console.log("Itinerary extractor not found");
+ document.getElementById("itinerary-not-installed-info").classList.remove("not-supported");
+ }
+
}).catch((e) => {
// The host is most likely not working correctly
// If we run this against an older host which doesn't support message replies
// this handler is never entered, so we really encountered an error just now!
console.warn("Failed to determine subsystem status", e);
document.body.classList.add("startup-failure");
});
Promise.all([
sendMessage("settings", "getVersion"),
chrome.runtime.getManifest()
]).then((results) => {
const versionInfo = results[0];
const manifest = results[1];
document.getElementById("version-info-host").innerText = chrome.i18n.getMessage("options_about_host_version",
versionInfo.host);
document.getElementById("version-info-extension").innerText = chrome.i18n.getMessage("options_about_extension_version", manifest.version);
document.getElementById("version-info").classList.remove("not-supported");
});
});
document.getElementById("open-krunner-settings").addEventListener("click", function (event) {
sendMessage("settings", "openKRunnerSettings");
event.preventDefault();
});
// Make translators credit behave like the one in KAboutData
var translatorsAboutData = "";
var translators = chrome.i18n.getMessage("options_about_translators");
if (translators && translators !== "Your names") {
translatorsAboutData = chrome.i18n.getMessage("options_about_translated_by", translators)
}
var translatorsAboutDataItem = document.getElementById("translators-aboutdata");
if (translatorsAboutData) {
translatorsAboutDataItem.innerText = translatorsAboutData;
} else {
translatorsAboutDataItem.style.display = "none";
}
});
diff --git a/extension/utils.js b/extension/utils.js
index 20b2d49d..fa90438c 100644
--- a/extension/utils.js
+++ b/extension/utils.js
@@ -1,56 +1,117 @@
/*
Copyright (C) 2019 Kai Uwe Broulik
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 3 of
the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
class SettingsUtils {
static storage() {
return (IS_FIREFOX ? chrome.storage.local : chrome.storage.sync);
}
static get() {
return new Promise((resolve, reject) => {
SettingsUtils.storage().get(DEFAULT_EXTENSION_SETTINGS, (items) => {
const error = chrome.runtime.lastError;
if (error) {
return reject(error);
}
resolve(items);
});
});
}
static set(settings) {
return new Promise((resolve, reject) => {
try {
SettingsUtils.storage().set(settings, () => {
const error = chrome.runtime.lastError;
if (error) {
return reject(error);
}
resolve();
});
} catch (e) {
reject(e);
}
});
}
static onChanged() {
return chrome.storage.onChanged;
}
}
+
+class BlobReader {
+ constructor(url) {
+ this._url = url;
+ this._timeout = -1;
+ this._type = BlobReader.READ_TYPE_TEXT;
+ }
+
+ // oof...
+ static get READ_TYPE_ARRAY_BUFFER() { return "ArrayBuffer"; }
+ static get READ_TYPE_BINARY_STRING() { return "BinaryString"; }
+ static get READ_TYPE_DATA_URL() { return "DataURL"; }
+ static get READ_TYPE_TEXT() { return "Text"; }
+
+ get timeout() {
+ return this._timeout;
+ }
+ set timeout(timeout) {
+ this._timeout = timeout;
+ }
+
+ get type() {
+ return this._type;
+ }
+ set type(type) {
+ this._type = type;
+ }
+
+ read() {
+ return new Promise((resolve, reject) => {
+ const xhr = new XMLHttpRequest();
+ xhr.onreadystatechange = () => {
+ if (xhr.readyState != 4) {
+ return;
+ }
+
+ if (!xhr.response) { // TODO check status code, too?
+ return reject("NO_RESPONSE");
+ }
+
+ const reader = new FileReader();
+ reader.onloadend = () => {
+ if (reader.error) {
+ return reject(reader.error);
+ }
+
+ resolve(reader.result);
+ };
+
+ reader["readAs" + this._type](xhr.response);
+ };
+
+ xhr.open("GET", this._url);
+ xhr.responseType = "blob";
+ if (this._timeout > -1) {
+ xhr.timeout = this._timeout;
+ }
+ xhr.send();
+ });
+ }
+};
diff --git a/host/CMakeLists.txt b/host/CMakeLists.txt
index fbe4345c..00ed353c 100644
--- a/host/CMakeLists.txt
+++ b/host/CMakeLists.txt
@@ -1,37 +1,40 @@
add_definitions(-DTRANSLATION_DOMAIN=\"plasma-browser-integration-host\")
configure_file(config-host.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-host.h)
set(HOST_SOURCES main.cpp
connection.cpp
pluginmanager.cpp
settings.cpp
mprisplugin.cpp
abstractbrowserplugin.cpp
kdeconnectplugin.cpp
downloadplugin.cpp
downloadjob.cpp
tabsrunnerplugin.cpp
purposeplugin.cpp
+ itineraryplugin.cpp
+ itineraryextractorjob.cpp
)
qt5_add_dbus_adaptor(HOST_SOURCES ../dbus/org.kde.plasma.browser_integration.TabsRunner.xml tabsrunnerplugin.h TabsRunnerPlugin)
qt5_add_dbus_adaptor(HOST_SOURCES ../dbus/org.kde.plasma.browser_integration.Settings.xml settings.h Settings)
qt5_add_dbus_adaptor(HOST_SOURCES ../dbus/org.mpris.MediaPlayer2.xml mprisplugin.h MPrisPlugin mprisroot MPrisRoot)
qt5_add_dbus_adaptor(HOST_SOURCES ../dbus/org.mpris.MediaPlayer2.Player.xml mprisplugin.h MPrisPlugin mprisplayer MPrisPlayer)
add_executable(plasma-browser-integration-host ${HOST_SOURCES})
target_link_libraries(
plasma-browser-integration-host
Qt5::DBus
Qt5::Gui
Qt5::Widgets
KF5::Crash
KF5::I18n
KF5::KIOCore
KF5::PurposeWidgets
+ KF5::Notifications
KF5::FileMetaData
)
install(TARGETS plasma-browser-integration-host ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
diff --git a/host/config-host.h.cmake b/host/config-host.h.cmake
index 91769e3d..336f706f 100644
--- a/host/config-host.h.cmake
+++ b/host/config-host.h.cmake
@@ -1 +1,3 @@
#define HOST_VERSION_STRING "${PROJECT_VERSION}"
+
+#define CMAKE_INSTALL_FULL_LIBEXECDIR_KF5 "${CMAKE_INSTALL_FULL_LIBEXECDIR_KF5}"
diff --git a/host/downloadjob.cpp b/host/downloadjob.cpp
index e4982a8f..398505e0 100644
--- a/host/downloadjob.cpp
+++ b/host/downloadjob.cpp
@@ -1,278 +1,289 @@
/*
Copyright (C) 2017 by Kai Uwe Broulik
Copyright (C) 2017 by David Edmundson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
#include "downloadjob.h"
#include "settings.h"
#include
#include
+#include
#include
#include
#include
DownloadJob::DownloadJob(int id)
: KJob()
, m_id(id)
{
// the thing with "canResume" in chrome downloads is that it just means
// "this download can be resumed right now because it is paused",
// it's not a general thing. I think we can always pause/resume downloads
// unless they're canceled/interrupted at which point we don't have a DownloadJob
// anymore anyway
setCapabilities(Killable | Suspendable);
// TODO When suspending on Firefox the download job goes away for some reason?!
// Until I have the virtue to figure that out just disallow suspending downloads on Firefox :)
if (Settings::self().environment() == Settings::Environment::Firefox) {
setCapabilities(Killable);
}
}
void DownloadJob::start()
{
QMetaObject::invokeMethod(this, "doStart", Qt::QueuedConnection);
}
void DownloadJob::doStart()
{
}
bool DownloadJob::doKill()
{
emit killRequested();
// TODO what if the user kills us from notification area while the
// "Save As" prompt is still open?
return true;
}
bool DownloadJob::doSuspend()
{
emit suspendRequested();
return true;
}
bool DownloadJob::doResume()
{
emit resumeRequested();
return true;
}
+QString DownloadJob::fileName() const
+{
+ return m_fileName;
+}
+
+QString DownloadJob::mimeType() const
+{
+ return m_mimeType;
+}
+
void DownloadJob::update(const QJsonObject &payload)
{
auto end = payload.constEnd();
bool descriptionDirty = false;
auto it = payload.constFind(QStringLiteral("url"));
if (it != end) {
m_url = QUrl(it->toString());
descriptionDirty = true; // TODO only if actually changed
}
it = payload.constFind(QStringLiteral("finalUrl"));
if (it != end) {
m_finalUrl = QUrl(it->toString());
descriptionDirty = true;
}
it = payload.constFind(QStringLiteral("filename"));
if (it != end) {
m_fileName = it->toString();
const QUrl destination = QUrl::fromLocalFile(it->toString());
setProperty("destUrl", destination.toString(QUrl::RemoveFilename | QUrl::StripTrailingSlash));
m_destination = destination;
descriptionDirty = true;
}
it = payload.constFind(QStringLiteral("mime"));
if (it != end) {
m_mimeType = it->toString();
}
it = payload.constFind(QStringLiteral("incognito"));
if (it != end) {
m_incognito = it->toBool();
}
it = payload.constFind(QStringLiteral("totalBytes"));
if (it != end) {
const qlonglong totalAmount = it->toDouble();
if (totalAmount > -1) {
setTotalAmount(Bytes, totalAmount);
}
}
it = payload.constFind(QStringLiteral("bytesReceived"));
if (it != end) {
setProcessedAmount(Bytes, it->toDouble());
}
setTotalAmount(Files, 1);
it = payload.constFind(QStringLiteral("paused"));
if (it != end) {
const bool paused = it->toBool();
if (paused) {
suspend();
} else {
resume();
}
}
it = payload.constFind(QStringLiteral("estimatedEndTime"));
if (it != end) {
qulonglong speed = 0;
// now calculate the speed from estimated end time and total size
// funny how chrome only gives us a time whereas KJob operates on speed
// and calculates the time this way :)
const QDateTime endTime = QDateTime::fromString(it->toString(), Qt::ISODate);
if (endTime.isValid()) {
const QDateTime now = QDateTime::currentDateTimeUtc();
qulonglong remainingBytes = totalAmount(Bytes) - processedAmount(Bytes);
quint64 remainingTime = now.secsTo(endTime);
if (remainingTime > 0) {
speed = remainingBytes / remainingTime;
}
}
emitSpeed(speed);
}
if (descriptionDirty) {
updateDescription();
}
const QString error = payload.value(QStringLiteral("error")).toString();
if (!error.isEmpty()) {
if (error == QLatin1String("USER_CANCELED")
|| error == QLatin1String("USER_SHUTDOWN")) {
setError(KIO::ERR_USER_CANCELED); // will keep Notification applet from showing a "finished"/error message
emitResult();
return;
}
// value is a QVariant so we can be lazy and support both KIO errors and custom test
// if QVariant is an int: use that as KIO error
// if QVariant is a QString: set UserError and message
static const QHash errors {
// for a list of these error codes *and their meaning* instead of looking at browser
// extension docs, check out Chromium's source code: download_interrupt_reason_values.h
{QStringLiteral("FILE_ACCESS_DENIED"), i18n("Access denied.")}, // KIO::ERR_ACCESS_DENIED
{QStringLiteral("FILE_NO_SPACE"), i18n("Insufficient free space.")}, // KIO::ERR_DISK_FULL
{QStringLiteral("FILE_NAME_TOO_LONG"), i18n("The file name you have chosen is too long.")},
{QStringLiteral("FILE_TOO_LARGE"), i18n("The file is too large to be downloaded.")},
// haha
{QStringLiteral("FILE_VIRUS_INFECTED"), i18n("The file possibly contains malicious contents.")},
{QStringLiteral("FILE_TRANSIENT_ERROR"), i18n("A temporary error has occurred. Please try again later.")},
{QStringLiteral("NETWORK_FAILED"), i18n("A network error has occurred.")},
{QStringLiteral("NETWORK_TIMEOUT"), i18n("The network operation timed out.")}, // TODO something less geeky
{QStringLiteral("NETWORK_DISCONNECTED"), i18n("The network connection has been lost.")},
{QStringLiteral("NETWORK_SERVER_DOWN"), i18n("The server is no longer reachable.")},
{QStringLiteral("SERVER_FAILED"), i18n("A server error has occurred.")},
// chromium code says "internal use" and this is really not something the user should see
// SERVER_NO_RANGE"
// SERVER_PRECONDITION
{QStringLiteral("SERVER_BAD_CONTENT"), i18n("The server does not have the requested data.")},
{QStringLiteral("CRASH"), i18n("The browser application closed unexpectedly.")}
};
const QString &errorValue = errors.value(error);
if (errorValue.isEmpty()) { // unknown error
setError(KIO::ERR_UNKNOWN);
setErrorText(i18n("An unknown error occurred while downloading."));
emitResult();
return;
}
// KIO::Error doesn't have a UserDefined one, let's just use magic numbers then
// TODO at least set the KIO::Errors that we do have
setError(1000);
setErrorText(errorValue);
emitResult();
return;
}
it = payload.constFind(QStringLiteral("state"));
if (it != end) {
const QString state = it->toString();
// We ignore "interrupted" state and only cancel if we get supplied an "error"
if (state == QLatin1String("complete")) {
setError(KJob::NoError);
setProcessedAmount(KJob::Files, 1);
// Write origin url into extended file attributes
saveOriginUrl();
emitResult();
return;
}
}
}
void DownloadJob::updateDescription()
{
description(this, i18nc("Job heading, like 'Copying'", "Downloading"),
qMakePair(i18nc("The URL being downloaded", "Source"), (m_finalUrl.isValid() ? m_finalUrl : m_url).toDisplayString()),
qMakePair(i18nc("The location being downloaded to", "Destination"), m_destination.toLocalFile())
);
}
void DownloadJob::saveOriginUrl()
{
if (m_incognito
// Blob URLs are dynamically created through JavaScript and cannot be accessed from the outside
|| m_finalUrl.scheme() == QLatin1String("blob")) {
return;
}
const QJsonObject settings = Settings::self().settingsForPlugin(QStringLiteral("downloads"));
const bool saveOriginUrl = settings.value(QStringLiteral("saveOriginUrl")).toBool();
if (!saveOriginUrl) {
return;
}
KFileMetaData::UserMetaData md(m_fileName);
QUrl url = m_finalUrl;
url.setPassword(QString());
md.setOriginUrl(url);
}
diff --git a/host/downloadjob.h b/host/downloadjob.h
index 957b34e3..ed7ee677 100644
--- a/host/downloadjob.h
+++ b/host/downloadjob.h
@@ -1,79 +1,82 @@
/*
Copyright (C) 2017 by Kai Uwe Broulik
Copyright (C) 2017 by David Edmundson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
#pragma once
#include
#include
class DownloadJob : public KJob
{
Q_OBJECT
public:
DownloadJob(int id);
enum class State {
None,
InProgress,
Interrupted,
Complete
};
void start() override;
void update(const QJsonObject &payload);
+ QString fileName() const;
+ QString mimeType() const;
+
Q_SIGNALS:
void killRequested();
void suspendRequested();
void resumeRequested();
private Q_SLOTS:
void doStart();
protected:
bool doKill() override;
bool doSuspend() override;
bool doResume() override;
private:
void updateDescription();
void saveOriginUrl();
int m_id = -1;
QUrl m_url;
QUrl m_finalUrl;
QUrl m_destination;
QString m_fileName;
QString m_mimeType;
// In doubt, assume incognito
bool m_incognito = true;
};
diff --git a/host/downloadplugin.cpp b/host/downloadplugin.cpp
index a4e28b00..5746442c 100644
--- a/host/downloadplugin.cpp
+++ b/host/downloadplugin.cpp
@@ -1,116 +1,169 @@
/*
Copyright (C) 2017 by Kai Uwe Broulik
Copyright (C) 2017 by David Edmundson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
#include "downloadplugin.h"
#include "connection.h"
#include "downloadjob.h"
#include
#include
+#include
+#include
+
+#include
+
+#include "itineraryextractorjob.h"
+
DownloadPlugin::DownloadPlugin(QObject* parent) :
AbstractBrowserPlugin(QStringLiteral("downloads"), 2, parent)
{
}
bool DownloadPlugin::onLoad()
{
// Have extension tell us about all the downloads
sendData(QStringLiteral("createAll"));
return true;
}
bool DownloadPlugin::onUnload()
{
for (auto it = m_jobs.constBegin(), end = m_jobs.constEnd(); it != end; ++it) {
it.value()->deleteLater(); // kill() would abort the download
}
return true;
}
void DownloadPlugin::handleData(const QString& event, const QJsonObject& payload)
{
const QJsonObject &download = payload.value(QStringLiteral("download")).toObject();
const int id = download.value(QStringLiteral("id")).toInt(-1);
if (id < 0) {
qWarning() << "Cannot update download with invalid id" << id;
return;
}
if (event == QLatin1String("created")) {
// If we get a created event for an already existing job, update it instead
auto *job = m_jobs.value(id);
if (job) {
job->update(download);
return;
}
job = new DownloadJob(id);
// first register and then update, otherwise we miss the initial population..
KIO::getJobTracker()->registerJob(job);
job->update(download);
m_jobs.insert(id, job);
connect(job, &DownloadJob::killRequested, this, [this, id] {
sendData(QStringLiteral("cancel"), {
{QStringLiteral("downloadId"), id}
});
});
connect(job, &DownloadJob::suspendRequested, this, [this, id] {
sendData(QStringLiteral("suspend"), {
{QStringLiteral("downloadId"), id}
});
});
connect(job, &DownloadJob::resumeRequested, this, [this, id] {
sendData(QStringLiteral("resume"), {
{QStringLiteral("downloadId"), id}
});
});
QObject::connect(job, &QObject::destroyed, this, [this, id] {
m_jobs.remove(id);
});
- job->start();
+ QObject::connect(job, &KJob::result, this, [this, job] {
+ if (job->error()) {
+ return;
+ }
- QObject::connect(job, &KJob::finished, this, [this, job, id] {
+ // FIXME check if enabled in settings
+ if (job->mimeType() == QLatin1String("application/pdf")) {
+ extractItinerary(job->fileName());
+ }
});
+ job->start();
+
} else if (event == QLatin1String("update")) {
auto *job = m_jobs.value(id);
if (!job) {
debug() << "Failed to find download to update with id" << id;
return;
}
job->update(download);
}
}
+void DownloadPlugin::extractItinerary(const QString &fileName)
+{
+ auto *job = new ItineraryExtractorJob(fileName);
+ job->setInputType(ItineraryExtractorJob::InputType::Pdf);
+ job->start();
+
+ connect(job, &KJob::result, /*this*/ [job, fileName] {
+ if (job->error()) {
+ return;
+ }
+
+ const QJsonArray data = job->extractedData();
+ if (data.isEmpty()) {
+ return;
+ }
+
+ // HACK some nicer notification
+ const QString type = data.first().toObject().value(QStringLiteral("@type")).toString();
+
+ QString message = i18n("Would you like to add this file to KDE Itinerary?");
+ QString iconName = QStringLiteral("map-globe");
+ if (type == QLatin1String("FlightReservation")) {
+ message = i18n("Would you like to add this boarding pass to KDE Itinerary?");
+ iconName = QStringLiteral("document-send");
+ }
+
+ KNotification *noti = KNotification::event(KNotification::Notification,
+ i18n("Add to Itinerary"),
+ message,
+ iconName);
+ noti->setHint(QStringLiteral("desktop-entry"), QStringLiteral("org.kde.itinerary"));
+ noti->setActions({i18n("Add to Itinerary")});
+ connect(noti, &KNotification::action1Activated, [fileName] {
+ QProcess::startDetached(QStringLiteral("itinerary"), {fileName});
+ });
+ noti->sendEvent();
+ });
+}
diff --git a/host/downloadplugin.h b/host/downloadplugin.h
index e07e8046..f1cbc2a4 100644
--- a/host/downloadplugin.h
+++ b/host/downloadplugin.h
@@ -1,42 +1,44 @@
/*
Copyright (C) 2017 by Kai Uwe Broulik
Copyright (C) 2017 by David Edmundson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
#pragma once
#include "abstractbrowserplugin.h"
#include
#include "downloadjob.h"
class DownloadPlugin : public AbstractBrowserPlugin
{
Q_OBJECT
public:
explicit DownloadPlugin(QObject *parent);
bool onLoad() override;
bool onUnload() override;
using AbstractBrowserPlugin::handleData;
void handleData(const QString &event, const QJsonObject &data) override;
private:
+ void extractItinerary(const QString &fileName);
+
QHash m_jobs;
};
diff --git a/host/itineraryextractorjob.cpp b/host/itineraryextractorjob.cpp
new file mode 100644
index 00000000..867c8722
--- /dev/null
+++ b/host/itineraryextractorjob.cpp
@@ -0,0 +1,139 @@
+/*
+ Copyright (C) 2019 by Kai Uwe Broulik
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+*/
+
+#include "itineraryextractorjob.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+#include
+
+ItineraryExtractorJob::ItineraryExtractorJob(const QByteArray &inputData, QObject *parent)
+ : KJob(parent)
+ , m_inputData(inputData)
+{
+
+}
+
+ItineraryExtractorJob::ItineraryExtractorJob(const QString &fileName, QObject *parent)
+ : KJob(parent)
+ , m_fileName(fileName)
+{
+
+}
+
+ItineraryExtractorJob::~ItineraryExtractorJob() = default;
+
+bool ItineraryExtractorJob::isSupported()
+{
+ QFileInfo fi(extractorPath());
+ return fi.exists() && fi.isExecutable();
+}
+
+QString ItineraryExtractorJob::extractorPath()
+{
+ return QFile::decodeName(CMAKE_INSTALL_FULL_LIBEXECDIR_KF5 "/kitinerary-extractor");
+}
+
+ItineraryExtractorJob::InputType ItineraryExtractorJob::inputType() const
+{
+ return m_inputType;
+}
+
+void ItineraryExtractorJob::setInputType(ItineraryExtractorJob::InputType inputType)
+{
+ m_inputType = inputType;
+}
+
+QJsonArray ItineraryExtractorJob::extractedData() const
+{
+ return m_extractedData;
+}
+
+void ItineraryExtractorJob::start()
+{
+ QMetaObject::invokeMethod(this, &ItineraryExtractorJob::doStart, Qt::QueuedConnection);
+}
+
+void ItineraryExtractorJob::doStart()
+{
+ if (m_inputData.isEmpty() && m_fileName.isEmpty()) {
+ setError(KIO::ERR_UNKNOWN); // TODO proper error code
+ emitResult();
+ return;
+ }
+
+ QProcess *process = new QProcess(this);
+
+ connect(process, &QProcess::started, this, [this, process] {
+ if (m_inputData.isEmpty()) {
+ return;
+ }
+
+ process->write(m_inputData);
+ process->closeWriteChannel();
+ });
+
+ connect(process, QOverload::of(&QProcess::finished), this, [this, process](int exitCode, QProcess::ExitStatus exitStatus) {
+ process->deleteLater();
+
+ if (exitCode != 0 || exitStatus != QProcess::NormalExit) {
+ qWarning() << "Failed to extract itinerary information"; // TODO print error
+ setError(KIO::ERR_UNKNOWN); // TODO proper error code
+ emitResult();
+ return;
+ }
+
+ const QByteArray output = process->readAllStandardOutput();
+ const QJsonArray itineraryInfo = QJsonDocument::fromJson(output).array();
+
+ m_extractedData = QJsonDocument::fromJson(output).array();
+ emitResult();
+ });
+
+ QStringList args;
+ if (!m_fileName.isEmpty()) {
+ args << m_fileName;
+ }
+
+ if (m_inputType != InputType::Any) {
+ const QMetaEnum me = QMetaEnum::fromType();
+
+ const QString key = QString::fromUtf8(me.key(static_cast(m_inputType)));
+ if (key.isEmpty()) {
+ setError(KIO::ERR_UNKNOWN); // TODO proper error
+ emitResult();
+ return;
+ }
+
+ args << QLatin1String("-t") << key;
+ }
+
+ process->start(extractorPath(), args);
+}
diff --git a/host/downloadjob.h b/host/itineraryextractorjob.h
similarity index 61%
copy from host/downloadjob.h
copy to host/itineraryextractorjob.h
index 957b34e3..62c3c27c 100644
--- a/host/downloadjob.h
+++ b/host/itineraryextractorjob.h
@@ -1,79 +1,70 @@
/*
- Copyright (C) 2017 by Kai Uwe Broulik
- Copyright (C) 2017 by David Edmundson
+ Copyright (C) 2019 by Kai Uwe Broulik
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
#pragma once
#include
-#include
+#include
+#include
+#include
-class DownloadJob : public KJob
+class ItineraryExtractorJob : public KJob
{
Q_OBJECT
public:
- DownloadJob(int id);
-
- enum class State {
- None,
- InProgress,
- Interrupted,
- Complete
+ ItineraryExtractorJob(const QByteArray &inputData, QObject *parent = nullptr);
+ ItineraryExtractorJob(const QString &fileName, QObject *parent = nullptr);
+ ~ItineraryExtractorJob() override;
+
+ enum class InputType {
+ Any,
+ Email,
+ Pdf,
+ PkPass,
+ ICal,
+ Html
};
+ Q_ENUM(InputType)
- void start() override;
+ static bool isSupported();
- void update(const QJsonObject &payload);
+ InputType inputType() const;
+ void setInputType(InputType inputType);
-Q_SIGNALS:
- void killRequested();
- void suspendRequested();
- void resumeRequested();
+ QJsonArray extractedData() const;
+
+ void start() override;
private Q_SLOTS:
void doStart();
-protected:
- bool doKill() override;
- bool doSuspend() override;
- bool doResume() override;
-
private:
- void updateDescription();
- void saveOriginUrl();
-
- int m_id = -1;
-
- QUrl m_url;
- QUrl m_finalUrl;
-
- QUrl m_destination;
+ static QString extractorPath();
+ InputType m_inputType = InputType::Any;
+ QByteArray m_inputData;
QString m_fileName;
- QString m_mimeType;
-
- // In doubt, assume incognito
- bool m_incognito = true;
-
+ QJsonArray m_extractedData;
};
diff --git a/host/itineraryplugin.cpp b/host/itineraryplugin.cpp
new file mode 100644
index 00000000..9338f046
--- /dev/null
+++ b/host/itineraryplugin.cpp
@@ -0,0 +1,109 @@
+/*
+ Copyright (C) 2019 by Kai Uwe Broulik
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+*/
+
+#include "itineraryplugin.h"
+
+#include
+#include
+
+#include "itineraryextractorjob.h"
+
+ItineraryPlugin::ItineraryPlugin(QObject *parent)
+ : AbstractBrowserPlugin(QStringLiteral("itinerary"), 1, parent)
+ , m_supported(ItineraryExtractorJob::isSupported())
+{
+
+}
+
+ItineraryPlugin::~ItineraryPlugin()
+{
+
+}
+
+QJsonObject ItineraryPlugin::status() const
+{
+ return {
+ {QStringLiteral("extractorFound"), m_supported}
+ };
+}
+
+bool ItineraryPlugin::onLoad()
+{
+ // Check again on load, so reloading the plugin updates
+ m_supported = ItineraryExtractorJob::isSupported();
+ return m_supported;
+}
+
+bool ItineraryPlugin::onUnload()
+{
+ return true;
+}
+
+QJsonObject ItineraryPlugin::handleData(int serial, const QString &event, const QJsonObject &data)
+{
+ if (event == QLatin1String("extract")) {
+
+ const QString dataString = data.value(QStringLiteral("data")).toString();
+
+ QByteArray inputData;
+ if (dataString.startsWith(QLatin1String("data:"))) {
+ const int b64start = dataString.indexOf(QLatin1Char(','));
+ QByteArray b64 = dataString.rightRef(dataString.size() - b64start - 1).toLatin1();
+ inputData = QByteArray::fromBase64(b64);
+ } else {
+ inputData = dataString.toUtf8();
+ }
+
+ auto *job = new ItineraryExtractorJob(inputData);
+
+ const QString type = data.value(QStringLiteral("type")).toString();
+
+ const QMetaEnum me = QMetaEnum::fromType();
+ const auto inputType = static_cast(me.keyToValue(qUtf8Printable(type)));
+ job->setInputType(inputType);
+
+ connect(job, &KJob::result, this, [this, job, serial] {
+ if (job->error()) {
+ sendReply(serial, {
+ {QStringLiteral("success"), false},
+ {QStringLiteral("errorCode"), job->error()},
+ {QStringLiteral("errorMessage"), job->errorText()}
+ });
+ return;
+ }
+
+ sendReply(serial, {
+ {QStringLiteral("success"), true},
+ {QStringLiteral("data"), job->extractedData()}
+ });
+ });
+
+ job->start();
+
+ } else if (event == QLatin1String("run")) {
+
+ // TODO open file in Itinerary
+
+ }
+
+ return {};
+}
diff --git a/host/downloadplugin.h b/host/itineraryplugin.h
similarity index 76%
copy from host/downloadplugin.h
copy to host/itineraryplugin.h
index e07e8046..0908fd82 100644
--- a/host/downloadplugin.h
+++ b/host/itineraryplugin.h
@@ -1,42 +1,46 @@
/*
- Copyright (C) 2017 by Kai Uwe Broulik
- Copyright (C) 2017 by David Edmundson
+ Copyright (C) 2019 by Kai Uwe Broulik
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
#pragma once
#include "abstractbrowserplugin.h"
-#include
-#include "downloadjob.h"
-
-class DownloadPlugin : public AbstractBrowserPlugin
+class ItineraryPlugin : public AbstractBrowserPlugin
{
Q_OBJECT
+
public:
- explicit DownloadPlugin(QObject *parent);
+ explicit ItineraryPlugin(QObject *parent);
+ ~ItineraryPlugin() override;
+
+ QJsonObject status() const override;
+
bool onLoad() override;
bool onUnload() override;
+
using AbstractBrowserPlugin::handleData;
- void handleData(const QString &event, const QJsonObject &data) override;
+ QJsonObject handleData(int serial, const QString &event, const QJsonObject &data) override;
+
private:
- QHash m_jobs;
+ bool m_supported = false;
+
};
diff --git a/host/main.cpp b/host/main.cpp
index c44045df..9010d05a 100644
--- a/host/main.cpp
+++ b/host/main.cpp
@@ -1,100 +1,102 @@
/*
Copyright (C) 2017 by Kai Uwe Broulik
Copyright (C) 2017 by David Edmundson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
#include
#include
#include
#include
#include "connection.h"
#include "pluginmanager.h"
#include "abstractbrowserplugin.h"
#include "settings.h"
#include "kdeconnectplugin.h"
#include "downloadplugin.h"
#include "tabsrunnerplugin.h"
#include "mprisplugin.h"
#include "purposeplugin.h"
+#include "itineraryplugin.h"
void msgHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
Q_UNUSED(type);
Q_UNUSED(context);
QJsonObject data;
data[QStringLiteral("subsystem")] = QStringLiteral("debug");
switch(type) {
case QtDebugMsg:
case QtInfoMsg:
data[QStringLiteral("action")] = QStringLiteral("debug");
break;
default:
data[QStringLiteral("action")] = QStringLiteral("warning");
}
data[QStringLiteral("payload")] = QJsonObject({{QStringLiteral("message"), msg}});
Connection::self()->sendData(data);
}
int main(int argc, char *argv[])
{
// otherwise when logging out, session manager will ask the host to quit
// (it's a "regular X app" after all) and then the browser will complain
qunsetenv("SESSION_MANAGER");
QApplication a(argc, argv);
// otherwise will close when download job finishes
a.setQuitOnLastWindowClosed(false);
// applicationName etc will be set in Settings once the browser identifies to us
qInstallMessageHandler(msgHandler);
KCrash::initialize();
// NOTE if you add a new plugin here, make sure to adjust the
// "DEFAULT_EXTENSION_SETTINGS" in constants.js or else it won't
// even bother loading your shiny new plugin!
PluginManager::self().addPlugin(&Settings::self());
PluginManager::self().addPlugin(new KDEConnectPlugin(&a));
PluginManager::self().addPlugin(new DownloadPlugin(&a));
PluginManager::self().addPlugin(new TabsRunnerPlugin(&a));
PluginManager::self().addPlugin(new MPrisPlugin(&a));
PluginManager::self().addPlugin(new PurposePlugin(&a));
+ PluginManager::self().addPlugin(new ItineraryPlugin(&a));
// TODO make this prettier, also prevent unloading them at any cost
PluginManager::self().loadPlugin(&Settings::self());
QString serviceName = QStringLiteral("org.kde.plasma.browser_integration");
if (!QDBusConnection::sessionBus().registerService(serviceName)) {
// now try appending PID in case multiple hosts are running
serviceName.append(QLatin1String("-")).append(QString::number(QCoreApplication::applicationPid()));
if (!QDBusConnection::sessionBus().registerService(serviceName)) {
qWarning() << "Failed to register DBus service name" << serviceName;
}
}
return a.exec();
}