--- a/browser/extensions/screenshots/bootstrap.js
+++ b/browser/extensions/screenshots/bootstrap.js
@@ -1,15 +1,14 @@
/* globals ADDON_DISABLE */
-const OLD_ADDON_PREF_NAME = "extensions.jid1-NeEaf3sAHdKHPA@jetpack.deviceIdInfo";
-const OLD_ADDON_ID = "jid1-NeEaf3sAHdKHPA@jetpack";
const ADDON_ID = "screenshots@mozilla.org";
const TELEMETRY_ENABLED_PREF = "datareporting.healthreport.uploadEnabled";
const PREF_BRANCH = "extensions.screenshots.";
const USER_DISABLE_PREF = "extensions.screenshots.disabled";
+const UPLOAD_DISABLED_PREF = "extensions.screenshots.upload-disabled";
const HISTORY_ENABLED_PREF = "places.history.enabled";
const { interfaces: Ci, utils: Cu } = Components;
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.defineModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
ChromeUtils.defineModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
@@ -205,43 +204,35 @@ function stop(webExtension, reason) {
return Promise.resolve(webExtension.shutdown(reason));
}
function handleMessage(msg, sender, sendReply) {
if (!msg) {
return;
}
- if (msg.funcName === "getTelemetryPref") {
+ if (msg.funcName === "isTelemetryEnabled") {
let telemetryEnabled = getBoolPref(TELEMETRY_ENABLED_PREF);
sendReply({type: "success", value: telemetryEnabled});
- } else if (msg.funcName === "getOldDeviceInfo") {
- let oldDeviceInfo = prefs.prefHasUserValue(OLD_ADDON_PREF_NAME) && prefs.getCharPref(OLD_ADDON_PREF_NAME);
- sendReply({type: "success", value: oldDeviceInfo || null});
- } else if (msg.funcName === "removeOldAddon") {
- AddonManager.getAddonByID(OLD_ADDON_ID, (addon) => {
- prefs.clearUserPref(OLD_ADDON_PREF_NAME);
- if (addon) {
- addon.uninstall();
- }
- sendReply({type: "success", value: !!addon});
- });
- return true;
- } else if (msg.funcName === "getHistoryPref") {
+ } else if (msg.funcName === "isUploadDisabled") {
+ let isESR = AppConstants.MOZ_UPDATE_CHANNEL === 'esr';
+ let uploadDisabled = getBoolPref(UPLOAD_DISABLED_PREF);
+ sendReply({type: "success", value: uploadDisabled || isESR});
+ } else if (msg.funcName === "isHistoryEnabled") {
let historyEnabled = getBoolPref(HISTORY_ENABLED_PREF);
sendReply({type: "success", value: historyEnabled});
- } else if (msg.funcName === "incrementDownloadCount") {
- Services.telemetry.scalarAdd('screenshots.download', 1);
- sendReply({type: "success", value: true});
- } else if (msg.funcName === "incrementUploadCount") {
- Services.telemetry.scalarAdd('screenshots.upload', 1);
- sendReply({type: "success", value: true});
- } else if (msg.funcName === "incrementCopyCount") {
- Services.telemetry.scalarAdd('screenshots.copy', 1);
- sendReply({type: "success", value: true});
+ } else if (msg.funcName === "incrementCount") {
+ let allowedScalars = ["download", "upload", "copy"];
+ let scalar = msg.args && msg.args[0] && msg.args[0].scalar;
+ if (!allowedScalars.includes(scalar)) {
+ sendReply({type: "error", name: `incrementCount passed an unrecognized scalar ${scalar}`});
+ } else {
+ Services.telemetry.scalarAdd(`screenshots.${scalar}`, 1);
+ sendReply({type: "success", value: true});
+ }
}
}
let photonPageAction;
// If the current Firefox version supports Photon (57 and later), this sets up
// a Photon page action and removes the UI for the WebExtension browser action.
// Does nothing otherwise. Ideally, in the future, WebExtension page actions
--- a/browser/extensions/screenshots/install.rdf
+++ b/browser/extensions/screenshots/install.rdf
@@ -7,14 +7,14 @@
<em:targetApplication>
<Description>
<em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> <!--Firefox-->
<em:minVersion>57.0a1</em:minVersion>
<em:maxVersion>*</em:maxVersion>
</Description>
</em:targetApplication>
<em:type>2</em:type>
- <em:version>25.0.0</em:version>
+ <em:version>29.0.0</em:version>
<em:bootstrap>true</em:bootstrap>
<em:homepageURL>https://screenshots.firefox.com/</em:homepageURL>
<em:multiprocessCompatible>true</em:multiprocessCompatible>
</Description>
</RDF>
--- a/browser/extensions/screenshots/moz.build
+++ b/browser/extensions/screenshots/moz.build
@@ -374,17 +374,18 @@ FINAL_TARGET_FILES.features['screenshots
]
FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"]["build"] += [
'webextension/build/buildSettings.js',
'webextension/build/inlineSelectionCss.js',
'webextension/build/onboardingCss.js',
'webextension/build/onboardingHtml.js',
'webextension/build/raven.js',
- 'webextension/build/shot.js'
+ 'webextension/build/shot.js',
+ 'webextension/build/thumbnailGenerator.js'
]
FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"]["icons"] += [
'webextension/icons/back-highlight.svg',
'webextension/icons/back.svg',
'webextension/icons/cancel.svg',
'webextension/icons/cloud.svg',
'webextension/icons/copy.png',
--- a/browser/extensions/screenshots/webextension/background/analytics.js
+++ b/browser/extensions/screenshots/webextension/background/analytics.js
@@ -1,124 +1,160 @@
/* globals main, auth, catcher, deviceInfo, communication, log */
"use strict";
this.analytics = (function() {
let exports = {};
let telemetryPrefKnown = false;
- let telemetryPref;
+ let telemetryEnabled;
+
+ const EVENT_BATCH_DURATION = 1000; // ms for setTimeout
+ let pendingEvents = [];
+ let pendingTimings = [];
+ let eventsTimeoutHandle, timingsTimeoutHandle;
+ const fetchOptions = {
+ method: "POST",
+ mode: "cors",
+ headers: { "content-type": "application/json" },
+ credentials: "include"
+ };
+
+ function flushEvents() {
+ if (pendingEvents.length === 0) {
+ return;
+ }
+
+ let eventsUrl = `${main.getBackend()}/event`;
+ let deviceId = auth.getDeviceId();
+ let sendTime = Date.now();
+
+ pendingEvents.forEach(event => {
+ event.queueTime = sendTime - event.eventTime
+ log.info(`sendEvent ${event.event}/${event.action}/${event.label || 'none'} ${JSON.stringify(event.options)}`);
+ });
+
+ let body = JSON.stringify({deviceId, events: pendingEvents});
+ let fetchRequest = fetch(eventsUrl, Object.assign({body}, fetchOptions));
+ fetchWatcher(fetchRequest);
+ pendingEvents = [];
+ }
+
+ function flushTimings() {
+ if (pendingTimings.length === 0) {
+ return;
+ }
+
+ let timingsUrl = `${main.getBackend()}/timing`;
+ let deviceId = auth.getDeviceId();
+ let body = JSON.stringify({deviceId, timings: pendingTimings});
+ let fetchRequest = fetch(timingsUrl, Object.assign({body}, fetchOptions));
+ fetchWatcher(fetchRequest);
+ pendingTimings.forEach(t => {
+ log.info(`sendTiming ${t.timingCategory}/${t.timingLabel}/${t.timingVar}: ${t.timingValue}`);
+ });
+ pendingTimings = [];
+ }
function sendTiming(timingLabel, timingVar, timingValue) {
// sendTiming is only called in response to sendEvent, so no need to check
// the telemetry pref again here.
let timingCategory = "addon";
- return new Promise((resolve, reject) => {
- let url = main.getBackend() + "/timing";
- let req = new XMLHttpRequest();
- req.open("POST", url);
- req.setRequestHeader("content-type", "application/json");
- req.onload = catcher.watchFunction(() => {
- if (req.status >= 300) {
- let exc = new Error("Bad response from POST /timing");
- exc.status = req.status;
- exc.statusText = req.statusText;
- reject(exc);
- } else {
- resolve();
- }
- });
- log.info(`sendTiming ${timingCategory}/${timingLabel}/${timingVar}: ${timingValue}`);
- req.send(JSON.stringify({
- deviceId: auth.getDeviceId(),
- timingCategory,
- timingLabel,
- timingVar,
- timingValue
- }));
+ pendingTimings.push({
+ timingCategory,
+ timingLabel,
+ timingVar,
+ timingValue
});
+ if (!timingsTimeoutHandle) {
+ timingsTimeoutHandle = setTimeout(() => {
+ timingsTimeoutHandle = null;
+ flushTimings();
+ }, EVENT_BATCH_DURATION);
+ }
}
exports.sendEvent = function(action, label, options) {
let eventCategory = "addon";
if (!telemetryPrefKnown) {
log.warn("sendEvent called before we were able to refresh");
return Promise.resolve();
}
- if (!telemetryPref) {
+ if (!telemetryEnabled) {
log.info(`Cancelled sendEvent ${eventCategory}/${action}/${label || 'none'} ${JSON.stringify(options)}`);
return Promise.resolve();
}
measureTiming(action, label);
// Internal-only events are used for measuring time between events,
// but aren't submitted to GA.
if (action === 'internal') {
return Promise.resolve();
}
if (typeof label == "object" && (!options)) {
options = label;
label = undefined;
}
options = options || {};
+
+ // Don't send events if in private browsing.
+ if (options.incognito) {
+ return Promise.resolve();
+ }
+
+ // Don't include in event data.
+ delete options.incognito;
+
let di = deviceInfo();
- return new Promise((resolve, reject) => {
- let url = main.getBackend() + "/event";
- let req = new XMLHttpRequest();
- req.open("POST", url);
- req.setRequestHeader("content-type", "application/json");
- req.onload = catcher.watchFunction(() => {
- if (req.status >= 300) {
- let exc = new Error("Bad response from POST /event");
- exc.status = req.status;
- exc.statusText = req.statusText;
- reject(exc);
- } else {
- resolve();
- }
- });
- options.applicationName = di.appName;
- options.applicationVersion = di.addonVersion;
- let abTests = auth.getAbTests();
- for (let [gaField, value] of Object.entries(abTests)) {
- options[gaField] = value;
- }
- log.info(`sendEvent ${eventCategory}/${action}/${label || 'none'} ${JSON.stringify(options)}`);
- req.send(JSON.stringify({
- deviceId: auth.getDeviceId(),
- event: eventCategory,
- action,
- label,
- options
- }));
+ options.applicationName = di.appName;
+ options.applicationVersion = di.addonVersion;
+ let abTests = auth.getAbTests();
+ for (let [gaField, value] of Object.entries(abTests)) {
+ options[gaField] = value;
+ }
+ pendingEvents.push({
+ eventTime: Date.now(),
+ event: eventCategory,
+ action,
+ label,
+ options
});
+ if (!eventsTimeoutHandle) {
+ eventsTimeoutHandle = setTimeout(() => {
+ eventsTimeoutHandle = null;
+ flushEvents();
+ }, EVENT_BATCH_DURATION);
+ }
+ // This function used to return a Promise that was not used at any of the
+ // call sites; doing this simply maintains that interface.
+ return Promise.resolve();
};
exports.refreshTelemetryPref = function() {
- return communication.sendToBootstrap("getTelemetryPref").then((result) => {
+ return communication.sendToBootstrap("isTelemetryEnabled").then((result) => {
telemetryPrefKnown = true;
if (result === communication.NO_BOOTSTRAP) {
- telemetryPref = true;
+ telemetryEnabled = true;
} else {
- telemetryPref = result;
+ telemetryEnabled = result;
}
}, (error) => {
// If there's an error reading the pref, we should assume that we shouldn't send data
telemetryPrefKnown = true;
- telemetryPref = false;
+ telemetryEnabled = false;
throw error;
});
};
- exports.getTelemetryPrefSync = function() {
+ exports.isTelemetryEnabled = function() {
catcher.watchPromise(exports.refreshTelemetryPref());
- return !!telemetryPref;
+ return telemetryEnabled;
};
- let timingData = {};
+ let timingData = new Map();
// Configuration for filtering the sendEvent stream on start/end events.
// When start or end events occur, the time is recorded.
// When end events occur, the elapsed time is calculated and submitted
// via `sendEvent`, where action = "perf-response-time", label = name of rule,
// and cd1 value is the elapsed time in milliseconds.
// If a cancel event happens between the start and end events, the start time
// is deleted.
@@ -192,28 +228,40 @@ this.analytics = (function() {
// Match a filter (action and optional label) against an action and label.
function match(filter, action, label) {
return filter.label ?
filter.action === action && filter.label === label :
filter.action === action;
}
function anyMatches(filters, action, label) {
- return !!filters.find(filter => match(filter, action, label));
+ return filters.some(filter => match(filter, action, label));
}
function measureTiming(action, label) {
rules.forEach(r => {
if (anyMatches(r.cancel, action, label)) {
delete timingData[r.name];
} else if (match(r.start, action, label)) {
timingData[r.name] = Date.now();
} else if (timingData[r.name] && match(r.end, action, label)) {
let endTime = Date.now();
let elapsed = endTime - timingData[r.name];
- catcher.watchPromise(sendTiming("perf-response-time", r.name, elapsed), true);
+ sendTiming("perf-response-time", r.name, elapsed);
delete timingData[r.name];
}
});
}
+ function fetchWatcher(request) {
+ catcher.watchPromise(
+ request.then(response => {
+ if (!response.ok) {
+ throw new Error(`Bad response from ${request.url}: ${response.status} ${response.statusText}`);
+ }
+ return response;
+ }),
+ true
+ );
+ }
+
return exports;
})();
--- a/browser/extensions/screenshots/webextension/background/auth.js
+++ b/browser/extensions/screenshots/webextension/background/auth.js
@@ -171,38 +171,16 @@ this.auth = (function() {
exports.getAbTests = function() {
return abTests;
};
exports.isRegistered = function() {
return registrationInfo.registered;
};
- exports.setDeviceInfoFromOldAddon = function(newDeviceInfo) {
- return registrationInfoFetched.then(() => {
- if (!(newDeviceInfo.deviceId && newDeviceInfo.secret)) {
- throw new Error("Bad deviceInfo");
- }
- if (registrationInfo.deviceId === newDeviceInfo.deviceId &&
- registrationInfo.secret === newDeviceInfo.secret) {
- // Probably we already imported the information
- return Promise.resolve(false);
- }
- registrationInfo = {
- deviceId: newDeviceInfo.deviceId,
- secret: newDeviceInfo.secret,
- registered: true
- };
- initialized = false;
- return browser.storage.local.set({registrationInfo}).then(() => {
- return true;
- });
- });
- };
-
communication.register("getAuthInfo", (sender, ownershipCheck) => {
return registrationInfoFetched.then(() => {
return exports.authHeaders();
}).then((authHeaders) => {
let info = registrationInfo;
if (info.registered) {
return login({ownershipCheck}).then((result) => {
return {
--- a/browser/extensions/screenshots/webextension/background/communication.js
+++ b/browser/extensions/screenshots/webextension/background/communication.js
@@ -58,17 +58,16 @@ this.communication = (function() {
if (isBootstrapMissingError(error)) {
return exports.NO_BOOTSTRAP;
}
throw error;
});
};
function isBootstrapMissingError(error) {
- // Note: some of this logic is copied into startBackground.js's getOldDeviceInfo call
if (!error) {
return false;
}
return ('errorCode' in error && error.errorCode === "NO_RECEIVING_END") ||
(!error.errorCode && error.message === "Could not establish connection. Receiving end does not exist.");
}
--- a/browser/extensions/screenshots/webextension/background/main.js
+++ b/browser/extensions/screenshots/webextension/background/main.js
@@ -75,52 +75,52 @@ this.main = (function() {
});
}
function startSelectionWithOnboarding(tab) {
return analytics.refreshTelemetryPref().then(() => {
return selectorLoader.testIfLoaded(tab.id);
}).then((isLoaded) => {
if (!isLoaded) {
- sendEvent("start-shot", "site-request");
+ sendEvent("start-shot", "site-request", {incognito: tab.incognito});
setIconActive(true, tab.id);
selectorLoader.toggle(tab.id, false);
}
});
}
function shouldOpenMyShots(url) {
return /^about:(?:newtab|blank|home)/i.test(url) || /^resource:\/\/activity-streams\//i.test(url);
}
// This is called by startBackground.js, directly in response to clicks on the Photon page action
exports.onClicked = catcher.watchFunction((tab) => {
if (shouldOpenMyShots(tab.url)) {
if (!hasSeenOnboarding) {
catcher.watchPromise(analytics.refreshTelemetryPref().then(() => {
- sendEvent("goto-onboarding", "selection-button");
+ sendEvent("goto-onboarding", "selection-button", {incognito: tab.incognito});
return forceOnboarding();
}));
return;
}
catcher.watchPromise(analytics.refreshTelemetryPref().then(() => {
- sendEvent("goto-myshots", "about-newtab");
+ sendEvent("goto-myshots", "about-newtab", {incognito: tab.incognito});
}));
catcher.watchPromise(
auth.authHeaders()
.then(() => browser.tabs.update({url: backend + "/shots"})));
} else {
catcher.watchPromise(
toggleSelector(tab)
.then(active => {
const event = active ? "start-shot" : "cancel-shot";
- sendEvent(event, "toolbar-button");
+ sendEvent(event, "toolbar-button", {incognito: tab.incognito});
}, (error) => {
if ((!hasSeenOnboarding) && error.popupMessage == "UNSHOOTABLE_PAGE") {
- sendEvent("goto-onboarding", "selection-button");
+ sendEvent("goto-onboarding", "selection-button", {incognito: tab.incognito});
return forceOnboarding();
}
throw error;
}));
}
});
function forceOnboarding() {
@@ -135,17 +135,17 @@ this.main = (function() {
if (!urlEnabled(tab.url)) {
senderror.showError({
popupMessage: "UNSHOOTABLE_PAGE"
});
return;
}
catcher.watchPromise(
toggleSelector(tab)
- .then(() => sendEvent("start-shot", "context-menu")));
+ .then(() => sendEvent("start-shot", "context-menu", {incognito: tab.incognito})));
});
function urlEnabled(url) {
if (shouldOpenMyShots(url)) {
return true;
}
if (isShotOrMyShotPage(url) || /^(?:about|data|moz-extension):/i.test(url) || isBlacklistedUrl(url)) {
return false;
@@ -204,17 +204,17 @@ this.main = (function() {
});
}
});
communication.register("copyShotToClipboard", (sender, blob) => {
return blobConverters.blobToArray(blob).then(buffer => {
return browser.clipboard.setImageData(
buffer, blob.type.split("/", 2)[1]).then(() => {
- catcher.watchPromise(communication.sendToBootstrap('incrementCopyCount'));
+ catcher.watchPromise(communication.sendToBootstrap("incrementCount", {scalar: "copy"}));
return browser.notifications.create({
type: "basic",
iconUrl: "../icons/copy.png",
title: browser.i18n.getMessage("notificationImageCopiedTitle"),
message: browser.i18n.getMessage("notificationImageCopiedDetails", pasteSymbol)
});
});
})
@@ -231,101 +231,55 @@ this.main = (function() {
return;
}
if (change.state && change.state.current != "in_progress") {
URL.revokeObjectURL(url);
browser.downloads.onChanged.removeListener(onChangedCallback);
}
});
browser.downloads.onChanged.addListener(onChangedCallback)
- catcher.watchPromise(communication.sendToBootstrap("incrementDownloadCount"));
+ catcher.watchPromise(communication.sendToBootstrap("incrementCount", {scalar: "download"}));
return browser.windows.getLastFocused().then(windowInfo => {
- return windowInfo.incognito;
- }).then((incognito) => {
return browser.downloads.download({
url,
- incognito,
+ incognito: windowInfo.incognito,
filename: info.filename
}).then((id) => {
downloadId = id;
});
});
});
communication.register("closeSelector", (sender) => {
setIconActive(false, sender.tab.id);
});
- catcher.watchPromise(communication.sendToBootstrap("getOldDeviceInfo").then((deviceInfo) => {
- if (deviceInfo === communication.NO_BOOTSTRAP || !deviceInfo) {
- return;
- }
- deviceInfo = JSON.parse(deviceInfo);
- if (deviceInfo && typeof deviceInfo == "object") {
- return auth.setDeviceInfoFromOldAddon(deviceInfo).then((updated) => {
- if (updated === communication.NO_BOOTSTRAP) {
- throw new Error("bootstrap.js disappeared unexpectedly");
- }
- if (updated) {
- return communication.sendToBootstrap("removeOldAddon");
- }
- });
- }
- }));
-
communication.register("hasSeenOnboarding", () => {
hasSeenOnboarding = true;
catcher.watchPromise(browser.storage.local.set({hasSeenOnboarding}));
setIconActive(false, null);
startBackground.photonPageActionPort.postMessage({
type: "setProperties",
title: browser.i18n.getMessage("contextMenuLabel")
});
});
- communication.register("abortFrameset", () => {
- sendEvent("abort-start-shot", "frame-page");
- // Note, we only show the error but don't report it, as we know that we can't
- // take shots of these pages:
- senderror.showError({
- popupMessage: "UNSHOOTABLE_PAGE"
- });
- });
-
- communication.register("abortNoDocumentBody", (sender, tagName) => {
- tagName = String(tagName || "").replace(/[^a-z0-9]/ig, "");
- sendEvent("abort-start-shot", `document-is-${tagName}`);
+ communication.register("abortStartShot", () => {
// Note, we only show the error but don't report it, as we know that we can't
// take shots of these pages:
senderror.showError({
popupMessage: "UNSHOOTABLE_PAGE"
});
});
- // Note: this signal is only needed until bug 1357589 is fixed.
- communication.register("openTermsPage", () => {
- return catcher.watchPromise(browser.tabs.create({url: "https://www.mozilla.org/about/legal/terms/services/"}));
- });
-
- // Note: this signal is also only needed until bug 1357589 is fixed.
- communication.register("openPrivacyPage", () => {
- return catcher.watchPromise(browser.tabs.create({url: "https://www.mozilla.org/privacy/firefox-cloud/"}));
- });
-
// A Screenshots page wants us to start/force onboarding
communication.register("requestOnboarding", (sender) => {
return startSelectionWithOnboarding(sender.tab);
});
- communication.register("isHistoryEnabled", () => {
- return catcher.watchPromise(communication.sendToBootstrap("getHistoryPref").then(historyEnabled => {
- return historyEnabled;
- }));
- });
-
communication.register("getPlatformOs", () => {
return catcher.watchPromise(browser.runtime.getPlatformInfo().then(platformInfo => {
return platformInfo.os;
}));
});
return exports;
})();
--- a/browser/extensions/screenshots/webextension/background/selectorLoader.js
+++ b/browser/extensions/screenshots/webextension/background/selectorLoader.js
@@ -83,25 +83,27 @@ this.selectorLoader = (function() {
loadingTabs.delete(tabId);
throw error;
});
};
// TODO: since bootstrap communication is now required, would this function
// make more sense inside background/main?
function downloadOnlyCheck(tabId) {
- return communication.sendToBootstrap("getHistoryPref").then((historyEnabled) => {
- return browser.tabs.get(tabId).then(tab => {
- let downloadOnly = !historyEnabled || tab.incognito;
- return browser.tabs.executeScript(tabId, {
- // Note: `window` here refers to a global accessible to content
- // scripts, but not the scripts in the underlying page. For more
- // details, see https://mdn.io/WebExtensions/Content_scripts#Content_script_environment
- code: `window.downloadOnly = ${downloadOnly}`,
- runAt: "document_start"
+ return communication.sendToBootstrap("isHistoryEnabled").then((historyEnabled) => {
+ return communication.sendToBootstrap("isUploadDisabled").then((uploadDisabled) => {
+ return browser.tabs.get(tabId).then(tab => {
+ let downloadOnly = !historyEnabled || uploadDisabled || tab.incognito;
+ return browser.tabs.executeScript(tabId, {
+ // Note: `window` here refers to a global accessible to content
+ // scripts, but not the scripts in the underlying page. For more
+ // details, see https://mdn.io/WebExtensions/Content_scripts#Content_script_environment
+ code: `window.downloadOnly = ${downloadOnly}`,
+ runAt: "document_start"
+ });
});
});
});
}
function executeModules(tabId, scripts) {
let lastPromise = Promise.resolve(null);
scripts.forEach((file) => {
--- a/browser/extensions/screenshots/webextension/background/senderror.js
+++ b/browser/extensions/screenshots/webextension/background/senderror.js
@@ -83,17 +83,17 @@ this.senderror = (function() {
// FIXME: need iconUrl for an image, see #2239
title,
message
});
}
};
exports.reportError = function(e) {
- if (!analytics.getTelemetryPrefSync()) {
+ if (!analytics.isTelemetryEnabled()) {
log.error("Telemetry disabled. Not sending critical error:", e);
return;
}
let dsn = auth.getSentryPublicDSN();
if (!dsn) {
log.warn("Screenshots error:", e);
return;
}
--- a/browser/extensions/screenshots/webextension/background/startBackground.js
+++ b/browser/extensions/screenshots/webextension/background/startBackground.js
@@ -17,25 +17,23 @@ this.startBackground = (function() {
"catcher.js",
"blobConverters.js",
"background/selectorLoader.js",
"background/communication.js",
"background/auth.js",
"background/senderror.js",
"build/raven.js",
"build/shot.js",
+ "build/thumbnailGenerator.js",
"background/analytics.js",
"background/deviceInfo.js",
"background/takeshot.js",
"background/main.js"
];
- // Maximum milliseconds to wait before checking for migration possibility
- const CHECK_MIGRATION_DELAY = 2000;
-
browser.contextMenus.create({
id: "create-screenshot",
title: browser.i18n.getMessage("contextMenuLabel"),
contexts: ["page"],
documentUrlPatterns: ["<all_urls>"]
});
browser.contextMenus.onClicked.addListener((info, tab) => {
@@ -53,40 +51,16 @@ this.startBackground = (function() {
console.error("Error loading Screenshots:", error);
});
return true;
});
let photonPageActionPort = null;
initPhotonPageAction();
- // We delay this check (by CHECK_MIGRATION_DELAY) just to avoid piling too
- // many things onto browser/add-on startup
- requestIdleCallback(() => {
- browser.runtime.sendMessage({funcName: "getOldDeviceInfo"}).then((result) => {
- if (result && result.type == "success" && result.value) {
- // There is a possible migration to run, so we'll load the entire background
- // page and continue the process
- return loadIfNecessary();
- }
- if (!result) {
- throw new Error("Got no result from getOldDeviceInfo");
- }
- if (result.type == "error") {
- throw new Error(`Error from getOldDeviceInfo: ${result.name}`);
- }
- }).catch((error) => {
- if (error && error.message == "Could not establish connection. Receiving end does not exist") {
- // Just a missing bootstrap.js, ignore
- } else {
- console.error("Screenshots error checking for Page Shot migration:", error);
- }
- });
- }, {timeout: CHECK_MIGRATION_DELAY});
-
let loadedPromise;
function loadIfNecessary() {
if (loadedPromise) {
return loadedPromise;
}
loadedPromise = Promise.resolve();
backgroundScripts.forEach((script) => {
--- a/browser/extensions/screenshots/webextension/background/takeshot.js
+++ b/browser/extensions/screenshots/webextension/background/takeshot.js
@@ -1,23 +1,24 @@
-/* globals communication, shot, main, auth, catcher, analytics, buildSettings, blobConverters */
+/* globals communication, shot, main, auth, catcher, analytics, buildSettings, blobConverters, thumbnailGenerator */
"use strict";
this.takeshot = (function() {
let exports = {};
const Shot = shot.AbstractShot;
const { sendEvent } = analytics;
communication.register("takeShot", catcher.watchFunction((sender, options) => {
let { captureType, captureText, scroll, selectedPos, shotId, shot, imageBlob } = options;
shot = new Shot(main.getBackend(), shotId, shot);
shot.favicon = sender.tab.favIconUrl;
let capturePromise = Promise.resolve();
let openedTab;
+ let thumbnailBlob;
if (!shot.clipNames().length) {
// canvas.drawWindow isn't available, so we fall back to captureVisibleTab
capturePromise = screenshotPage(selectedPos, scroll).then((dataUrl) => {
shot.addClip({
createdDate: Date.now(),
image: {
url: "data:",
captureType,
@@ -28,18 +29,19 @@ this.takeshot = (function() {
y: selectedPos.bottom - selectedPos.top
}
}
});
});
}
let convertBlobPromise = Promise.resolve();
if (buildSettings.uploadBinary && !imageBlob) {
- imageBlob = blobConverters.dataUrlToBlob(shot.getClip(shot.clipNames()[0]).image.url);
- shot.getClip(shot.clipNames()[0]).image.url = "";
+ let clipImage = shot.getClip(shot.clipNames()[0]).image;
+ imageBlob = blobConverters.dataUrlToBlob(clipImage.url);
+ clipImage.url = "";
} else if (!buildSettings.uploadBinary && imageBlob) {
convertBlobPromise = blobConverters.blobToDataUrl(imageBlob).then((dataUrl) => {
shot.getClip(shot.clipNames()[0]).image.url = dataUrl;
});
imageBlob = null;
}
let shotAbTests = {};
let abTests = auth.getAbTests();
@@ -49,36 +51,48 @@ this.takeshot = (function() {
}
}
if (Object.keys(shotAbTests).length) {
shot.abTests = shotAbTests;
}
return catcher.watchPromise(capturePromise.then(() => {
return convertBlobPromise;
}).then(() => {
+ if (buildSettings.uploadBinary) {
+ let blobToUrlPromise = blobConverters.blobToDataUrl(imageBlob);
+ return thumbnailGenerator.createThumbnailBlobFromPromise(shot, blobToUrlPromise);
+ }
+ return thumbnailGenerator.createThumbnailUrl(shot);
+ }).then((thumbnailImage) => {
+ if (buildSettings.uploadBinary) {
+ thumbnailBlob = thumbnailImage;
+ } else {
+ shot.thumbnail = thumbnailImage;
+ }
+ }).then(() => {
return browser.tabs.create({url: shot.creatingUrl})
}).then((tab) => {
openedTab = tab;
sendEvent('internal', 'open-shot-tab');
- return uploadShot(shot, imageBlob);
+ return uploadShot(shot, imageBlob, thumbnailBlob);
}).then(() => {
- return browser.tabs.update(openedTab.id, {url: shot.viewUrl}).then(
+ return browser.tabs.update(openedTab.id, {url: shot.viewUrl, loadReplace: true}).then(
null,
(error) => {
// FIXME: If https://bugzilla.mozilla.org/show_bug.cgi?id=1365718 is resolved,
// use the errorCode added as an additional check:
if ((/invalid tab id/i).test(error)) {
// This happens if the tab was closed before the upload completed
return browser.tabs.create({url: shot.viewUrl});
}
throw error;
}
);
}).then(() => {
- catcher.watchPromise(communication.sendToBootstrap('incrementUploadCount'));
+ catcher.watchPromise(communication.sendToBootstrap("incrementCount", {scalar: "upload"}));
return shot.viewUrl;
}).catch((error) => {
browser.tabs.remove(openedTab.id);
throw error;
}));
}));
communication.register("screenshotPage", (sender, selectedPos, scroll) => {
@@ -126,57 +140,75 @@ this.takeshot = (function() {
function concatBuffers(buffer1, buffer2) {
var tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
tmp.set(new Uint8Array(buffer1), 0);
tmp.set(new Uint8Array(buffer2), buffer1.byteLength);
return tmp.buffer;
}
/** Creates a multipart TypedArray, given {name: value} fields
- and {name: blob} files
+ and a files array in the format of
+ [{fieldName: "NAME", filename: "NAME.png", blob: fileBlob}, {...}, ...]
Returns {body, "content-type"}
*/
- function createMultipart(fields, fileField, fileFilename, blob) {
+ function createMultipart(fields, files) {
let boundary = "---------------------------ScreenshotBoundary" + Date.now();
- return blobConverters.blobToArray(blob).then((blobAsBuffer) => {
- let body = [];
- for (let name in fields) {
- body.push("--" + boundary);
- body.push(`Content-Disposition: form-data; name="${name}"`);
- body.push("");
- body.push(fields[name]);
+ let body = [];
+ for (let name in fields) {
+ body.push("--" + boundary);
+ body.push(`Content-Disposition: form-data; name="${name}"`);
+ body.push("");
+ body.push(fields[name]);
+ }
+ body.push("");
+ body = body.join("\r\n");
+ let enc = new TextEncoder("utf-8");
+ body = enc.encode(body).buffer;
+
+ let blobToArrayPromises = files.map(f => {
+ return blobConverters.blobToArray(f.blob);
+ });
+
+ return Promise.all(blobToArrayPromises).then(buffers => {
+ for (let i = 0; i < buffers.length; i++) {
+ let filePart = [];
+ filePart.push("--" + boundary);
+ filePart.push(`Content-Disposition: form-data; name="${files[i].fieldName}"; filename="${files[i].filename}"`);
+ filePart.push(`Content-Type: ${files[i].blob.type}`);
+ filePart.push("");
+ filePart.push("");
+ filePart = filePart.join("\r\n");
+ filePart = concatBuffers(enc.encode(filePart).buffer, buffers[i]);
+ body = concatBuffers(body, filePart);
+ body = concatBuffers(body, enc.encode("\r\n").buffer);
}
- body.push("--" + boundary);
- body.push(`Content-Disposition: form-data; name="${fileField}"; filename="${fileFilename}"`);
- body.push(`Content-Type: ${blob.type}`);
- body.push("");
- body.push("");
- body = body.join("\r\n");
- let enc = new TextEncoder("utf-8");
- body = enc.encode(body);
- body = concatBuffers(body.buffer, blobAsBuffer);
+
let tail = `\r\n--${boundary}--`;
tail = enc.encode(tail);
body = concatBuffers(body, tail.buffer);
return {
"content-type": `multipart/form-data; boundary=${boundary}`,
body
};
});
}
- function uploadShot(shot, blob) {
+ function uploadShot(shot, blob, thumbnail) {
let headers;
return auth.authHeaders().then((_headers) => {
headers = _headers;
if (blob) {
+ let files = [ {fieldName: "blob", filename: "screenshot.png", blob} ];
+ if (thumbnail) {
+ files.push({fieldName: "thumbnail", filename: "thumbnail.png", blob: thumbnail});
+ }
return createMultipart(
{shot: JSON.stringify(shot.asJson())},
- "blob", "screenshot.png", blob
+ files
);
}
return {
"content-type": "application/json",
body: JSON.stringify(shot.asJson())
};
}).then((submission) => {
--- a/browser/extensions/screenshots/webextension/build/inlineSelectionCss.js
+++ b/browser/extensions/screenshots/webextension/build/inlineSelectionCss.js
@@ -71,16 +71,18 @@ window.inlineSelectionCss = `
.button.download:active, .download.highlight-button-cancel:active, .download.highlight-button-save:active, .download.highlight-button-download:active, .download.highlight-button-copy:active, .download.preview-button-save:active {
background-color: #dedede; }
.button.share, .share.highlight-button-cancel, .share.highlight-button-save, .share.highlight-button-download, .share.highlight-button-copy, .share.preview-button-save {
background-image: url("../img/icon-share.svg"); }
.button.share:hover, .share.highlight-button-cancel:hover, .share.highlight-button-save:hover, .share.highlight-button-download:hover, .share.highlight-button-copy:hover, .share.preview-button-save:hover {
background-color: #ededf0; }
.button.share.active, .share.active.highlight-button-cancel, .share.active.highlight-button-save, .share.active.highlight-button-download, .share.active.highlight-button-copy, .share.active.preview-button-save, .button.share:active, .share.highlight-button-cancel:active, .share.highlight-button-save:active, .share.highlight-button-download:active, .share.highlight-button-copy:active, .share.preview-button-save:active {
background-color: #dedede; }
+ .button.share.newicon, .share.newicon.highlight-button-cancel, .share.newicon.highlight-button-save, .share.newicon.highlight-button-download, .share.newicon.highlight-button-copy, .share.newicon.preview-button-save {
+ background-image: url("../img/icon-share-alternate.svg"); }
.button.trash, .trash.highlight-button-cancel, .trash.highlight-button-save, .trash.highlight-button-download, .trash.highlight-button-copy, .trash.preview-button-save {
background-image: url("../img/icon-trash.svg"); }
.button.trash:hover, .trash.highlight-button-cancel:hover, .trash.highlight-button-save:hover, .trash.highlight-button-download:hover, .trash.highlight-button-copy:hover, .trash.preview-button-save:hover {
background-color: #ededf0; }
.button.trash:active, .trash.highlight-button-cancel:active, .trash.highlight-button-save:active, .trash.highlight-button-download:active, .trash.highlight-button-copy:active, .trash.preview-button-save:active {
background-color: #dedede; }
.button.edit, .edit.highlight-button-cancel, .edit.highlight-button-save, .edit.highlight-button-download, .edit.highlight-button-copy, .edit.preview-button-save {
background-image: url("../img/icon-edit.svg"); }
@@ -475,59 +477,46 @@ window.inlineSelectionCss = `
right: 5px; }
.bottom-selection .highlight-buttons {
bottom: 5px; }
.left-selection .highlight-buttons {
right: auto;
left: 5px; }
.highlight-button-cancel {
- background-image: url("MOZ_EXTENSION/icons/cancel.svg");
- background-position: center center;
- background-repeat: no-repeat;
- background-size: 18px 18px;
border: 1px solid #dedede;
margin: 5px;
width: 40px; }
.highlight-button-save {
- background-image: url("MOZ_EXTENSION/icons/cloud.svg");
- background-repeat: no-repeat;
- background-size: 20px 18px;
font-size: 18px;
margin: 5px;
min-width: 80px; }
- html[dir="ltr"] .highlight-button-save {
- background-position: 8px center; }
- html[dir="rtl"] .highlight-button-save {
- background-position: right 10px center; }
- html[dir="ltr"] .highlight-button-save {
- padding-left: 34px; }
- html[dir="rtl"] .highlight-button-save {
- padding-right: 40px; }
+ html[dir="ltr"] .highlight-button-save img {
+ padding-right: 8px; }
+ html[dir="rtl"] .highlight-button-save img {
+ padding-left: 8px; }
.highlight-button-download {
- background-image: url("MOZ_EXTENSION/icons/download.svg");
- background-position: center center;
- background-repeat: no-repeat;
- background-size: 18px 18px;
border: 1px solid #dedede;
display: block;
margin: 5px;
width: 40px; }
.highlight-button-download.download-only-button {
- width: auto;
- background-position: 7px;
- padding-left: 32px; }
+ font-size: 18px;
+ width: auto; }
+ .highlight-button-download.download-only-button img {
+ height: 16px;
+ width: 16px; }
+ html[dir="ltr"] .highlight-button-download.download-only-button img {
+ padding-right: 8px; }
+ html[dir="rtl"] .highlight-button-download.download-only-button img {
+ padding-left: 8px; }
.highlight-button-copy {
- background-image: url("MOZ_EXTENSION/icons/copy.svg");
- background-position: center center;
- background-repeat: no-repeat;
- background-size: 18px 18px;
border: 1px solid #dedede;
display: block;
margin: 5px;
width: 40px; }
.pixel-dimensions {
position: absolute;
pointer-events: none;
@@ -537,46 +526,48 @@ window.inlineSelectionCss = `
color: #000;
text-shadow: -1px -1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #fff, 1px 1px 0 #fff; }
.preview-buttons {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
- right: 0;
top: -2px; }
+ html[dir="rtl"] .preview-buttons {
+ left: 0; }
+ html[dir="ltr"] .preview-buttons {
+ right: 0; }
.preview-image {
position: relative;
height: 80%;
max-width: 100%;
margin: auto 2em;
text-align: center;
animation-delay: 50ms;
animation: bounce-in 300ms forwards ease-in-out; }
-.preview-image img {
+.preview-image > img {
display: block;
width: auto;
height: auto;
max-width: 100%;
max-height: 90%;
margin: 50px auto;
border: 1px solid rgba(255, 255, 255, 0.8); }
.preview-button-save {
- background-image: url("MOZ_EXTENSION/icons/cloud.svg");
- background-position: 8px center;
- background-repeat: no-repeat;
- background-size: 20px 18px;
font-size: 18px;
margin: 5px;
- min-width: 80px;
- padding-left: 34px; }
+ min-width: 80px; }
+ html[dir="ltr"] .preview-button-save img {
+ padding-right: 8px; }
+ html[dir="rtl"] .preview-button-save img {
+ padding-left: 8px; }
.fixed-container {
align-items: center;
display: flex;
flex-direction: column;
height: 100vh;
justify-content: center;
left: 0;
@@ -633,16 +624,30 @@ window.inlineSelectionCss = `
font-family: -apple-system, BlinkMacSystemFont, "segoe ui", "helvetica neue", helvetica, ubuntu, roboto, noto, arial, sans-serif;
font-size: 24px;
line-height: 32px;
text-align: center;
padding-top: 20px;
width: 400px;
user-select: none; }
+.cancel-shot {
+ background-color: transparent;
+ cursor: pointer;
+ outline: none;
+ border-radius: 3px;
+ border: 1px #9b9b9b solid;
+ color: #fff;
+ cursor: pointer;
+ font-family: -apple-system, BlinkMacSystemFont, "segoe ui", "helvetica neue", helvetica, ubuntu, roboto, noto, arial, sans-serif;
+ font-size: 16px;
+ margin-top: 40px;
+ padding: 10px 25px;
+ pointer-events: all; }
+
.myshots-all-buttons-container {
display: flex;
flex-direction: row-reverse;
background: #f5f5f5;
border-radius: 2px;
box-sizing: border-box;
height: 80px;
padding: 8px;
--- a/browser/extensions/screenshots/webextension/build/shot.js
+++ b/browser/extensions/screenshots/webextension/build/shot.js
@@ -232,29 +232,35 @@ class AbstractShot {
this.images = [];
if (attrs.images) {
this.images = attrs.images.map(
(json) => new this.Image(json));
}
this.openGraph = attrs.openGraph || null;
this.twitterCard = attrs.twitterCard || null;
this.documentSize = attrs.documentSize || null;
- this.fullScreenThumbnail = attrs.fullScreenThumbnail || null;
+ this.thumbnail = attrs.thumbnail || null;
this.abTests = attrs.abTests || null;
this._clips = {};
if (attrs.clips) {
for (let clipId in attrs.clips) {
let clip = attrs.clips[clipId];
this._clips[clipId] = new this.Clip(this, clipId, clip);
}
}
+ let isProd = typeof process !== "undefined" && process.env.NODE_ENV === "production";
+
for (let attr in attrs) {
if (attr !== "clips" && attr !== "id" && !this.REGULAR_ATTRS.includes(attr) && !this.DEPRECATED_ATTRS.includes(attr)) {
- throw new Error("Unexpected attribute: " + attr);
+ if (isProd) {
+ console.warn("Unexpected attribute: " + attr);
+ } else {
+ throw new Error("Unexpected attribute: " + attr);
+ }
} else if (attr === "id") {
console.warn("passing id in attrs in AbstractShot constructor");
console.trace();
assert(attrs.id === this.id);
}
}
}
@@ -564,26 +570,26 @@ class AbstractShot {
assert(typeof val.height == "number");
assert(typeof val.width == "number");
this._documentSize = val;
} else {
this._documentSize = null;
}
}
- get fullScreenThumbnail() {
- return this._fullScreenThumbnail;
+ get thumbnail() {
+ return this._thumbnail;
}
- set fullScreenThumbnail(val) {
+ set thumbnail(val) {
assert(typeof val == "string" || !val);
if (val) {
assert(isUrl(val));
- this._fullScreenThumbnail = val;
+ this._thumbnail = val;
} else {
- this._fullScreenThumbnail = null;
+ this._thumbnail = null;
}
}
get abTests() {
return this._abTests;
}
set abTests(val) {
if (val === null || val === undefined) {
@@ -598,28 +604,29 @@ class AbstractShot {
this._abTests = val;
}
}
AbstractShot.prototype.REGULAR_ATTRS = (`
origin fullUrl docTitle userTitle createdDate favicon images
siteName openGraph twitterCard documentSize
-fullScreenThumbnail abTests
+thumbnail abTests
`).split(/\s+/g);
// Attributes that will be accepted in the constructor, but ignored/dropped
AbstractShot.prototype.DEPRECATED_ATTRS = (`
microdata history ogTitle createdDevice head body htmlAttrs bodyAttrs headAttrs
readable hashtags comments showPage isPublic resources deviceId url
+fullScreenThumbnail
`).split(/\s+/g);
AbstractShot.prototype.RECALL_ATTRS = (`
url docTitle userTitle createdDate favicon
-openGraph twitterCard images fullScreenThumbnail
+openGraph twitterCard images thumbnail
`).split(/\s+/g);
AbstractShot.prototype._OPENGRAPH_PROPERTIES = (`
title type url image audio description determiner locale site_name video
image:secure_url image:type image:width image:height
video:secure_url video:type video:width image:height
audio:secure_url audio:type
article:published_time article:modified_time article:expiration_time article:author article:section article:tag
new file mode 100644
--- /dev/null
+++ b/browser/extensions/screenshots/webextension/build/thumbnailGenerator.js
@@ -0,0 +1,150 @@
+this.thumbnailGenerator = (function () {let exports={}; // This is used in addon/webextension/background/takeshot.js,
+// server/src/pages/shot/controller.js, and
+// server/scr/pages/shotindex/view.js. It is used in a browser
+// environment.
+
+// Resize down 1/2 at a time produces better image quality.
+// Not quite as good as using a third-party filter (which will be
+// slower), but good enough.
+const maxResizeScaleFactor = 0.5
+
+// The shot will be scaled or cropped down to 210px on x, and cropped or
+// scaled down to a maximum of 280px on y.
+// x: 210
+// y: <= 280
+const maxThumbnailWidth = 210;
+const maxThumbnailHeight = 280;
+
+/**
+ * @param {int} imageHeight Height in pixels of the original image.
+ * @param {int} imageWidth Width in pixels of the original image.
+ * @returns {width, height, scaledX, scaledY}
+ */
+function getThumbnailDimensions(imageWidth, imageHeight) {
+ const displayAspectRatio = 3 / 4;
+ let imageAspectRatio = imageWidth / imageHeight;
+ let thumbnailImageWidth, thumbnailImageHeight;
+ let scaledX, scaledY;
+
+ if (imageAspectRatio > displayAspectRatio) {
+ // "Landscape" mode
+ // Scale on y, crop on x
+ let yScaleFactor = (imageHeight > maxThumbnailHeight) ? (maxThumbnailHeight / imageHeight) : 1.0;
+ thumbnailImageHeight = scaledY = Math.round(imageHeight * yScaleFactor);
+ scaledX = Math.round(imageWidth * yScaleFactor);
+ thumbnailImageWidth = Math.min(scaledX, maxThumbnailWidth);
+ } else {
+ // "Portrait" mode
+ // Scale on x, crop on y
+ let xScaleFactor = (imageWidth > maxThumbnailWidth) ? (maxThumbnailWidth / imageWidth) : 1.0;
+ thumbnailImageWidth = scaledX = Math.round(imageWidth * xScaleFactor);
+ scaledY = Math.round(imageHeight * xScaleFactor);
+ // The CSS could widen the image, in which case we crop more off of y.
+ thumbnailImageHeight = Math.min(scaledY, maxThumbnailHeight,
+ maxThumbnailHeight / (maxThumbnailWidth / imageWidth));
+ }
+
+ return {
+ width: thumbnailImageWidth,
+ height: thumbnailImageHeight,
+ scaledX,
+ scaledY
+ }
+}
+
+/**
+ * @param {dataUrl} String Data URL of the original image.
+ * @param {int} imageHeight Height in pixels of the original image.
+ * @param {int} imageWidth Width in pixels of the original image.
+ * @param {String} urlOrBlob 'blob' for a blob, otherwise data url.
+ * @returns A promise that resolves to the data URL or blob of the thumbnail image, or null.
+ */
+function createThumbnail(dataUrl, imageWidth, imageHeight, urlOrBlob) {
+ // There's cost associated with generating, transmitting, and storing
+ // thumbnails, so we'll opt out if the image size is below a certain threshold
+ const thumbnailThresholdFactor = 1.20;
+ const thumbnailWidthThreshold = maxThumbnailWidth * thumbnailThresholdFactor;
+ const thumbnailHeightThreshold = maxThumbnailHeight * thumbnailThresholdFactor;
+
+ if (imageWidth <= thumbnailWidthThreshold &&
+ imageHeight <= thumbnailHeightThreshold) {
+ // Do not create a thumbnail.
+ return Promise.resolve(null);
+ }
+
+ let thumbnailDimensions = getThumbnailDimensions(imageWidth, imageHeight);
+
+ return new Promise((resolve, reject) => {
+ let thumbnailImage = new Image();
+ let srcWidth = imageWidth;
+ let srcHeight = imageHeight;
+ let destWidth, destHeight;
+
+ thumbnailImage.onload = function() {
+ destWidth = Math.round(srcWidth * maxResizeScaleFactor);
+ destHeight = Math.round(srcHeight * maxResizeScaleFactor);
+ if (destWidth <= thumbnailDimensions.scaledX || destHeight <= thumbnailDimensions.scaledY) {
+ srcWidth = Math.round(srcWidth * (thumbnailDimensions.width / thumbnailDimensions.scaledX));
+ srcHeight = Math.round(srcHeight * (thumbnailDimensions.height / thumbnailDimensions.scaledY));
+ destWidth = thumbnailDimensions.width;
+ destHeight = thumbnailDimensions.height;
+ }
+
+ const thumbnailCanvas = document.createElement('canvas');
+ thumbnailCanvas.width = destWidth;
+ thumbnailCanvas.height = destHeight;
+ const ctx = thumbnailCanvas.getContext("2d");
+ ctx.imageSmoothingEnabled = false;
+
+ ctx.drawImage(
+ thumbnailImage,
+ 0, 0, srcWidth, srcHeight,
+ 0, 0, destWidth, destHeight);
+
+ if (thumbnailCanvas.width <= thumbnailDimensions.width ||
+ thumbnailCanvas.height <= thumbnailDimensions.height) {
+ if (urlOrBlob === "blob") {
+ thumbnailCanvas.toBlob((blob) => {
+ resolve(blob);
+ });
+ } else {
+ resolve(thumbnailCanvas.toDataURL("image/png"))
+ }
+ return;
+ }
+
+ srcWidth = destWidth;
+ srcHeight = destHeight;
+ thumbnailImage.src = thumbnailCanvas.toDataURL();
+ }
+ thumbnailImage.src = dataUrl;
+ });
+}
+
+function createThumbnailUrl(shot) {
+ const image = shot.getClip(shot.clipNames()[0]).image;
+ if (!image.url) {
+ return Promise.resolve(null);
+ }
+ return createThumbnail(
+ image.url, image.dimensions.x, image.dimensions.y, "dataurl");
+}
+
+function createThumbnailBlobFromPromise(shot, blobToUrlPromise) {
+ return blobToUrlPromise.then(dataUrl => {
+ const image = shot.getClip(shot.clipNames()[0]).image;
+ return createThumbnail(
+ dataUrl, image.dimensions.x, image.dimensions.y, "blob");
+ });
+}
+
+if (typeof exports != "undefined") {
+ exports.getThumbnailDimensions = getThumbnailDimensions;
+ exports.createThumbnailUrl = createThumbnailUrl;
+ exports.createThumbnailBlobFromPromise = createThumbnailBlobFromPromise;
+}
+
+return exports;
+})();
+null;
+
--- a/browser/extensions/screenshots/webextension/manifest.json
+++ b/browser/extensions/screenshots/webextension/manifest.json
@@ -1,12 +1,12 @@
{
"manifest_version": 2,
"name": "Firefox Screenshots",
- "version": "25.0.0",
+ "version": "29.0.0",
"description": "__MSG_addonDescription__",
"author": "__MSG_addonAuthorsList__",
"homepage_url": "https://github.com/mozilla-services/screenshots",
"applications": {
"gecko": {
"id": "screenshots@mozilla.org",
"strict_min_version": "57.0a1"
}
--- a/browser/extensions/screenshots/webextension/onboarding/slides.js
+++ b/browser/extensions/screenshots/webextension/onboarding/slides.js
@@ -139,26 +139,16 @@ this.slides = (function() {
doc.querySelector("#skip").addEventListener("click", watchFunction(assertIsTrusted((event) => {
shooter.sendEvent("cancel-slides", "skip");
callbacks.onEnd();
})));
doc.querySelector("#done").addEventListener("click", watchFunction(assertIsTrusted((event) => {
shooter.sendEvent("finish-slides", "done");
callbacks.onEnd();
})));
- // Note: e10s breaks the terms and privacy anchor tags. Work around this by
- // manually opening the correct URLs on click until bug 1357589 is fixed.
- doc.querySelector("#terms").addEventListener("click", watchFunction(assertIsTrusted((event) => {
- event.preventDefault();
- callBackground("openTermsPage");
- })));
- doc.querySelector("#privacy").addEventListener("click", watchFunction(assertIsTrusted((event) => {
- event.preventDefault();
- callBackground("openPrivacyPage");
- })));
doc.querySelector("#slide-overlay").addEventListener("click", watchFunction(assertIsTrusted((event) => {
if (event.target == doc.querySelector("#slide-overlay")) {
shooter.sendEvent("cancel-slides", "background-click");
callbacks.onEnd();
}
})));
setSlide(1);
}
--- a/browser/extensions/screenshots/webextension/selector/shooter.js
+++ b/browser/extensions/screenshots/webextension/selector/shooter.js
@@ -55,21 +55,17 @@ this.shooter = (function() { // eslint-d
} else {
canvas.width = width * window.devicePixelRatio;
canvas.height = height * window.devicePixelRatio;
}
if (expand) {
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
}
ui.iframe.hide();
- try {
- ctx.drawWindow(window, selectedPos.left, selectedPos.top, width, height, "#fff");
- } finally {
- ui.iframe.unhide();
- }
+ ctx.drawWindow(window, selectedPos.left, selectedPos.top, width, height, "#fff");
let limit = buildSettings.pngToJpegCutoff;
let dataUrl = canvas.toDataURL();
if (limit && dataUrl.length > limit) {
let jpegDataUrl = canvas.toDataURL("image/jpeg");
if (jpegDataUrl.length < dataUrl.length) {
// Only use the JPEG if it is actually smaller
dataUrl = jpegDataUrl;
}
@@ -147,31 +143,33 @@ this.shooter = (function() { // eslint-d
return clipboard.copy(url).then((copied) => {
return callBackground("openShot", { url, copied });
});
}, (error) => {
if ('popupMessage' in error && (error.popupMessage == "REQUEST_ERROR" || error.popupMessage == 'CONNECTION_ERROR')) {
// The error has been signaled to the user, but unlike other errors (or
// success) we should not abort the selection
deactivateAfterFinish = false;
+ // We need to unhide the UI since screenshotPage() hides it.
+ ui.iframe.unhide();
return;
}
if (error.name != "BackgroundError") {
// BackgroundError errors are reported in the Background page
throw error;
}
}).then(() => {
if (deactivateAfterFinish) {
uicontrol.deactivate();
}
}));
};
- exports.downloadShot = function(selectedPos) {
- let dataUrl = screenshotPage(selectedPos);
+ exports.downloadShot = function(selectedPos, previewDataUrl) {
+ let dataUrl = previewDataUrl || screenshotPage(selectedPos);
let promise = Promise.resolve(dataUrl);
if (!dataUrl) {
promise = callBackground(
"screenshotPage",
selectedPos.asJson(),
{
scrollX: window.scrollX,
scrollY: window.scrollY,
@@ -191,25 +189,51 @@ this.shooter = (function() { // eslint-d
location: selectedPos
}
});
ui.triggerDownload(dataUrl, shotObject.filename);
uicontrol.deactivate();
}));
};
- exports.copyShot = function(selectedPos) {
- let dataUrl = screenshotPage(selectedPos);
+ let copyInProgress = null;
+ exports.copyShot = function(selectedPos, previewDataUrl) {
+ // This is pretty slow. We'll ignore additional user triggered copy events
+ // while it is in progress.
+ if (copyInProgress) {
+ return;
+ }
+ // A max of five seconds in case some error occurs.
+ copyInProgress = setTimeout(() => {
+ copyInProgress = null;
+ }, 5000);
+
+ let unsetCopyInProgress = () => {
+ if (copyInProgress) {
+ clearTimeout(copyInProgress);
+ copyInProgress = null;
+ }
+ }
+ let dataUrl = previewDataUrl || screenshotPage(selectedPos);
let blob = blobConverters.dataUrlToBlob(dataUrl);
catcher.watchPromise(callBackground("copyShotToClipboard", blob).then(() => {
uicontrol.deactivate();
- }));
+ unsetCopyInProgress();
+ }, unsetCopyInProgress));
};
exports.sendEvent = function(...args) {
+ let maybeOptions = args[args.length - 1];
+
+ if (typeof maybeOptions === "object") {
+ maybeOptions.incognito = browser.extension.inIncognitoContext;
+ } else {
+ args.push({incognito: browser.extension.inIncognitoContext});
+ }
+
callBackground("sendEvent", ...args);
};
catcher.watchFunction(() => {
shotObject = new AbstractShot(
backend,
randomString(RANDOM_STRING_LENGTH) + "/" + domainFromUrl(location),
{
--- a/browser/extensions/screenshots/webextension/selector/ui.js
+++ b/browser/extensions/screenshots/webextension/selector/ui.js
@@ -1,46 +1,25 @@
-/* globals log, util, catcher, inlineSelectionCss, callBackground, assertIsTrusted, assertIsBlankDocument, buildSettings */
+/* globals log, util, catcher, inlineSelectionCss, callBackground, assertIsTrusted, assertIsBlankDocument, buildSettings blobConverters */
"use strict";
this.ui = (function() { // eslint-disable-line no-unused-vars
let exports = {};
const SAVE_BUTTON_HEIGHT = 50;
const { watchFunction } = catcher;
- // The <body> tag itself can have margins and offsets, which need to be used when
- // setting the position of the boxEl.
- function getBodyRect() {
- if (getBodyRect.cached) {
- return getBodyRect.cached;
- }
- let rect = document.body.getBoundingClientRect();
- let cached = {
- top: rect.top + window.scrollY,
- bottom: rect.bottom + window.scrollY,
- left: rect.left + window.scrollX,
- right: rect.right + window.scrollX
- };
- // FIXME: I can't decide when this is necessary
- // *not* necessary on http://patriciogonzalezvivo.com/2015/thebookofshaders/
- // (actually causes mis-selection there)
- // *is* necessary on http://atirip.com/2015/03/17/sorry-sad-state-of-matrix-transforms-in-browsers/
- cached = {top: 0, bottom: 0, left: 0, right: 0};
- getBodyRect.cached = cached;
- return cached;
- }
-
exports.isHeader = function(el) {
while (el) {
if (el.classList &&
(el.classList.contains("myshots-button") ||
el.classList.contains("visible") ||
- el.classList.contains("full-page"))) {
+ el.classList.contains("full-page") ||
+ el.classList.contains("cancel-shot"))) {
return true;
}
el = el.parentNode;
}
return false;
}
let substitutedCss = inlineSelectionCss.replace(/MOZ_EXTENSION([^"]+)/g, (match, filename) => {
@@ -63,21 +42,32 @@ this.ui = (function() { // eslint-disabl
clearTimeout(this.sizeTracking.windowDelayer);
}
this.sizeTracking.windowDelayer = setTimeout(watchFunction(() => {
this.updateElementSize(true);
}), 50);
}
function localizeText(doc) {
- let els = doc.querySelectorAll("[data-l10n-id]");
+ let els = doc.querySelectorAll("[data-l10n-id], [data-l10n-title]");
for (let el of els) {
let id = el.getAttribute("data-l10n-id");
- let text = browser.i18n.getMessage(id);
- el.textContent = text;
+ if (id) {
+ let text = browser.i18n.getMessage(id);
+ el.textContent = text;
+ }
+ let title = el.getAttribute("data-l10n-title");
+ if (title) {
+ let titleText = browser.i18n.getMessage(title);
+ let sanitized = titleText && titleText.replace("&", "&")
+ .replace('"', """)
+ .replace("<", "<")
+ .replace(">", ">");
+ el.setAttribute("title", sanitized);
+ }
}
}
function highContrastCheck(win) {
let result, doc, el;
doc = win.document;
el = doc.createElement("div");
el.style.backgroundImage = "url('#')";
@@ -100,16 +90,18 @@ this.ui = (function() { // eslint-disabl
function renderDownloadNotice(initAtBottom = false) {
let notice = makeEl("table", "notice");
notice.innerHTML = `
<div class="notice-tooltip">
<p data-l10n-id="downloadOnlyDetails"></p>
<ul>
<li data-l10n-id="downloadOnlyDetailsPrivate"></li>
<li data-l10n-id="downloadOnlyDetailsNeverRemember"></li>
+ <li data-l10n-id="downloadOnlyDetailsESR"></li>
+ <li data-l10n-id="downloadOnlyDetailsNoUploadPref"></li>
</ul>
</div>
<tbody>
<tr class="notice-wrapper">
<td class="notice-content" data-l10n-id="downloadOnlyNotice"></td>
<td class="notice-help"></td>
</tr>
<tbody>`;
@@ -215,23 +207,25 @@ this.ui = (function() { // eslint-disabl
document.documentElement.clientWidth,
document.body.clientWidth,
document.documentElement.scrollWidth,
document.body.scrollWidth);
if (width !== this.sizeTracking.lastWidth) {
this.sizeTracking.lastWidth = width;
this.element.style.width = width + "px";
// Since this frame has an absolute position relative to the parent
- // document, if the parent document has a max-width that is narrower
- // than the viewport, then the x of the parent document is not at 0 of
- // the viewport. That makes the frame shifted to the right. This left
- // margin negates that.
- let boundingRect = document.body.getBoundingClientRect();
- if (boundingRect.x) {
- this.element.style.marginLeft = `-${boundingRect.x}px`;
+ // document, if the parent document's body has a relative position and
+ // left and/or top not at 0, then the left and/or top of the parent
+ // document's body is not at (0, 0) of the viewport. That makes the
+ // frame shifted relative to the viewport. These margins negates that.
+ if (window.getComputedStyle(document.body).position === "relative") {
+ let docBoundingRect = document.documentElement.getBoundingClientRect();
+ let bodyBoundingRect = document.body.getBoundingClientRect();
+ this.element.style.marginLeft = `-${bodyBoundingRect.right - docBoundingRect.right}px`;
+ this.element.style.marginTop = `-${bodyBoundingRect.bottom - docBoundingRect.bottom}px`;
}
}
if (force && visible) {
this.element.style.display = "";
}
},
initSizeWatch() {
@@ -297,23 +291,24 @@ this.ui = (function() { // eslint-disabl
<div class="preview-overlay">
<div class="fixed-container">
<div class="face-container">
<div class="eye left"><div class="eyeball"></div></div>
<div class="eye right"><div class="eyeball"></div></div>
<div class="face"></div>
</div>
<div class="preview-instructions" data-l10n-id="screenshotInstructions"></div>
+ <button class="cancel-shot">${browser.i18n.getMessage("cancelScreenshot")}</button>
<div class="myshots-all-buttons-container">
${isDownloadOnly() ? '' : `
- <button class="myshots-button myshots-link" tabindex="1" data-l10n-id="myShotsLink"></button>
+ <button class="myshots-button" tabindex="1" data-l10n-id="myShotsLink"></button>
<div class="spacer"></div>
`}
- <button class="myshots-button visible" tabindex="2" data-l10n-id="saveScreenshotVisibleArea"></button>
- <button class="myshots-button full-page" tabindex="3" data-l10n-id="saveScreenshotFullPage"></button>
+ <button class="visible" tabindex="2" data-l10n-id="saveScreenshotVisibleArea"></button>
+ <button class="full-page" tabindex="3" data-l10n-id="saveScreenshotFullPage"></button>
</div>
</div>
</div>
</body>`;
installHandlerOnDocument(this.document);
if (this.addClassName) {
this.document.body.className = this.addClassName;
}
@@ -324,16 +319,18 @@ this.ui = (function() { // eslint-disabl
if (!(isDownloadOnly())) {
overlay.querySelector(".myshots-button").addEventListener(
"click", watchFunction(assertIsTrusted(standardOverlayCallbacks.onOpenMyShots)));
}
overlay.querySelector(".visible").addEventListener(
"click", watchFunction(assertIsTrusted(standardOverlayCallbacks.onClickVisible)));
overlay.querySelector(".full-page").addEventListener(
"click", watchFunction(assertIsTrusted(standardOverlayCallbacks.onClickFullPage)));
+ overlay.querySelector(".cancel-shot").addEventListener(
+ "click", watchFunction(assertIsTrusted(standardOverlayCallbacks.onClickCancel)));
resolve();
}), {once: true});
document.body.appendChild(this.element);
} else {
resolve();
}
});
},
@@ -377,17 +374,21 @@ this.ui = (function() { // eslint-disabl
util.removeNode(this.element);
this.element = null;
this.document = null;
}
};
function getAttributeText(l10nID) {
let text = browser.i18n.getMessage(l10nID);
- return text && text.replace('"', """);
+ return text &&
+ text.replace("&", "&")
+ .replace('"', """)
+ .replace("<", "<")
+ .replace(">", ">");
}
let iframePreview = exports.iframePreview = {
element: null,
document: null,
display(installHandlerOnDocument, standardOverlayCallbacks) {
return new Promise((resolve, reject) => {
if (!this.element) {
@@ -405,28 +406,33 @@ this.ui = (function() { // eslint-disabl
<style>${substitutedCss}</style>
<title></title>
</head>
<body>
<div class="preview-overlay">
<div class="preview-image">
<div class="preview-buttons">
<button class="highlight-button-cancel"
- title="${getAttributeText("cancelScreenshot")}"></button>
+ data-l10n-title="cancelScreenshot"><img
+ src="${browser.extension.getURL("icons/cancel.svg")}" /></button>
<button class="highlight-button-copy"
- title="${getAttributeText("copyScreenshot")}"></button>
+ data-l10n-title="copyScreenshot"><img
+ src="${browser.extension.getURL("icons/copy.svg")}" /></button>
${isDownloadOnly() ?
`<button class="highlight-button-download download-only-button"
- title="${getAttributeText("downloadScreenshot")}"
- data-l10n-id="downloadScreenshot"></button>` :
+ data-l10n-title="downloadScreenshot"><img
+ src="${browser.extension.getURL("icons/download.svg")}"
+ />${browser.i18n.getMessage("downloadScreenshot")}</button>` :
`<button class="highlight-button-download"
- title="${getAttributeText("downloadScreenshot")}"></button>
+ data-l10n-title="downloadScreenshot"><img
+ src="${browser.extension.getURL("icons/download.svg")}" /></button>
<button class="preview-button-save"
- title="${getAttributeText("saveScreenshotSelectedArea")}"
- data-l10n-id="saveScreenshotSelectedArea"></button>`
+ data-l10n-title="saveScreenshotSelectedArea"><img
+ src="${browser.extension.getURL("icons/cloud.svg")}"
+ />${browser.i18n.getMessage("saveScreenshotSelectedArea")}</button>`
}
</div>
</div>
</div>
</body>`;
installHandlerOnDocument(this.document);
this.document.documentElement.dir = browser.i18n.getMessage("@@bidi_dir");
this.document.documentElement.lang = browser.i18n.getMessage("@@ui_locale");
@@ -577,17 +583,16 @@ this.ui = (function() { // eslint-disabl
callbacks.copy(e);
e.preventDefault();
e.stopPropagation();
}));
this.copy.style.display = "";
} else {
this.copy.style.display = "none";
}
- let bodyRect = getBodyRect();
let winBottom = window.innerHeight;
let pageYOffset = window.pageYOffset;
if ((pos.right - pos.left) < 78 || (pos.bottom - pos.top) < 78) {
this.el.classList.add("small-selection");
} else {
this.el.classList.remove("small-selection");
@@ -601,35 +606,35 @@ this.ui = (function() { // eslint-disabl
this.el.classList.remove("bottom-selection");
}
if (pos.right < 200) {
this.el.classList.add("left-selection");
} else {
this.el.classList.remove("left-selection");
}
- this.el.style.top = (pos.top - bodyRect.top) + "px";
- this.el.style.left = (pos.left - bodyRect.left) + "px";
- this.el.style.height = (pos.bottom - pos.top - bodyRect.top) + "px";
- this.el.style.width = (pos.right - pos.left - bodyRect.left) + "px";
+ this.el.style.top = `${pos.top}px`;
+ this.el.style.left = `${pos.left}px`;
+ this.el.style.height = `${pos.bottom - pos.top}px`;
+ this.el.style.width = `${pos.right - pos.left}px`;
this.bgTop.style.top = "0px";
- this.bgTop.style.height = (pos.top - bodyRect.top) + "px";
+ this.bgTop.style.height = `${pos.top}px`;
this.bgTop.style.left = "0px";
this.bgTop.style.width = "100%";
- this.bgBottom.style.top = (pos.bottom - bodyRect.top) + "px";
+ this.bgBottom.style.top = `${pos.bottom}px`;
this.bgBottom.style.height = "100vh";
this.bgBottom.style.left = "0px";
this.bgBottom.style.width = "100%";
- this.bgLeft.style.top = (pos.top - bodyRect.top) + "px";
- this.bgLeft.style.height = pos.bottom - pos.top + "px";
+ this.bgLeft.style.top = `${pos.top}px`;
+ this.bgLeft.style.height = `${pos.bottom - pos.top}px`;
this.bgLeft.style.left = "0px";
- this.bgLeft.style.width = (pos.left - bodyRect.left) + "px";
- this.bgRight.style.top = (pos.top - bodyRect.top) + "px";
- this.bgRight.style.height = pos.bottom - pos.top + "px";
- this.bgRight.style.left = (pos.right - bodyRect.left) + "px";
+ this.bgLeft.style.width = `${pos.left}px`;
+ this.bgRight.style.top = `${pos.top}px`;
+ this.bgRight.style.height = `${pos.bottom - pos.top}px`;
+ this.bgRight.style.left = `${pos.right}px`;
this.bgRight.style.width = "100%";
// the download notice is injected into an iframe that matches the document size
// in order to reposition it on scroll we need to bind an updated positioning
// function to some window events.
this.repositionDownloadNotice = () => {
if (this.downloadNotice) {
const currentYOffset = window.pageYOffset;
const currentWinBottom = window.innerHeight;
@@ -639,37 +644,16 @@ this.ui = (function() { // eslint-disabl
if (this.downloadNotice) {
this.downloadNotice.style.top = (pageYOffset + winBottom - 60) + "px";
// event callbacks are delayed 100ms each to keep from overloading things
this.windowChangeStop = this.delayExecution(100, this.repositionDownloadNotice);
window.addEventListener('scroll', watchFunction(assertIsTrusted(this.windowChangeStop)));
window.addEventListener('resize', watchFunction(assertIsTrusted(this.windowChangeStop)));
}
-
- if (!(this.isElementInViewport(this.buttons))) {
- this.cancel.style.position = this.download.style.position = "fixed";
- this.cancel.style.left = (pos.left - bodyRect.left - 50) + "px";
- this.download.style.left = ((pos.left - bodyRect.left - 100)) + "px";
- this.cancel.style.top = this.download.style.top = (pos.top - bodyRect.top) + "px";
- if (this.save) {
- this.save.style.position = "fixed";
- this.save.style.left = ((pos.left - bodyRect.left) - 190) + "px";
- this.save.style.top = (pos.top - bodyRect.top) + "px";
- }
- } else {
- this.cancel.style.position = this.download.style.position = "initial";
- this.cancel.style.top = this.download.style.top = 0;
- this.cancel.style.left = this.download.style.left = 0;
- if (this.save) {
- this.save.style.position = "initial";
- this.save.style.top = 0;
- this.save.style.left = 0;
- }
- }
},
// used to eventually move the download-only warning
// when a user ends scrolling or ends resizing a window
delayExecution(delay, cb) {
let timer;
return function() {
if (typeof timer !== 'undefined') {
@@ -695,41 +679,55 @@ this.ui = (function() { // eslint-disabl
_createEl() {
let boxEl = this.el;
if (boxEl) {
return;
}
boxEl = makeEl("div", "highlight");
let buttons = makeEl("div", "highlight-buttons");
let cancel = makeEl("button", "highlight-button-cancel");
+ let cancelImg = makeEl("img");
+ cancelImg.src = browser.extension.getURL("icons/cancel.svg");
cancel.title = browser.i18n.getMessage("cancelScreenshot");
+ cancel.appendChild(cancelImg);
buttons.appendChild(cancel);
let copy = makeEl("button", "highlight-button-copy");
copy.title = browser.i18n.getMessage("copyScreenshot");
+ let copyImg = makeEl("img");
+ copyImg.src = browser.extension.getURL("icons/copy.svg");
+ copy.appendChild(copyImg);
buttons.appendChild(copy);
let download, save;
if (isDownloadOnly()) {
download = makeEl("button", "highlight-button-download download-only-button");
+ let downloadImg = makeEl("img");
+ downloadImg.src = browser.extension.getURL("icons/download.svg");
+ download.appendChild(downloadImg);
+ download.append(browser.i18n.getMessage("downloadScreenshot"));
download.title = browser.i18n.getMessage("downloadScreenshot");
- download.textContent = browser.i18n.getMessage("downloadScreenshot");
} else {
download = makeEl("button", "highlight-button-download");
download.title = browser.i18n.getMessage("downloadScreenshot");
+ let downloadImg = makeEl("img");
+ downloadImg.src = browser.extension.getURL("icons/download.svg");
+ download.appendChild(downloadImg);
save = makeEl("button", "highlight-button-save");
- save.textContent = browser.i18n.getMessage("saveScreenshotSelectedArea");
+ let saveImg = makeEl("img");
+ saveImg.src = browser.extension.getURL("icons/cloud.svg");
+ save.appendChild(saveImg);
+ save.append(browser.i18n.getMessage("saveScreenshotSelectedArea"));
save.title = browser.i18n.getMessage("saveScreenshotSelectedArea");
}
buttons.appendChild(download);
if (save) {
buttons.appendChild(save);
}
- this.buttons = buttons;
this.cancel = cancel;
this.download = download;
this.copy = copy;
this.save = save;
boxEl.appendChild(buttons);
for (let name of movements) {
let elTarget = makeEl("div", "mover-target direction-" + name);
let elMover = makeEl("div", "mover");
@@ -788,21 +786,16 @@ this.ui = (function() { // eslint-disabl
if (target.nodeType === document.ELEMENT_NODE && target.classList.contains("highlight-buttons")) {
return true;
}
target = target.parentNode;
}
return false;
},
- isElementInViewport(el) {
- let rect = el.getBoundingClientRect();
- return (rect.right <= window.innerWidth);
- },
-
clearSaveDisabled() {
if (!this.save) {
// Happens if we try to remove the disabled status after the worker
// has been shut down
return;
}
this.save.removeAttribute("disabled");
},
@@ -867,17 +860,18 @@ this.ui = (function() { // eslint-disabl
util.removeNode(this.el);
this.el = this.xEl = this.yEl = null;
}
};
exports.Preview = {
display(dataUrl, showCropWarning) {
let img = makeEl("IMG");
- img.src = dataUrl;
+ let imgBlob = blobConverters.dataUrlToBlob(dataUrl);
+ img.src = URL.createObjectURL(imgBlob);
iframe.document().querySelector(".preview-image").appendChild(img);
if (showCropWarning && !(isDownloadOnly())) {
let imageCroppedEl = makeEl("table", "notice");
imageCroppedEl.style.bottom = "10px";
imageCroppedEl.innerHTML = `<tbody>
<tr class="notice-wrapper">
<td class="notice-content"></td>
</tr>
--- a/browser/extensions/screenshots/webextension/selector/uicontrol.js
+++ b/browser/extensions/screenshots/webextension/selector/uicontrol.js
@@ -119,16 +119,25 @@ this.uicontrol = (function() {
H3: true,
H4: true,
H5: true,
H6: true
};
let captureType;
+ function removeDimensionLimitsOnFullPageShot() {
+ if (captureType === "fullPageTruncated") {
+ captureType = "fullPage";
+ selectedPos = new Selection(
+ 0, 0,
+ getDocumentWidth(), getDocumentHeight());
+ }
+ }
+
let standardDisplayCallbacks = {
cancel: () => {
sendEvent("cancel-shot", "overlay-cancel-button");
exports.deactivate();
}, save: () => {
sendEvent("save-shot", "overlay-save-button");
shooter.takeShot("selection", selectedPos);
}, download: () => {
@@ -140,16 +149,22 @@ this.uicontrol = (function() {
}
};
let standardOverlayCallbacks = {
cancel: () => {
sendEvent("cancel-shot", "cancel-preview-button");
exports.deactivate();
},
+ onClickCancel: e => {
+ sendEvent("cancel-shot", "cancel-selection-button");
+ e.preventDefault();
+ e.stopPropagation();
+ exports.deactivate();
+ },
onOpenMyShots: () => {
sendEvent("goto-myshots", "selection-button");
callBackground("openMyShots")
.then(() => exports.deactivate())
.catch(() => {
// Handled in communication.js
});
},
@@ -180,30 +195,27 @@ this.uicontrol = (function() {
setState("previewing");
},
onSavePreview: () => {
sendEvent(`save-${captureType.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()}`, "save-preview-button");
shooter.takeShot(captureType, selectedPos, dataUrl);
},
onDownloadPreview: () => {
sendEvent(`download-${captureType.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()}`, "download-preview-button");
-
// Downloaded shots don't have dimension limits
- if (captureType === "fullPageTruncated") {
- captureType = "fullPage";
- selectedPos = new Selection(
- 0, 0,
- getDocumentWidth(), getDocumentHeight());
- }
-
- shooter.downloadShot(selectedPos);
+ let previewDataUrl = (captureType === "fullPageTruncated") ? null : dataUrl;
+ removeDimensionLimitsOnFullPageShot();
+ shooter.downloadShot(selectedPos, previewDataUrl);
},
onCopyPreview: () => {
sendEvent(`copy-${captureType.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()}`, "copy-preview-button");
- shooter.copyShot(selectedPos);
+ // Copied shots don't have dimension limits
+ let previewDataUrl = (captureType === "fullPageTruncated") ? null : dataUrl;
+ removeDimensionLimitsOnFullPageShot();
+ shooter.copyShot(selectedPos, previewDataUrl);
}
};
/** Holds all the objects that handle events for each state: */
let stateHandlers = {};
function getState() {
return getState.state;
@@ -363,17 +375,17 @@ this.uicontrol = (function() {
elementFromPoint() {
return ui.iframe.getElementFromPoint(
this.x - window.pageXOffset,
this.y - window.pageYOffset
);
}
distanceTo(x, y) {
- return Math.sqrt(Math.pow(this.x - x, 2), Math.pow(this.y - y));
+ return Math.sqrt(Math.pow(this.x - x, 2) + Math.pow(this.y - y, 2));
}
}
/** *********************************************
* all stateHandlers
*/
let dataUrl;
@@ -411,28 +423,34 @@ this.uicontrol = (function() {
cachedEl: null,
start() {
selectedPos = mousedownPos = null;
this.cachedEl = null;
watchPromise(ui.iframe.display(installHandlersOnDocument, standardOverlayCallbacks).then(() => {
ui.iframe.usePreSelection();
ui.Box.remove();
- const handler = watchFunction(assertIsTrusted(keyupHandler));
- document.addEventListener("keyup", handler);
- registeredDocumentHandlers.push({name: "keyup", doc: document, handler, useCapture: false});
+ const upHandler = watchFunction(assertIsTrusted(keyupHandler));
+ document.addEventListener("keyup", upHandler);
+ registeredDocumentHandlers.push({name: "keyup", doc: document, upHandler, useCapture: false});
+ const downHandler = watchFunction(assertIsTrusted(keydownHandler));
+ document.addEventListener("keydown", downHandler);
+ registeredDocumentHandlers.push({name: "keydown", doc: document, downHandler, useCapture: false});
}));
},
mousemove(event) {
ui.PixelDimensions.display(event.pageX, event.pageY, event.pageX, event.pageY);
if (event.target.classList &&
(!event.target.classList.contains("preview-overlay"))) {
// User is hovering over a toolbar button or control
autoDetectRect = null;
+ if (this.cachedEl) {
+ this.cachedEl = null;
+ }
ui.HoverBox.hide();
return;
}
let el;
if (event.target.classList && event.target.classList.contains("preview-overlay")) {
// The hover is on the overlay, so we need to figure out the real element
el = ui.iframe.getElementFromPoint(
event.pageX + window.scrollX - window.pageXOffset,
@@ -865,22 +883,25 @@ this.uicontrol = (function() {
* Selection communication
*/
// If the slides module is loaded then we're supposed to onboard
let shouldOnboard = typeof slides !== "undefined";
exports.activate = function() {
if (!document.body) {
- callBackground("abortNoDocumentBody", document.documentElement.tagName);
+ callBackground("abortStartShot");
+ let tagName = String(document.documentElement.tagName || "").replace(/[^a-z0-9]/ig, "");
+ sendEvent("abort-start-shot", `document-is-${tagName}`);
selectorLoader.unloadModules();
return;
}
if (isFrameset()) {
- callBackground("abortFrameset");
+ callBackground("abortStartShot");
+ sendEvent("abort-start-shot", "frame-page");
selectorLoader.unloadModules();
return;
}
addHandlers();
if (shouldOnboard) {
setState("onboarding");
} else {
setState("crosshairs");
@@ -935,16 +956,17 @@ this.uicontrol = (function() {
if (handler[eventName]) {
return handler[eventName](event);
}
return undefined;
}).bind(null, eventName)));
primedDocumentHandlers.set(eventName, fn);
});
primedDocumentHandlers.set("keyup", watchFunction(assertIsTrusted(keyupHandler)));
+ primedDocumentHandlers.set("keydown", watchFunction(assertIsTrusted(keydownHandler)));
window.addEventListener('beforeunload', beforeunloadHandler);
}
let mousedownSetOnDocument = false;
function installHandlersOnDocument(docObj) {
for (let [eventName, handler] of primedDocumentHandlers) {
let watchHandler = watchFunction(handler);
@@ -960,37 +982,45 @@ this.uicontrol = (function() {
}
}
function beforeunloadHandler() {
sendEvent("cancel-shot", "tab-load");
exports.deactivate();
}
- function keyupHandler(event) {
- if (event.shiftKey || event.altKey) {
- // unused modifier keys
- return;
- }
+ function keydownHandler(event) {
+ // In MacOS, the keyup event for 'c' is not fired when performing cmd+c.
if (event.code === "KeyC" && (event.ctrlKey || event.metaKey)) {
callBackground("getPlatformOs").then(os => {
if ((event.ctrlKey && os !== "mac") ||
(event.metaKey && os === "mac")) {
sendEvent("copy-shot", "keyboard-copy");
shooter.copyShot(selectedPos);
}
}).catch(() => {
// handled by catcher.watchPromise
});
}
+ }
+
+ function keyupHandler(event) {
+ if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) {
+ // unused modifier keys
+ return;
+ }
if ((event.key || event.code) === "Escape") {
sendEvent("cancel-shot", "keyboard-escape");
exports.deactivate();
}
- if ((event.key || event.code) === "Enter" && getState.state === "selected") {
+ // Enter to trigger Save or Download by default. But if the user tabbed to
+ // select another button, then we do not want this.
+ if ((event.key || event.code) === "Enter"
+ && getState.state === "selected"
+ && ui.iframe.document().activeElement.tagName === "BODY") {
if (ui.isDownloadOnly()) {
sendEvent("download-shot", "keyboard-enter");
shooter.downloadShot(selectedPos);
} else {
sendEvent("save-shot", "keyboard-enter");
shooter.takeShot("selection", selectedPos);
}
}
--- a/browser/extensions/screenshots/webextension/sitehelper.js
+++ b/browser/extensions/screenshots/webextension/sitehelper.js
@@ -53,17 +53,17 @@ this.sitehelper = (function() {
document.addEventListener("delete-everything", catcher.watchFunction((event) => {
// FIXME: reset some data in the add-on
}, false));
document.addEventListener("request-login", catcher.watchFunction((event) => {
let shotId = event.detail;
catcher.watchPromise(callBackground("getAuthInfo", shotId || null).then((info) => {
sendBackupCookieRequest(info.authHeaders);
- sendCustomEvent("login-successful", {deviceId: info.deviceId, isOwner: info.isOwner});
+ sendCustomEvent("login-successful", {deviceId: info.deviceId, isOwner: info.isOwner, backupCookieRequest: true});
}));
}));
document.addEventListener("request-onboarding", catcher.watchFunction((event) => {
callBackground("requestOnboarding");
}));
// Depending on the script loading order, the site might get the addon-present event,