--- a/browser/extensions/screenshots/bootstrap.js
+++ b/browser/extensions/screenshots/bootstrap.js
@@ -1,15 +1,16 @@
/* 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 HISTORY_ENABLED_PREF = "places.history.enabled";
const { interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Console",
@@ -44,16 +45,17 @@ const prefObserver = {
// aData is the name of the pref that's been changed (relative to aSubject)
if (aData == USER_DISABLE_PREF) {
// eslint-disable-next-line promise/catch-or-return
appStartupPromise = appStartupPromise.then(handleStartup);
}
}
};
+
const appStartupObserver = {
register() {
Services.obs.addObserver(this, "sessionstore-windows-restored", false); // eslint-disable-line mozilla/no-useless-parameters
},
unregister() {
Services.obs.removeObserver(this, "sessionstore-windows-restored", false); // eslint-disable-line mozilla/no-useless-parameters
},
@@ -218,16 +220,28 @@ function handleMessage(msg, sender, send
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") {
+ 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});
}
}
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
@@ -261,20 +275,20 @@ function initPhotonPageAction(api, webEx
if (listenerPort.name != "photonPageActionPort") {
return;
}
port = listenerPort;
port.onMessage.addListener((message) => {
switch (message.type) {
case "setProperties":
if (message.title) {
- photonPageAction.title = message.title;
+ photonPageAction.setTitle(message.title);
}
if (message.iconPath) {
- photonPageAction.iconURL = webExtension.extension.getURL(message.iconPath);
+ photonPageAction.setIconURL(webExtension.extension.getURL(message.iconPath));
}
break;
default:
console.error("Unrecognized message:", message);
break;
}
});
});
--- 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>23.0.0</em:version>
+ <em:version>25.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
@@ -375,22 +375,23 @@ FINAL_TARGET_FILES.features['screenshots
]
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',
+ 'webextension/icons/copy.svg',
'webextension/icons/done.svg',
'webextension/icons/download.svg',
+ 'webextension/icons/help-16.svg',
'webextension/icons/icon-16-v2.svg',
'webextension/icons/icon-32-v2.svg',
'webextension/icons/icon-highlight-32-v2.svg',
- 'webextension/icons/icon-starred-32-v2.svg',
'webextension/icons/icon-welcome-face-without-eyes.svg',
'webextension/icons/menu-fullpage.svg',
'webextension/icons/menu-myshot-white.svg',
'webextension/icons/menu-myshot.svg',
'webextension/icons/menu-visible.svg',
'webextension/icons/onboarding-1.png',
'webextension/icons/onboarding-2.png',
'webextension/icons/onboarding-3.png',
--- a/browser/extensions/screenshots/webextension/background/analytics.js
+++ b/browser/extensions/screenshots/webextension/background/analytics.js
@@ -3,26 +3,62 @@
"use strict";
this.analytics = (function() {
let exports = {};
let telemetryPrefKnown = false;
let telemetryPref;
+ 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
+ }));
+ });
+ }
+
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) {
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 || {};
let di = deviceInfo();
return new Promise((resolve, reject) => {
let url = main.getBackend() + "/event";
@@ -72,10 +108,112 @@ this.analytics = (function() {
});
};
exports.getTelemetryPrefSync = function() {
catcher.watchPromise(exports.refreshTelemetryPref());
return !!telemetryPref;
};
+ let timingData = {};
+
+ // 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.
+ let rules = [{
+ name: 'page-action',
+ start: { action: 'start-shot', label: 'toolbar-button' },
+ end: { action: 'internal', label: 'unhide-preselection-frame' },
+ cancel: [{ action: 'cancel-shot' }]
+ }, {
+ name: 'context-menu',
+ start: { action: 'start-shot', label: 'context-menu' },
+ end: { action: 'internal', label: 'unhide-preselection-frame' },
+ cancel: [{ action: 'cancel-shot' }]
+ }, {
+ name: 'capture-full-page',
+ start: { action: 'capture-full-page' },
+ end: { action: 'internal', label: 'unhide-preview-frame' },
+ cancel: [{ action: 'cancel-shot' }]
+ }, {
+ name: 'capture-visible',
+ start: { action: 'capture-visible' },
+ end: { action: 'internal', label: 'unhide-preview-frame' },
+ cancel: [{ action: 'cancel-shot' }]
+ }, {
+ name: 'make-selection',
+ start: { action: 'make-selection' },
+ end: { action: 'internal', label: 'unhide-selection-frame' },
+ cancel: [{ action: 'cancel-shot' }]
+ }, {
+ name: 'save-shot',
+ start: { action: 'save-shot' },
+ end: { action: 'internal', label: 'open-shot-tab' },
+ cancel: [{ action: 'cancel-shot' }, { action: 'upload-failed' }]
+ }, {
+ name: 'save-visible',
+ start: { action: 'save-visible' },
+ end: { action: 'internal', label: 'open-shot-tab' },
+ cancel: [{ action: 'cancel-shot' }, { action: 'upload-failed' }]
+ }, {
+ name: 'save-full-page',
+ start: { action: 'save-full-page' },
+ end: { action: 'internal', label: 'open-shot-tab' },
+ cancel: [{ action: 'cancel-shot' }, { action: 'upload-failed' }]
+ }, {
+ name: 'save-full-page-truncated',
+ start: { action: 'save-full-page-truncated' },
+ end: { action: 'internal', label: 'open-shot-tab' },
+ cancel: [{ action: 'cancel-shot' }, { action: 'upload-failed' }]
+ }, {
+ name: 'download-shot',
+ start: { action: 'download-shot' },
+ end: { action: 'internal', label: 'deactivate' },
+ cancel: [{ action: 'cancel-shot' }]
+ }, {
+ name: 'download-full-page',
+ start: { action: 'download-full-page' },
+ end: { action: 'internal', label: 'deactivate' },
+ cancel: [{ action: 'cancel-shot' }]
+ }, {
+ name: 'download-full-page-truncated',
+ start: { action: 'download-full-page-truncated' },
+ end: { action: 'internal', label: 'deactivate' },
+ cancel: [{ action: 'cancel-shot' }]
+ }, {
+ name: 'download-visible',
+ start: { action: 'download-visible' },
+ end: { action: 'internal', label: 'deactivate' },
+ cancel: [{ action: 'cancel-shot' }]
+ }];
+
+ // 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));
+ }
+
+ 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);
+ delete timingData[r.name];
+ }
+ });
+ }
+
return exports;
})();
--- a/browser/extensions/screenshots/webextension/background/main.js
+++ b/browser/extensions/screenshots/webextension/background/main.js
@@ -48,19 +48,16 @@ this.main = (function() {
if (/^https?:\/\//.test(permission)) {
exports.setBackend(permission);
break;
}
}
function setIconActive(active, tabId) {
let path = active ? "icons/icon-highlight-32-v2.svg" : "icons/icon-32-v2.svg";
- if ((!hasSeenOnboarding) && !active) {
- path = "icons/icon-starred-32-v2.svg";
- }
startBackground.photonPageActionPort.postMessage({
type: "setProperties",
iconPath: path
});
}
function toggleSelector(tab) {
return analytics.refreshTelemetryPref()
@@ -91,22 +88,16 @@ this.main = (function() {
}
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 (tab.incognito) {
- senderror.showError({
- popupMessage: "PRIVATE_WINDOW"
- });
- return;
- }
if (shouldOpenMyShots(tab.url)) {
if (!hasSeenOnboarding) {
catcher.watchPromise(analytics.refreshTelemetryPref().then(() => {
sendEvent("goto-onboarding", "selection-button");
return forceOnboarding();
}));
return;
}
@@ -136,22 +127,16 @@ this.main = (function() {
return browser.tabs.create({url: getOnboardingUrl()});
}
exports.onClickedContextMenu = catcher.watchFunction((info, tab) => {
if (!tab) {
// Not in a page/tab context, ignore
return;
}
- if (tab.incognito) {
- senderror.showError({
- popupMessage: "PRIVATE_WINDOW"
- });
- return;
- }
if (!urlEnabled(tab.url)) {
senderror.showError({
popupMessage: "UNSHOOTABLE_PAGE"
});
return;
}
catcher.watchPromise(
toggleSelector(tab)
@@ -215,37 +200,58 @@ this.main = (function() {
type: "basic",
iconUrl: "../icons/copy.png",
title: browser.i18n.getMessage("notificationLinkCopiedTitle"),
message: browser.i18n.getMessage("notificationLinkCopiedDetails", pasteSymbol)
});
}
});
+ 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'));
+ return browser.notifications.create({
+ type: "basic",
+ iconUrl: "../icons/copy.png",
+ title: browser.i18n.getMessage("notificationImageCopiedTitle"),
+ message: browser.i18n.getMessage("notificationImageCopiedDetails", pasteSymbol)
+ });
+ });
+ })
+ });
+
communication.register("downloadShot", (sender, info) => {
// 'data:' urls don't work directly, let's use a Blob
// see http://stackoverflow.com/questions/40269862/save-data-uri-as-file-using-downloads-download-api
const blob = blobConverters.dataUrlToBlob(info.url);
let url = URL.createObjectURL(blob);
let downloadId;
let onChangedCallback = catcher.watchFunction(function(change) {
if (!downloadId || downloadId != change.id) {
return;
}
if (change.state && change.state.current != "in_progress") {
URL.revokeObjectURL(url);
browser.downloads.onChanged.removeListener(onChangedCallback);
}
});
browser.downloads.onChanged.addListener(onChangedCallback)
- return browser.downloads.download({
- url,
- filename: info.filename
- }).then((id) => {
- downloadId = id;
+ catcher.watchPromise(communication.sendToBootstrap("incrementDownloadCount"));
+ return browser.windows.getLastFocused().then(windowInfo => {
+ return windowInfo.incognito;
+ }).then((incognito) => {
+ return browser.downloads.download({
+ url,
+ incognito,
+ filename: info.filename
+ }).then((id) => {
+ downloadId = id;
+ });
});
});
communication.register("closeSelector", (sender) => {
setIconActive(false, sender.tab.id);
});
catcher.watchPromise(communication.sendToBootstrap("getOldDeviceInfo").then((deviceInfo) => {
@@ -304,10 +310,22 @@ this.main = (function() {
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
@@ -1,9 +1,9 @@
-/* globals catcher, log */
+/* globals catcher, communication, log */
"use strict";
var global = this;
this.selectorLoader = (function() {
const exports = {};
@@ -60,32 +60,53 @@ this.selectorLoader = (function() {
}).then(result => {
return result && result[0];
});
};
let loadingTabs = new Set();
exports.loadModules = function(tabId, hasSeenOnboarding) {
- let promise;
loadingTabs.add(tabId);
+ let promise = downloadOnlyCheck(tabId);
if (hasSeenOnboarding) {
- promise = executeModules(tabId, standardScripts.concat(selectorScripts));
+ promise = promise.then(() => {
+ return executeModules(tabId, standardScripts.concat(selectorScripts));
+ });
} else {
- promise = executeModules(tabId, standardScripts.concat(onboardingScripts).concat(selectorScripts));
+ promise = promise.then(() => {
+ return executeModules(tabId, standardScripts.concat(onboardingScripts).concat(selectorScripts));
+ });
}
return promise.then((result) => {
loadingTabs.delete(tabId);
return result;
}, (error) => {
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"
+ });
+ });
+ });
+ }
+
function executeModules(tabId, scripts) {
let lastPromise = Promise.resolve(null);
scripts.forEach((file) => {
lastPromise = lastPromise.then(() => {
return browser.tabs.executeScript(tabId, {
file,
runAt: "document_start"
}).catch((error) => {
--- a/browser/extensions/screenshots/webextension/background/senderror.js
+++ b/browser/extensions/screenshots/webextension/background/senderror.js
@@ -1,14 +1,12 @@
-/* globals analytics, communication, makeUuid, Raven, catcher, auth, log */
+/* globals startBackground, analytics, communication, makeUuid, Raven, catcher, auth, log */
"use strict";
-const startTime = Date.now();
-
this.senderror = (function() {
let exports = {};
let manifest = browser.runtime.getManifest();
// Do not show an error more than every ERROR_TIME_LIMIT milliseconds:
const ERROR_TIME_LIMIT = 3000;
@@ -74,17 +72,17 @@ this.senderror = (function() {
let showMessage = messages[popupMessage].showMessage;
if (error.message && showMessage) {
if (message) {
message += "\n" + error.message;
} else {
message = error.message;
}
}
- if (Date.now() - startTime > 5 * 1000) {
+ if (Date.now() - startBackground.startTime > 5 * 1000) {
browser.notifications.create(id, {
type: "basic",
// FIXME: need iconUrl for an image, see #2239
title,
message
});
}
};
--- a/browser/extensions/screenshots/webextension/background/startBackground.js
+++ b/browser/extensions/screenshots/webextension/background/startBackground.js
@@ -1,19 +1,20 @@
/* globals browser, main, communication */
/* This file handles:
clicks on the Photon page action
browser.contextMenus.onClicked
browser.runtime.onMessage
and loads the rest of the background page in response to those events, forwarding
the events to main.onClicked, main.onClickedContextMenu, or communication.onMessage
*/
+const startTime = Date.now();
this.startBackground = (function() {
- let exports = {};
+ let exports = {startTime};
const backgroundScripts = [
"log.js",
"makeUuid.js",
"catcher.js",
"blobConverters.js",
"background/selectorLoader.js",
"background/communication.js",
@@ -40,34 +41,16 @@ this.startBackground = (function() {
browser.contextMenus.onClicked.addListener((info, tab) => {
loadIfNecessary().then(() => {
main.onClickedContextMenu(info, tab);
}).catch((error) => {
console.error("Error loading Screenshots:", error);
});
});
- // Note this duplicates functionality in main.js, but we need to change
- // the onboarding icon before main.js loads up
- let iconPath = null;
- browser.storage.local.get(["hasSeenOnboarding"]).then((result) => {
- let hasSeenOnboarding = !!result.hasSeenOnboarding;
- if (!hasSeenOnboarding) {
- iconPath = "icons/icon-starred-32-v2.svg";
- if (photonPageActionPort) {
- photonPageActionPort.postMessage({
- type: "setProperties",
- iconPath
- });
- }
- }
- }).catch((error) => {
- console.error("Error loading Screenshots onboarding flag:", error);
- });
-
browser.runtime.onMessage.addListener((req, sender, sendResponse) => {
loadIfNecessary().then(() => {
return communication.onMessage(req, sender, sendResponse);
}).catch((error) => {
console.error("Error loading Screenshots:", error);
});
return true;
});
@@ -144,18 +127,17 @@ this.startBackground = (function() {
break;
default:
console.error("Unrecognized message:", message);
break;
}
});
photonPageActionPort.postMessage({
type: "setProperties",
- title: browser.i18n.getMessage("contextMenuLabel"),
- iconPath
+ title: browser.i18n.getMessage("contextMenuLabel")
});
// Export these so that main.js can use them.
Object.defineProperties(exports, {
"photonPageActionPort": {
enumerable: true,
get() {
return photonPageActionPort;
--- a/browser/extensions/screenshots/webextension/background/takeshot.js
+++ b/browser/extensions/screenshots/webextension/background/takeshot.js
@@ -52,31 +52,33 @@ this.takeshot = (function() {
shot.abTests = shotAbTests;
}
return catcher.watchPromise(capturePromise.then(() => {
return convertBlobPromise;
}).then(() => {
return browser.tabs.create({url: shot.creatingUrl})
}).then((tab) => {
openedTab = tab;
+ sendEvent('internal', 'open-shot-tab');
return uploadShot(shot, imageBlob);
}).then(() => {
return browser.tabs.update(openedTab.id, {url: shot.viewUrl}).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'));
return shot.viewUrl;
}).catch((error) => {
browser.tabs.remove(openedTab.id);
throw error;
}));
}));
communication.register("screenshotPage", (sender, selectedPos, scroll) => {
--- a/browser/extensions/screenshots/webextension/build/buildSettings.js
+++ b/browser/extensions/screenshots/webextension/build/buildSettings.js
@@ -1,11 +1,11 @@
window.buildSettings = {
defaultSentryDsn: "https://904ccdd4866247c092ae8fc1a4764a63:940d44bdc71d4daea133c19080ccd38d@sentry.prod.mozaws.net/224",
logLevel: "" || "warn",
captureText: ("" === "true"),
uploadBinary: ("" === "true"),
pngToJpegCutoff: parseInt("" || 2500000, 10),
- maxImageHeight: parseInt("" || 5000, 10),
- maxImageWidth: parseInt("" || 5000, 10)
+ maxImageHeight: parseInt("" || 10000, 10),
+ maxImageWidth: parseInt("" || 10000, 10)
};
null;
--- a/browser/extensions/screenshots/webextension/build/inlineSelectionCss.js
+++ b/browser/extensions/screenshots/webextension/build/inlineSelectionCss.js
@@ -1,11 +1,11 @@
/* Created from build/server/static/css/inline-selection.css */
window.inlineSelectionCss = `
-.button, .highlight-button-cancel, .highlight-button-save, .highlight-button-download, .preview-button-save {
+.button, .highlight-button-cancel, .highlight-button-save, .highlight-button-download, .highlight-button-copy, .preview-button-save {
display: flex;
align-items: center;
justify-content: center;
border: 0;
border-radius: 3px;
cursor: pointer;
font-size: 16px;
font-weight: 400;
@@ -14,90 +14,90 @@ window.inlineSelectionCss = `
outline: none;
padding: 0 10px;
position: relative;
text-align: center;
text-decoration: none;
transition: background 150ms cubic-bezier(0.07, 0.95, 0, 1), border 150ms cubic-bezier(0.07, 0.95, 0, 1);
user-select: none;
white-space: nowrap; }
- .button.small, .small.highlight-button-cancel, .small.highlight-button-save, .small.highlight-button-download, .small.preview-button-save {
+ .button.small, .small.highlight-button-cancel, .small.highlight-button-save, .small.highlight-button-download, .small.highlight-button-copy, .small.preview-button-save {
height: 32px;
line-height: 32px;
padding: 0 8px; }
- .button.tiny, .tiny.highlight-button-cancel, .tiny.highlight-button-save, .tiny.highlight-button-download, .tiny.preview-button-save {
+ .button.tiny, .tiny.highlight-button-cancel, .tiny.highlight-button-save, .tiny.highlight-button-download, .tiny.highlight-button-copy, .tiny.preview-button-save {
font-size: 14px;
height: 26px;
border: 1px solid #c7c7c7; }
- .button.tiny:hover, .tiny.highlight-button-cancel:hover, .tiny.highlight-button-save:hover, .tiny.highlight-button-download:hover, .tiny.preview-button-save:hover, .button.tiny:focus, .tiny.highlight-button-cancel:focus, .tiny.highlight-button-save:focus, .tiny.highlight-button-download:focus, .tiny.preview-button-save:focus {
+ .button.tiny:hover, .tiny.highlight-button-cancel:hover, .tiny.highlight-button-save:hover, .tiny.highlight-button-download:hover, .tiny.highlight-button-copy:hover, .tiny.preview-button-save:hover, .button.tiny:focus, .tiny.highlight-button-cancel:focus, .tiny.highlight-button-save:focus, .tiny.highlight-button-download:focus, .tiny.highlight-button-copy:focus, .tiny.preview-button-save:focus {
background: #ededf0;
border-color: #989898; }
- .button.tiny:active, .tiny.highlight-button-cancel:active, .tiny.highlight-button-save:active, .tiny.highlight-button-download:active, .tiny.preview-button-save:active {
+ .button.tiny:active, .tiny.highlight-button-cancel:active, .tiny.highlight-button-save:active, .tiny.highlight-button-download:active, .tiny.highlight-button-copy:active, .tiny.preview-button-save:active {
background: #dedede;
border-color: #989898; }
- .button.block-button, .block-button.highlight-button-cancel, .block-button.highlight-button-save, .block-button.highlight-button-download, .block-button.preview-button-save {
+ .button.block-button, .block-button.highlight-button-cancel, .block-button.highlight-button-save, .block-button.highlight-button-download, .block-button.highlight-button-copy, .block-button.preview-button-save {
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
border: 0;
border-right: 1px solid #c7c7c7;
box-shadow: 0;
border-radius: 0;
flex-shrink: 0;
font-size: 20px;
height: 100px;
line-height: 100%;
overflow: hidden; }
@media (max-width: 719px) {
- .button.block-button, .block-button.highlight-button-cancel, .block-button.highlight-button-save, .block-button.highlight-button-download, .block-button.preview-button-save {
+ .button.block-button, .block-button.highlight-button-cancel, .block-button.highlight-button-save, .block-button.highlight-button-download, .block-button.highlight-button-copy, .block-button.preview-button-save {
justify-content: flex-start;
font-size: 16px;
height: 72px;
margin-right: 10px;
padding: 0 5px; } }
- .button.block-button:hover, .block-button.highlight-button-cancel:hover, .block-button.highlight-button-save:hover, .block-button.highlight-button-download:hover, .block-button.preview-button-save:hover {
+ .button.block-button:hover, .block-button.highlight-button-cancel:hover, .block-button.highlight-button-save:hover, .block-button.highlight-button-download:hover, .block-button.highlight-button-copy:hover, .block-button.preview-button-save:hover {
background: #ededf0; }
- .button.block-button:active, .block-button.highlight-button-cancel:active, .block-button.highlight-button-save:active, .block-button.highlight-button-download:active, .block-button.preview-button-save:active {
+ .button.block-button:active, .block-button.highlight-button-cancel:active, .block-button.highlight-button-save:active, .block-button.highlight-button-download:active, .block-button.highlight-button-copy:active, .block-button.preview-button-save:active {
background: #dedede; }
- .button.download, .download.highlight-button-cancel, .download.highlight-button-save, .download.highlight-button-download, .download.preview-button-save, .button.edit, .edit.highlight-button-cancel, .edit.highlight-button-save, .edit.highlight-button-download, .edit.preview-button-save, .button.trash, .trash.highlight-button-cancel, .trash.highlight-button-save, .trash.highlight-button-download, .trash.preview-button-save, .button.share, .share.highlight-button-cancel, .share.highlight-button-save, .share.highlight-button-download, .share.preview-button-save, .button.flag, .flag.highlight-button-cancel, .flag.highlight-button-save, .flag.highlight-button-download, .flag.preview-button-save {
+ .button.download, .download.highlight-button-cancel, .download.highlight-button-save, .download.highlight-button-download, .download.highlight-button-copy, .download.preview-button-save, .button.edit, .edit.highlight-button-cancel, .edit.highlight-button-save, .edit.highlight-button-download, .edit.highlight-button-copy, .edit.preview-button-save, .button.trash, .trash.highlight-button-cancel, .trash.highlight-button-save, .trash.highlight-button-download, .trash.highlight-button-copy, .trash.preview-button-save, .button.share, .share.highlight-button-cancel, .share.highlight-button-save, .share.highlight-button-download, .share.highlight-button-copy, .share.preview-button-save, .button.flag, .flag.highlight-button-cancel, .flag.highlight-button-save, .flag.highlight-button-download, .flag.highlight-button-copy, .flag.preview-button-save {
background-repeat: no-repeat;
background-size: 50%;
background-position: center;
margin-right: 10px;
transition: background-color 150ms cubic-bezier(0.07, 0.95, 0, 1); }
- .button.download, .download.highlight-button-cancel, .download.highlight-button-save, .download.highlight-button-download, .download.preview-button-save {
+ .button.download, .download.highlight-button-cancel, .download.highlight-button-save, .download.highlight-button-download, .download.highlight-button-copy, .download.preview-button-save {
background-image: url("../img/icon-download.svg"); }
- .button.download:hover, .download.highlight-button-cancel:hover, .download.highlight-button-save:hover, .download.highlight-button-download:hover, .download.preview-button-save:hover {
+ .button.download:hover, .download.highlight-button-cancel:hover, .download.highlight-button-save:hover, .download.highlight-button-download:hover, .download.highlight-button-copy:hover, .download.preview-button-save:hover {
background-color: #ededf0; }
- .button.download:active, .download.highlight-button-cancel:active, .download.highlight-button-save:active, .download.highlight-button-download:active, .download.preview-button-save:active {
+ .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.preview-button-save {
+ .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.preview-button-save:hover {
+ .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.preview-button-save, .button.share:active, .share.highlight-button-cancel:active, .share.highlight-button-save:active, .share.highlight-button-download:active, .share.preview-button-save:active {
+ .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.trash, .trash.highlight-button-cancel, .trash.highlight-button-save, .trash.highlight-button-download, .trash.preview-button-save {
+ .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.preview-button-save:hover {
+ .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.preview-button-save:active {
+ .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.preview-button-save {
+ .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"); }
- .button.edit:hover, .edit.highlight-button-cancel:hover, .edit.highlight-button-save:hover, .edit.highlight-button-download:hover, .edit.preview-button-save:hover {
+ .button.edit:hover, .edit.highlight-button-cancel:hover, .edit.highlight-button-save:hover, .edit.highlight-button-download:hover, .edit.highlight-button-copy:hover, .edit.preview-button-save:hover {
background-color: #ededf0; }
- .button.edit:active, .edit.highlight-button-cancel:active, .edit.highlight-button-save:active, .edit.highlight-button-download:active, .edit.preview-button-save:active {
+ .button.edit:active, .edit.highlight-button-cancel:active, .edit.highlight-button-save:active, .edit.highlight-button-download:active, .edit.highlight-button-copy:active, .edit.preview-button-save:active {
background-color: #dedede; }
- .button.flag, .flag.highlight-button-cancel, .flag.highlight-button-save, .flag.highlight-button-download, .flag.preview-button-save {
+ .button.flag, .flag.highlight-button-cancel, .flag.highlight-button-save, .flag.highlight-button-download, .flag.highlight-button-copy, .flag.preview-button-save {
background-image: url("../img/icon-flag.svg"); }
- .button.flag:hover, .flag.highlight-button-cancel:hover, .flag.highlight-button-save:hover, .flag.highlight-button-download:hover, .flag.preview-button-save:hover {
+ .button.flag:hover, .flag.highlight-button-cancel:hover, .flag.highlight-button-save:hover, .flag.highlight-button-download:hover, .flag.highlight-button-copy:hover, .flag.preview-button-save:hover {
background-color: #ededf0; }
- .button.flag:active, .flag.highlight-button-cancel:active, .flag.highlight-button-save:active, .flag.highlight-button-download:active, .flag.preview-button-save:active {
+ .button.flag:active, .flag.highlight-button-cancel:active, .flag.highlight-button-save:active, .flag.highlight-button-download:active, .flag.highlight-button-copy:active, .flag.preview-button-save:active {
background-color: #dedede; }
.inverse-color-scheme {
background: #38383d;
color: #f9f9fa; }
.inverse-color-scheme a {
color: #e1e1e6; }
@@ -118,46 +118,46 @@ window.inlineSelectionCss = `
background: #38383d;
color: #f9f9fa; }
.alt-color-scheme h1 {
color: #6f7fb6; }
.alt-color-scheme a {
color: #e1e1e6;
text-decoration: underline; }
-.button.primary, .primary.highlight-button-cancel, .highlight-button-save, .primary.highlight-button-download, .preview-button-save {
+.button.primary, .primary.highlight-button-cancel, .highlight-button-save, .primary.highlight-button-download, .primary.highlight-button-copy, .preview-button-save {
background-color: #009ec0;
color: #fff; }
- .button.primary:hover, .primary.highlight-button-cancel:hover, .highlight-button-save:hover, .primary.highlight-button-download:hover, .preview-button-save:hover, .button.primary:focus, .primary.highlight-button-cancel:focus, .highlight-button-save:focus, .primary.highlight-button-download:focus, .preview-button-save:focus {
+ .button.primary:hover, .primary.highlight-button-cancel:hover, .highlight-button-save:hover, .primary.highlight-button-download:hover, .primary.highlight-button-copy:hover, .preview-button-save:hover, .button.primary:focus, .primary.highlight-button-cancel:focus, .highlight-button-save:focus, .primary.highlight-button-download:focus, .primary.highlight-button-copy:focus, .preview-button-save:focus {
background-color: #00819c; }
- .button.primary:active, .primary.highlight-button-cancel:active, .highlight-button-save:active, .primary.highlight-button-download:active, .preview-button-save:active {
+ .button.primary:active, .primary.highlight-button-cancel:active, .highlight-button-save:active, .primary.highlight-button-download:active, .primary.highlight-button-copy:active, .preview-button-save:active {
background-color: #006c83; }
-.button.secondary, .highlight-button-cancel, .secondary.highlight-button-save, .highlight-button-download, .secondary.preview-button-save {
+.button.secondary, .highlight-button-cancel, .secondary.highlight-button-save, .highlight-button-download, .highlight-button-copy, .secondary.preview-button-save {
background-color: #f9f9fa;
color: #38383d; }
- .button.secondary:hover, .highlight-button-cancel:hover, .secondary.highlight-button-save:hover, .highlight-button-download:hover, .secondary.preview-button-save:hover {
+ .button.secondary:hover, .highlight-button-cancel:hover, .secondary.highlight-button-save:hover, .highlight-button-download:hover, .highlight-button-copy:hover, .secondary.preview-button-save:hover {
background-color: #ededf0; }
- .button.secondary:active, .highlight-button-cancel:active, .secondary.highlight-button-save:active, .highlight-button-download:active, .secondary.preview-button-save:active {
+ .button.secondary:active, .highlight-button-cancel:active, .secondary.highlight-button-save:active, .highlight-button-download:active, .highlight-button-copy:active, .secondary.preview-button-save:active {
background-color: #dedede; }
-.button.transparent, .transparent.highlight-button-cancel, .transparent.highlight-button-save, .transparent.highlight-button-download, .transparent.preview-button-save {
+.button.transparent, .transparent.highlight-button-cancel, .transparent.highlight-button-save, .transparent.highlight-button-download, .transparent.highlight-button-copy, .transparent.preview-button-save {
background-color: transparent;
color: #38383d; }
- .button.transparent:hover, .transparent.highlight-button-cancel:hover, .transparent.highlight-button-save:hover, .transparent.highlight-button-download:hover, .transparent.preview-button-save:hover {
+ .button.transparent:hover, .transparent.highlight-button-cancel:hover, .transparent.highlight-button-save:hover, .transparent.highlight-button-download:hover, .transparent.highlight-button-copy:hover, .transparent.preview-button-save:hover {
background-color: #ededf0; }
- .button.transparent:focus, .transparent.highlight-button-cancel:focus, .transparent.highlight-button-save:focus, .transparent.highlight-button-download:focus, .transparent.preview-button-save:focus, .button.transparent:active, .transparent.highlight-button-cancel:active, .transparent.highlight-button-save:active, .transparent.highlight-button-download:active, .transparent.preview-button-save:active {
+ .button.transparent:focus, .transparent.highlight-button-cancel:focus, .transparent.highlight-button-save:focus, .transparent.highlight-button-download:focus, .transparent.highlight-button-copy:focus, .transparent.preview-button-save:focus, .button.transparent:active, .transparent.highlight-button-cancel:active, .transparent.highlight-button-save:active, .transparent.highlight-button-download:active, .transparent.highlight-button-copy:active, .transparent.preview-button-save:active {
background-color: #dedede; }
-.button.warning, .warning.highlight-button-cancel, .warning.highlight-button-save, .warning.highlight-button-download, .warning.preview-button-save {
+.button.warning, .warning.highlight-button-cancel, .warning.highlight-button-save, .warning.highlight-button-download, .warning.highlight-button-copy, .warning.preview-button-save {
color: #fff;
background: #d92215; }
- .button.warning:hover, .warning.highlight-button-cancel:hover, .warning.highlight-button-save:hover, .warning.highlight-button-download:hover, .warning.preview-button-save:hover, .button.warning:focus, .warning.highlight-button-cancel:focus, .warning.highlight-button-save:focus, .warning.highlight-button-download:focus, .warning.preview-button-save:focus {
+ .button.warning:hover, .warning.highlight-button-cancel:hover, .warning.highlight-button-save:hover, .warning.highlight-button-download:hover, .warning.highlight-button-copy:hover, .warning.preview-button-save:hover, .button.warning:focus, .warning.highlight-button-cancel:focus, .warning.highlight-button-save:focus, .warning.highlight-button-download:focus, .warning.highlight-button-copy:focus, .warning.preview-button-save:focus {
background: #b81d12; }
- .button.warning:active, .warning.highlight-button-cancel:active, .warning.highlight-button-save:active, .warning.highlight-button-download:active, .warning.preview-button-save:active {
+ .button.warning:active, .warning.highlight-button-cancel:active, .warning.highlight-button-save:active, .warning.highlight-button-download:active, .warning.highlight-button-copy:active, .warning.preview-button-save:active {
background: #a11910; }
.subtitle-link {
color: #009ec0; }
.loader {
background: #2e2d30;
border-radius: 2px;
@@ -360,16 +360,85 @@ window.inlineSelectionCss = `
.bghighlight {
background-color: rgba(0, 0, 0, 0.7);
position: absolute;
z-index: 9999999999; }
body.hcm .bghighlight {
background-color: black;
opacity: 0.7; }
+.notice {
+ display: flex;
+ height: 41px;
+ left: 50%;
+ position: fixed;
+ transform: translate(-50%, 0);
+ transition: top 125ms ease-out, translate 125ms ease-out;
+ user-select: none;
+ will-change: top, translate;
+ z-index: 10000000000; }
+ .notice .notice-wrapper {
+ align-items: center;
+ background: #737373;
+ border-radius: 100px;
+ display: flex;
+ flex-wrap: nowrap;
+ justify-content: space-between;
+ padding: 10px 15px; }
+ .notice .notice-content {
+ color: #fff;
+ flex: 1;
+ font-family: -apple-system, BlinkMacSystemFont, "segoe ui", "helvetica neue", helvetica, ubuntu, roboto, noto, arial, sans-serif;
+ font-size: 14px;
+ font-weight: bold;
+ white-space: nowrap; }
+ .notice .notice-help {
+ background-image: url("MOZ_EXTENSION/icons/help-16.svg");
+ background-position: center center;
+ background-repeat: no-repeat;
+ height: 16px;
+ width: 16px; }
+
+.notice-tooltip {
+ background: #fff;
+ border-radius: 3px;
+ border: 1px solid #9d9d9e;
+ bottom: 60px;
+ color: #000;
+ cursor: default;
+ display: none;
+ font-family: -apple-system, BlinkMacSystemFont, "segoe ui", "helvetica neue", helvetica, ubuntu, roboto, noto, arial, sans-serif;
+ font-size: 14px;
+ font-weight: normal;
+ line-height: 22px;
+ overflow-wrap: break-word;
+ padding: 15px;
+ position: absolute;
+ right: -14px;
+ white-space: normal;
+ width: 300px;
+ z-index: 10000000000;
+ /* down-arrow for the tooltip */ }
+ .notice:hover .notice-tooltip {
+ display: block; }
+ .notice-tooltip p {
+ margin: 0; }
+ .notice-tooltip ul {
+ margin-bottom: 0; }
+ .notice-tooltip::after {
+ border-left: 10px solid transparent;
+ border-right: 10px solid transparent;
+ border-top: 10px solid #f9f9fa;
+ content: "";
+ height: 0;
+ left: 86%;
+ position: absolute;
+ top: 100%;
+ width: 0; }
+
.preview-overlay {
align-items: center;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
height: 100%;
justify-content: center;
left: 0;
margin: 0;
@@ -424,31 +493,45 @@ window.inlineSelectionCss = `
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: 65px center; }
+ background-position: right 10px center; }
html[dir="ltr"] .highlight-button-save {
padding-left: 34px; }
html[dir="rtl"] .highlight-button-save {
padding-right: 40px; }
.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; }
+
+.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;
font-weight: bold;
font-family: -apple-system, BlinkMacSystemFont, "segoe ui", "helvetica neue", helvetica, ubuntu, roboto, noto, arial, sans-serif;
font-size: 70%;
color: #000;
@@ -547,29 +630,18 @@ window.inlineSelectionCss = `
justify-content: center;
animation: pulse 125mm cubic-bezier(0.07, 0.95, 0, 1);
color: #fff;
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; }
-
-#imageCroppedWarning {
- position: absolute;
- background: rgba(0, 0, 0, 0.8);
- bottom: 0;
- color: #fff;
- font-family: -apple-system, BlinkMacSystemFont, "segoe ui", "helvetica neue", helvetica, ubuntu, roboto, noto, arial, sans-serif;
- font-size: 12px;
- padding: 10px;
- text-align: center;
- width: 100%;
- z-index: 2; }
+ width: 400px;
+ user-select: none; }
.myshots-all-buttons-container {
display: flex;
flex-direction: row-reverse;
background: #f5f5f5;
border-radius: 2px;
box-sizing: border-box;
height: 80px;
--- a/browser/extensions/screenshots/webextension/build/shot.js
+++ b/browser/extensions/screenshots/webextension/build/shot.js
@@ -24,17 +24,17 @@ function isUrl(url) {
return true;
}
if ((/^chrome:.{0,8000}/i).test(url)) {
return true;
}
if ((/^view-source:/i).test(url)) {
return isUrl(url.substr("view-source:".length));
}
- return (/^https?:\/\/[a-z0-9.-_]{1,8000}[a-z0-9](:[0-9]{1,8000})?\/?/i).test(url);
+ return (/^https?:\/\/[a-z0-9._-]{1,8000}[a-z0-9](:[0-9]{1,8000})?\/?/i).test(url);
}
function isValidClipImageUrl(url) {
return isUrl(url) && !(url.indexOf(')') > -1);
}
function assertUrl(url) {
if (!url) {
@@ -43,17 +43,17 @@ function assertUrl(url) {
if (!isUrl(url)) {
let exc = new Error("Not a URL");
exc.scheme = url.split(":")[0];
throw exc;
}
}
function isSecureWebUri(url) {
- return (/^https?:\/\/[a-z0-9.-_]{1,8000}[a-z0-9](:[0-9]{1,8000})?\/?/i).test(url);
+ return (/^https?:\/\/[a-z0-9._-]{1,8000}[a-z0-9](:[0-9]{1,8000})?\/?/i).test(url);
}
function assertOrigin(url) {
assertUrl(url);
if (url.search(/^https?:/i) != -1) {
let match = (/^https?:\/\/[^/:]{1,4000}\/?$/i).exec(url);
if (!match) {
throw new Error("Bad origin, might include path");
@@ -124,17 +124,17 @@ function resolveUrl(base, url) {
return url;
}
if (url.indexOf("//") === 0) {
// Protocol-relative URL
return (/^https?:/i).exec(base)[0] + url;
}
if (url.indexOf("/") === 0) {
// Domain-relative URL
- return (/^https?:\/\/[a-z0-9.-_]{1,4000}/i).exec(base)[0] + url;
+ return (/^https?:\/\/[a-z0-9._-]{1,4000}/i).exec(base)[0] + url;
}
// Otherwise, a full relative URL
while (url.indexOf("./") === 0) {
url = url.substr(2);
}
if (!base) {
// It's not an absolute URL, and we don't have a base URL, so we have
// to throw away the URL
@@ -204,17 +204,17 @@ function makeRandomId() {
}
return id;
}
class AbstractShot {
constructor(backend, id, attrs) {
attrs = attrs || {};
- assert((/^[a-zA-Z0-9]{1,4000}\/[a-z0-9.-_]{1,4000}$/).test(id), "Bad ID (should be alphanumeric):", JSON.stringify(id));
+ assert((/^[a-zA-Z0-9]{1,4000}\/[a-z0-9._-]{1,4000}$/).test(id), "Bad ID (should be alphanumeric):", JSON.stringify(id));
this._backend = backend;
this._id = id;
this.origin = attrs.origin || null;
this.fullUrl = attrs.fullUrl || null;
if ((!attrs.fullUrl) && attrs.url) {
console.warn("Received deprecated attribute .url");
this.fullUrl = attrs.url;
}
@@ -384,17 +384,17 @@ class AbstractShot {
}
return clipFilename + extension;
}
get urlDisplay() {
if (!this.url) {
return null;
}
- if (this.url.search(/^https?/i) != -1) {
+ if (/^https?:\/\//i.test(this.url)) {
let txt = this.url;
txt = txt.replace(/^[a-z]{1,4000}:\/\//i, "");
txt = txt.replace(/\/.{0,4000}/, "");
txt = txt.replace(/^www\./i, "");
return txt;
} else if (this.url.startsWith("data:")) {
return "data:url";
}
--- a/browser/extensions/screenshots/webextension/domainFromUrl.js
+++ b/browser/extensions/screenshots/webextension/domainFromUrl.js
@@ -9,20 +9,20 @@ this.domainFromUrl = (function() {
return function urlDomainForId(location) { // eslint-disable-line no-unused-vars
let domain = location.hostname;
if (!domain) {
domain = location.origin.split(":")[0];
if (!domain) {
domain = "unknown";
}
}
- if (domain.search(/^[a-z0-9.-_]{1,1000}$/i) === -1) {
+ if (domain.search(/^[a-z0-9._-]{1,1000}$/i) === -1) {
// Probably a unicode domain; we could use punycode but it wouldn't decode
// well in the URL anyway. Instead we'll punt.
- domain = domain.replace(/[^a-z0-9.-_]/ig, "");
+ domain = domain.replace(/[^a-z0-9._-]/ig, "");
if (!domain) {
domain = "site";
}
}
return domain;
};
})();
new file mode 100644
--- /dev/null
+++ b/browser/extensions/screenshots/webextension/icons/copy.svg
@@ -0,0 +1,7 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+ <style>.st0{fill:#3e3d40}</style>
+ <path class="st0" fill="context-fill" d="M14.707 8.293l-3-3A1 1 0 0 0 11 5h-1V4a1 1 0 0 0-.293-.707l-3-3A1 1 0 0 0 6 0H3a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h3v3a2 2 0 0 0 2 2h5a2 2 0 0 0 2-2V9a1 1 0 0 0-.293-.707zM12.586 9H11V7.414zm-5-5H6V2.414zM6 7v2H3V2h2v2.5a.5.5 0 0 0 .5.5H8a2 2 0 0 0-2 2zm2 7V7h2v2.5a.5.5 0 0 0 .5.5H13v4z"></path>
+</svg>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/screenshots/webextension/icons/help-16.svg
@@ -0,0 +1,6 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+ <path fill="rgba(249, 249, 250, .8)" d="M8 1a7 7 0 1 0 7 7 7.008 7.008 0 0 0-7-7zm0 13a6 6 0 1 1 6-6 6.007 6.007 0 0 1-6 6zM8 3.125A2.7 2.7 0 0 0 5.125 6a.875.875 0 0 0 1.75 0c0-1 .6-1.125 1.125-1.125a1.105 1.105 0 0 1 1.13.744.894.894 0 0 1-.53 1.016A2.738 2.738 0 0 0 7.125 9v.337a.875.875 0 0 0 1.75 0v-.37a1.041 1.041 0 0 1 .609-.824A2.637 2.637 0 0 0 10.82 5.16 2.838 2.838 0 0 0 8 3.125zm0 7.625A1.25 1.25 0 1 0 9.25 12 1.25 1.25 0 0 0 8 10.75z"></path>
+</svg>
deleted file mode 100644
--- a/browser/extensions/screenshots/webextension/icons/icon-starred-32-v2.svg
+++ /dev/null
@@ -1,1 +0,0 @@
-<svg data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32"><title>icon-starred-32-v2</title><path fill="context-fill" fill-opacity="context-fill-opacity" d="M8 2a4 4 0 0 0-4 4h4zm6 0h-4v4h4zm14 22a4 4 0 0 0 4-4h-4zm-12.2.64l6 4.6a4 4 0 0 0 5.57-1l-8.09-6.04zm6.06-7.2L9.9 26a5 5 0 1 1-2.3-3.27l2.8-2L7.92 19A5 5 0 1 1 4 10.1V8h4v3a5 5 0 0 1 2 4 5 5 0 0 1-.06.58l3.94 2.76 4.46-3.29a9 9 0 0 0 3.52 2.39zM5 17.5A2.5 2.5 0 1 0 2.5 15 2.5 2.5 0 0 0 5 17.5zm0 12A2.5 2.5 0 1 0 2.5 27 2.5 2.5 0 0 0 5 29.5zM16.51 6H16V2h3.34a9 9 0 0 0-2.83 4zM28 17.49V18h4v-3.34a9 9 0 0 1-4 2.83z"/><circle fill="#00feff" fill-opacity="context-fill-opacity" cx="25" cy="9" r="7"/><path fill="#005a71" fill-opacity="context-fill-opacity" d="M25 4a.89.89 0 0 1 .89.69l.4 1.65.07.31.31-.09 1.64-.48a.91.91 0 0 1 1.11.54.87.87 0 0 1-.22 1L28 8.78l-.27.22.23.22 1.24 1.17a.87.87 0 0 1 .22 1 .91.91 0 0 1-1.11.54l-1.64-.48-.31-.09-.07.31-.4 1.65a.92.92 0 0 1-1.78 0l-.4-1.65-.07-.31-.31.09-1.64.48a.91.91 0 0 1-1.11-.54.87.87 0 0 1 .22-1L22 9.22l.27-.22-.27-.22-1.21-1.17a.87.87 0 0 1-.22-1 .91.91 0 0 1 1.11-.54l1.64.48.31.09.07-.31.4-1.65A.89.89 0 0 1 25 4"/></svg>
\ No newline at end of file
--- 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": "23.0.0",
+ "version": "25.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"
}
@@ -29,19 +29,21 @@
"sitehelper.js"
]
}
],
"web_accessible_resources": [
"blank.html",
"icons/cancel.svg",
"icons/download.svg",
+ "icons/copy.svg",
"icons/icon-256.png",
"icons/back.svg",
"icons/back-highlight.svg",
+ "icons/help-16.svg",
"icons/menu-fullpage.svg",
"icons/menu-visible.svg",
"icons/menu-myshot.svg",
"icons/onboarding-1.png",
"icons/onboarding-2.png",
"icons/onboarding-3.png",
"icons/onboarding-4.png",
"icons/onboarding-5.png",
--- a/browser/extensions/screenshots/webextension/selector/shooter.js
+++ b/browser/extensions/screenshots/webextension/selector/shooter.js
@@ -191,16 +191,24 @@ this.shooter = (function() { // eslint-d
location: selectedPos
}
});
ui.triggerDownload(dataUrl, shotObject.filename);
uicontrol.deactivate();
}));
};
+ exports.copyShot = function(selectedPos) {
+ let dataUrl = screenshotPage(selectedPos);
+ let blob = blobConverters.dataUrlToBlob(dataUrl);
+ catcher.watchPromise(callBackground("copyShotToClipboard", blob).then(() => {
+ uicontrol.deactivate();
+ }));
+ };
+
exports.sendEvent = function(...args) {
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
@@ -86,16 +86,45 @@ this.ui = (function() { // eslint-disabl
let computed = win.getComputedStyle(el);
// When Windows is in High Contrast mode, Firefox replaces background
// image URLs with the string "none".
result = computed && computed.backgroundImage === "none";
doc.body.removeChild(el);
return result;
}
+ let isDownloadOnly = exports.isDownloadOnly = function() {
+ return window.downloadOnly;
+ }
+
+ // the download notice is rendered in iframes that match the document height
+ // or the window height. If parent iframe matches window height, pass in true
+ 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>
+ </ul>
+ </div>
+ <tbody>
+ <tr class="notice-wrapper">
+ <td class="notice-content" data-l10n-id="downloadOnlyNotice"></td>
+ <td class="notice-help"></td>
+ </tr>
+ <tbody>`;
+ localizeText(notice);
+ if (initAtBottom) {
+ notice.style.bottom = '10px';
+ }
+ return notice;
+ }
+
function initializeIframe() {
let el = document.createElement("iframe");
el.src = browser.extension.getURL("blank.html");
el.style.zIndex = "99999999999";
el.style.border = "none";
el.style.top = "0";
el.style.left = "0";
el.style.margin = "0";
@@ -150,16 +179,17 @@ this.ui = (function() { // eslint-disabl
hide() {
this.element.style.display = "none";
this.stopSizeWatch();
},
unhide() {
this.updateElementSize();
this.element.style.display = "";
+ catcher.watchPromise(callBackground("sendEvent", "internal", "unhide-selection-frame"));
if (highContrastCheck(this.element.contentWindow)) {
this.element.contentDocument.body.classList.add("hcm");
}
this.initSizeWatch();
this.element.focus();
},
updateElementSize(force) {
@@ -169,31 +199,40 @@ this.ui = (function() { // eslint-disabl
const visible = this.element.style.display !== "none";
if (force && visible) {
this.element.style.display = "none";
}
let height = Math.max(
document.documentElement.clientHeight,
document.body.clientHeight,
document.documentElement.scrollHeight,
- document.body.scrollHeight,
- window.innerHeight);
+ document.body.scrollHeight);
if (height !== this.sizeTracking.lastHeight) {
this.sizeTracking.lastHeight = height;
this.element.style.height = height + "px";
}
+ // Do not use window.innerWidth since that includes the width of the
+ // scroll bar.
let width = Math.max(
document.documentElement.clientWidth,
document.body.clientWidth,
document.documentElement.scrollWidth,
- document.body.scrollWidth,
- window.innerWidth);
+ 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`;
+ }
}
if (force && visible) {
this.element.style.display = "";
}
},
initSizeWatch() {
this.stopSizeWatch();
@@ -259,42 +298,45 @@ this.ui = (function() { // eslint-disabl
<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>
<div class="myshots-all-buttons-container">
- <button class="myshots-button myshots-link" tabindex="1" data-l10n-id="myShotsLink"></button>
- <div class="spacer"></div>
+ ${isDownloadOnly() ? '' : `
+ <button class="myshots-button myshots-link" 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>
</div>
</div>
</div>
</body>`;
installHandlerOnDocument(this.document);
if (this.addClassName) {
this.document.body.className = this.addClassName;
}
this.document.documentElement.dir = browser.i18n.getMessage("@@bidi_dir");
this.document.documentElement.lang = browser.i18n.getMessage("@@ui_locale");
const overlay = this.document.querySelector(".preview-overlay");
localizeText(this.document);
- overlay.querySelector(".myshots-button").addEventListener(
- "click", watchFunction(assertIsTrusted(standardOverlayCallbacks.onOpenMyShots)));
+ 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)));
resolve();
}), {once: true});
document.body.appendChild(this.element);
- this.unhide();
} else {
resolve();
}
});
},
hide() {
window.removeEventListener("scroll", watchFunction(assertIsTrusted(this.onScroll)));
@@ -303,16 +345,17 @@ this.ui = (function() { // eslint-disabl
this.element.style.display = "none";
}
},
unhide() {
window.addEventListener("scroll", watchFunction(assertIsTrusted(this.onScroll)));
window.addEventListener("resize", this.onResize, true);
this.element.style.display = "";
+ catcher.watchPromise(callBackground("sendEvent", "internal", "unhide-preselection-frame"));
if (highContrastCheck(this.element.contentWindow)) {
this.element.contentDocument.body.classList.add("hcm");
}
this.element.focus();
},
onScroll() {
exports.HoverBox.hide();
@@ -332,16 +375,21 @@ this.ui = (function() { // eslint-disabl
remove() {
this.hide();
util.removeNode(this.element);
this.element = null;
this.document = null;
}
};
+ function getAttributeText(l10nID) {
+ let text = browser.i18n.getMessage(l10nID);
+ return text && text.replace('"', """);
+ }
+
let iframePreview = exports.iframePreview = {
element: null,
document: null,
display(installHandlerOnDocument, standardOverlayCallbacks) {
return new Promise((resolve, reject) => {
if (!this.element) {
this.element = initializeIframe();
this.element.id = "firefox-screenshots-preview-iframe";
@@ -356,32 +404,49 @@ this.ui = (function() { // eslint-disabl
<head>
<style>${substitutedCss}</style>
<title></title>
</head>
<body>
<div class="preview-overlay">
<div class="preview-image">
<div class="preview-buttons">
- <button class="highlight-button-cancel"></button>
- <button class="highlight-button-download"></button>
- <button class="preview-button-save" data-l10n-id="saveScreenshotSelectedArea"></button>
+ <button class="highlight-button-cancel"
+ title="${getAttributeText("cancelScreenshot")}"></button>
+ <button class="highlight-button-copy"
+ title="${getAttributeText("copyScreenshot")}"></button>
+ ${isDownloadOnly() ?
+ `<button class="highlight-button-download download-only-button"
+ title="${getAttributeText("downloadScreenshot")}"
+ data-l10n-id="downloadScreenshot"></button>` :
+ `<button class="highlight-button-download"
+ title="${getAttributeText("downloadScreenshot")}"></button>
+ <button class="preview-button-save"
+ title="${getAttributeText("saveScreenshotSelectedArea")}"
+ data-l10n-id="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");
localizeText(this.document);
const overlay = this.document.querySelector(".preview-overlay");
+ if (isDownloadOnly()) {
+ overlay.appendChild(renderDownloadNotice(true));
+ } else {
+ overlay.querySelector(".preview-button-save").addEventListener(
+ "click", watchFunction(assertIsTrusted(standardOverlayCallbacks.onSavePreview)));
+ }
+ overlay.querySelector(".highlight-button-copy").addEventListener(
+ "click", watchFunction(assertIsTrusted(standardOverlayCallbacks.onCopyPreview)));
overlay.querySelector(".highlight-button-download").addEventListener(
"click", watchFunction(assertIsTrusted(standardOverlayCallbacks.onDownloadPreview)));
- overlay.querySelector(".preview-button-save").addEventListener(
- "click", watchFunction(assertIsTrusted(standardOverlayCallbacks.onSavePreview)));
overlay.querySelector(".highlight-button-cancel").addEventListener(
"click", watchFunction(assertIsTrusted(standardOverlayCallbacks.cancel)));
resolve();
});
document.body.appendChild(this.element);
} else {
resolve();
}
@@ -391,16 +456,17 @@ this.ui = (function() { // eslint-disabl
hide() {
if (this.element) {
this.element.style.display = "none";
}
},
unhide() {
this.element.style.display = "";
+ catcher.watchPromise(callBackground("sendEvent", "internal", "unhide-preview-frame"));
this.element.focus();
},
remove() {
this.hide();
util.removeNode(this.element);
this.element = null;
this.document = null;
@@ -474,44 +540,54 @@ this.ui = (function() { // eslint-disabl
if (callbacks !== undefined && callbacks.cancel) {
// We use onclick here because we don't want addEventListener
// to add multiple event handlers to the same button
this.cancel.onclick = watchFunction(assertIsTrusted(callbacks.cancel));
this.cancel.style.display = "";
} else {
this.cancel.style.display = "none";
}
- if (callbacks !== undefined && callbacks.save) {
+ if (callbacks !== undefined && callbacks.save && this.save) {
// We use onclick here because we don't want addEventListener
// to add multiple event handlers to the same button
this.save.removeAttribute("disabled");
this.save.onclick = watchFunction(assertIsTrusted((e) => {
this.save.setAttribute("disabled", "true");
callbacks.save(e);
}));
this.save.style.display = "";
- } else {
+ } else if (this.save) {
this.save.style.display = "none";
}
if (callbacks !== undefined && callbacks.download) {
this.download.removeAttribute("disabled");
this.download.onclick = watchFunction(assertIsTrusted((e) => {
this.download.setAttribute("disabled", true);
callbacks.download(e);
e.preventDefault();
e.stopPropagation();
return false;
}));
this.download.style.display = "";
} else {
this.download.style.display = "none";
}
+ if (callbacks !== undefined && callbacks.copy) {
+ this.copy.removeAttribute("disabled");
+ this.copy.onclick = watchFunction(assertIsTrusted((e) => {
+ this.copy.setAttribute("disabled", true);
+ callbacks.copy(e);
+ e.preventDefault();
+ e.stopPropagation();
+ }));
+ this.copy.style.display = "";
+ } else {
+ this.copy.style.display = "none";
+ }
let bodyRect = getBodyRect();
- // Note, document.documentElement.scrollHeight is zero on some strange pages (such as the page created when you load an image):
- let docHeight = Math.max(document.documentElement.scrollHeight || 0, document.body.scrollHeight);
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");
@@ -534,43 +610,86 @@ this.ui = (function() { // eslint-disabl
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.bgTop.style.top = "0px";
this.bgTop.style.height = (pos.top - bodyRect.top) + "px";
this.bgTop.style.left = "0px";
this.bgTop.style.width = "100%";
this.bgBottom.style.top = (pos.bottom - bodyRect.top) + "px";
- this.bgBottom.style.height = docHeight - (pos.bottom - bodyRect.top) + "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.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.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;
+ this.downloadNotice.style.top = (currentYOffset + currentWinBottom - 60) + "px";
+ }
+ };
+
+ 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 = this.save.style.position = "fixed";
+ 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.save.style.left = ((pos.left - bodyRect.left) - 190) + "px";
- this.cancel.style.top = this.download.style.top = this.save.style.top = (pos.top - bodyRect.top) + "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 = this.save.style.position = "initial";
- this.cancel.style.top = this.download.style.top = this.save.style.top = 0;
- this.cancel.style.left = this.download.style.left = this.save.style.left = 0;
+ 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') {
+ clearTimeout(timer);
+ }
+ timer = setTimeout(cb, delay);
}
},
remove() {
- for (let name of ["el", "bgTop", "bgLeft", "bgRight", "bgBottom"]) {
+ if (this.downloadNotice) {
+ window.removeEventListener('scroll', this.windowChangeStop, true);
+ window.removeEventListener('resize', this.windowChangeStop, true);
+ }
+ for (let name of ["el", "bgTop", "bgLeft", "bgRight", "bgBottom", "downloadNotice"]) {
if (name in this) {
util.removeNode(this[name]);
this[name] = null;
}
}
},
_createEl() {
@@ -578,26 +697,42 @@ this.ui = (function() { // eslint-disabl
if (boxEl) {
return;
}
boxEl = makeEl("div", "highlight");
let buttons = makeEl("div", "highlight-buttons");
let cancel = makeEl("button", "highlight-button-cancel");
cancel.title = browser.i18n.getMessage("cancelScreenshot");
buttons.appendChild(cancel);
- let download = makeEl("button", "highlight-button-download");
- download.title = browser.i18n.getMessage("downloadScreenshot");
+
+ let copy = makeEl("button", "highlight-button-copy");
+ copy.title = browser.i18n.getMessage("copyScreenshot");
+ buttons.appendChild(copy);
+
+ let download, save;
+
+ if (isDownloadOnly()) {
+ download = makeEl("button", "highlight-button-download download-only-button");
+ 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");
+ save = makeEl("button", "highlight-button-save");
+ save.textContent = browser.i18n.getMessage("saveScreenshotSelectedArea");
+ save.title = browser.i18n.getMessage("saveScreenshotSelectedArea");
+ }
buttons.appendChild(download);
- let save = makeEl("button", "highlight-button-save");
- save.textContent = browser.i18n.getMessage("saveScreenshotSelectedArea");
- save.title = browser.i18n.getMessage("saveScreenshotSelectedArea");
- buttons.appendChild(save);
+ 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");
elTarget.appendChild(elMover);
boxEl.appendChild(elTarget);
}
@@ -605,16 +740,20 @@ this.ui = (function() { // eslint-disabl
iframe.document().body.appendChild(this.bgTop);
this.bgLeft = makeEl("div", "bghighlight");
iframe.document().body.appendChild(this.bgLeft);
this.bgRight = makeEl("div", "bghighlight");
iframe.document().body.appendChild(this.bgRight);
this.bgBottom = makeEl("div", "bghighlight");
iframe.document().body.appendChild(this.bgBottom);
iframe.document().body.appendChild(boxEl);
+ if (isDownloadOnly()) {
+ this.downloadNotice = renderDownloadNotice();
+ iframe.document().body.appendChild(this.downloadNotice);
+ }
this.el = boxEl;
},
draggerDirection(target) {
while (target) {
if (target.nodeType == document.ELEMENT_NODE) {
if (target.classList.contains("mover-target")) {
for (let name of movements) {
@@ -690,16 +829,19 @@ this.ui = (function() { // eslint-disabl
this.el.style.width = (rect.right - rect.left + 2) + "px";
this.el.style.height = (rect.bottom - rect.top + 2) + "px";
},
hide() {
if (this.el) {
this.el.style.display = "none";
}
+ if (this.downloadNotice) {
+ this.downloadNotice.display = "none";
+ }
},
remove() {
util.removeNode(this.el);
this.el = null;
}
};
@@ -727,20 +869,26 @@ this.ui = (function() { // eslint-disabl
}
};
exports.Preview = {
display(dataUrl, showCropWarning) {
let img = makeEl("IMG");
img.src = dataUrl;
iframe.document().querySelector(".preview-image").appendChild(img);
- if (showCropWarning) {
- let imageCroppedEl = makeEl("DIV");
- imageCroppedEl.id = "imageCroppedWarning";
- imageCroppedEl.textContent = browser.i18n.getMessage("imageCroppedWarning", buildSettings.maxImageHeight);
+ 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>
+ </tbody>`;
+ let contentCell = imageCroppedEl.getElementsByTagName("td");
+ contentCell[0].textContent = browser.i18n.getMessage("imageCroppedWarning", buildSettings.maxImageHeight);
iframe.document().querySelector(".preview-overlay").appendChild(imageCroppedEl);
}
}
};
/** Removes every UI this module creates */
exports.remove = function() {
for (let name in exports) {
--- a/browser/extensions/screenshots/webextension/selector/uicontrol.js
+++ b/browser/extensions/screenshots/webextension/selector/uicontrol.js
@@ -129,16 +129,19 @@ this.uicontrol = (function() {
sendEvent("cancel-shot", "overlay-cancel-button");
exports.deactivate();
}, save: () => {
sendEvent("save-shot", "overlay-save-button");
shooter.takeShot("selection", selectedPos);
}, download: () => {
sendEvent("download-shot", "overlay-download-button");
shooter.downloadShot(selectedPos);
+ }, copy: () => {
+ sendEvent("copy-shot", "overlay-copy-button");
+ shooter.copyShot(selectedPos);
}
};
let standardOverlayCallbacks = {
cancel: () => {
sendEvent("cancel-shot", "cancel-preview-button");
exports.deactivate();
},
@@ -150,17 +153,17 @@ this.uicontrol = (function() {
// Handled in communication.js
});
},
onClickVisible: () => {
sendEvent("capture-visible", "selection-button");
selectedPos = new Selection(
window.scrollX, window.scrollY,
window.scrollX + window.innerWidth, window.scrollY + window.innerHeight);
- captureType = 'visible';
+ captureType = "visible";
setState("previewing");
},
onClickFullPage: () => {
sendEvent("capture-full-page", "selection-button");
captureType = "fullPage";
let width = getDocumentWidth();
if (width > MAX_PAGE_WIDTH) {
captureType = "fullPageTruncated";
@@ -187,16 +190,20 @@ this.uicontrol = (function() {
if (captureType === "fullPageTruncated") {
captureType = "fullPage";
selectedPos = new Selection(
0, 0,
getDocumentWidth(), getDocumentHeight());
}
shooter.downloadShot(selectedPos);
+ },
+ onCopyPreview: () => {
+ sendEvent(`copy-${captureType.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()}`, "copy-preview-button");
+ shooter.copyShot(selectedPos);
}
};
/** Holds all the objects that handle events for each state: */
let stateHandlers = {};
function getState() {
return getState.state;
@@ -881,16 +888,17 @@ this.uicontrol = (function() {
}
function isFrameset() {
return document.body.tagName == "FRAMESET";
}
exports.deactivate = function() {
try {
+ sendEvent("internal", "deactivate");
setState("cancel");
callBackground('closeSelector');
selectorLoader.unloadModules();
} catch (e) {
log.error('Error in deactivate', e)
// Sometimes this fires so late that the document isn't available
// We don't care about the exception, so we swallow it here
}
@@ -953,26 +961,40 @@ this.uicontrol = (function() {
}
function beforeunloadHandler() {
sendEvent("cancel-shot", "tab-load");
exports.deactivate();
}
function keyupHandler(event) {
- if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) {
- // Modified
+ if (event.shiftKey || event.altKey) {
+ // unused modifier keys
return;
}
+ 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
+ });
+ }
if ((event.key || event.code) === "Escape") {
sendEvent("cancel-shot", "keyboard-escape");
exports.deactivate();
}
- if ((event.key || event.code) === "Enter") {
- if (getState.state === "selected") {
+ if ((event.key || event.code) === "Enter" && getState.state === "selected") {
+ if (ui.isDownloadOnly()) {
+ sendEvent("download-shot", "keyboard-enter");
+ shooter.downloadShot(selectedPos);
+ } else {
sendEvent("save-shot", "keyboard-enter");
shooter.takeShot("selection", selectedPos);
}
}
}
function removeHandlers() {
window.removeEventListener("beforeunload", beforeunloadHandler);
--- a/browser/extensions/screenshots/webextension/sitehelper.js
+++ b/browser/extensions/screenshots/webextension/sitehelper.js
@@ -1,26 +1,19 @@
-/* globals catcher, callBackground */
+/* globals catcher, callBackground, content */
/** This is a content script added to all screenshots.firefox.com pages, and allows the site to
communicate with the add-on */
"use strict";
this.sitehelper = (function() {
- let ContentXMLHttpRequest = XMLHttpRequest;
// This gives us the content's copy of XMLHttpRequest, instead of the wrapped
// copy that this content script gets:
- if (location.origin === "https://screenshots.firefox.com" ||
- location.origin === "http://localhost:10080") {
- // Note http://localhost:10080 is the default development server
- // This code should always run, unless this content script is
- // somehow run in a bad/malicious context
- ContentXMLHttpRequest = window.wrappedJSObject.XMLHttpRequest;
- }
+ let ContentXMLHttpRequest = content.XMLHttpRequest;
catcher.registerHandler((errorObj) => {
callBackground("reportError", errorObj);
});
function sendCustomEvent(name, detail) {
if (typeof detail == "object") {