-
Notifications
You must be signed in to change notification settings - Fork 8
/
background.js
293 lines (246 loc) · 9.34 KB
/
background.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
/*
Firefox addon "Save Screenshot"
Copyright (C) 2022 Manuel Reimer <[email protected]>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
'use strict';
// Fired if one of our context menu entries is clicked.
function ContextMenuClicked(aInfo) {
SendMessage(aInfo.menuItemId);
}
// Fired if toolbar button is clicked
function ToolbarButtonClicked() {
SendMessage("{}");
}
// Fired if shortcut is pressed
function CommandPressed(aName) {
const info = aName.split("-");
SendMessage('{"format": "' + info[1] + '", "region": "' + info[0] + '"}');
}
// Triggers UI update (toolbar button popup and context menu)
async function UpdateUI() {
// Get menu list
const menus = await GetMenuList();
//
// Update toolbar button popup
//
if (menus.length)
browser.browserAction.setPopup({popup: "popup/choose_format.html"});
else
browser.browserAction.setPopup({popup: ""});
//
// Update context menu
//
if (browser.contextMenus !== undefined) { // If not on Android
await browser.contextMenus.removeAll();
const prefs = await Storage.get();
if (prefs.show_contextmenu) {
const topmenu = browser.contextMenus.create({
id: "{}",
title: browser.i18n.getMessage("extensionName"),
contexts: ["page"]
});
menus.forEach((entry) => {
browser.contextMenus.create({
id: entry.data,
title: entry.label,
contexts: ["page"],
parentId: topmenu
});
});
}
}
}
// Register event listener to receive option update notifications and
// content script requests
browser.runtime.onMessage.addListener((data, sender) => {
// An option change with request for redraw happened
if (data.type == "OptionsChanged" && data.redraw)
UpdateUI();
// The content script requests us to take a screenshot
if (data.type == "TakeScreenshot")
TakeScreenshot(data, sender.tab);
});
const DOWNLOAD_CACHE = {};
async function TakeScreenshot(data, tab) {
const prefs = await Storage.get();
const formats = {png: "png", jpg: "jpeg", copy: "png"};
let content = await browser.tabs.captureTab(tab.id, {
format: formats[data.format],
quality: prefs.jpegquality,
rect: {
x: data.left,
y: data.top,
width: data.width,
height: data.height
}
});
// Handle copy to clipboard
if (data.format == "copy") {
const buffer = new DataURLParser(content).arrayBuffer();
await browser.clipboard.setImageData(buffer, "png");
if (prefs.copynotification)
browser.notifications.create("info-notification", {
"type": "basic",
"title": browser.i18n.getMessage("extensionName"),
"message": browser.i18n.getMessage("info_screenshot_copied")
});
}
// All other data formats have to be handled as downloads
else {
// Add image comment if we are allowed to
if (prefs.image_comment)
content = await ApplyImageComment(content, tab.title, tab.url);
const filename = GetDefaultFileName("saved_page", tab, prefs.filenameformat) + "." + data.format;
// The method "open" requires a temporary <a> hyperlink whose creation and
// handling has to be offloaded to our content script
if (prefs.savemethod == "open") {
await browser.tabs.sendMessage(tab.id, {
type: "TriggerOpen",
content: content,
filename: filename
});
}
// All other download types are handled with the "browser.downloads" API
else {
const blob = new DataURLParser(content).blob();
const options = {
filename: prefs.targetdir ? prefs.targetdir + "/" + filename: filename,
url: URL.createObjectURL(blob),
saveAs: (prefs.savemethod == "saveas")
};
// Trigger download
const id = await browser.downloads.download(options);
// Store download options for usage in "onChanged".
DOWNLOAD_CACHE[id] = options;
}
}
}
// Download change listener.
// Used to create a notification in the "non prompting" mode, so the user knows
// that his screenshot has been created. Also handles cleanup after download.
browser.downloads.onChanged.addListener(async (delta) => {
if (delta.id in DOWNLOAD_CACHE) { // Was the download triggered by us?
if (delta.state && delta.state.current === "complete") { // Is it done?
const options = DOWNLOAD_CACHE[delta.id];
// When saving without prompting, then trigger notification
const prefs = await Storage.get();
if (!options.saveAs && prefs.savenotification)
browser.notifications.create("info-notification", {
"type": "basic",
"title": browser.i18n.getMessage("extensionName"),
"message": browser.i18n.getMessage("info_screenshot_saved") + "\n" + options.filename
});
// Free memory used for our "blob URL"
URL.revokeObjectURL(options.url);
// Remove data for this download from our cache
delete DOWNLOAD_CACHE[delta.id];
}
}
});
// Gets the default file name, used for saving the screenshot
function GetDefaultFileName(aDefaultFileName, tab, aFilenameFormat) {
//prioritize formatted variant
const formatted = SanitizeFileName(ApplyFilenameFormat(aFilenameFormat, tab));
if (formatted)
return formatted;
// If possible, base the file name on document title
if (tab.title) {
const title = SanitizeFileName(tab.title);
if (title)
return title;
}
// Otherwise try to use the actual HTML filename
const url = new URL(tab.url)
const path = url.pathname;
if (path) {
const filename = SanitizeFileName(path.substring(path.lastIndexOf('/')+1));
if (filename)
return filename;
}
// Finally use the provided default name
return aDefaultFileName;
}
// Replaces format character sequences with the actual values
function ApplyFilenameFormat(aFormat, tab) {
const now = new Date();
aFormat = aFormat.replace(/%Y/,now.getFullYear());
aFormat = aFormat.replace(/%y/,now.getFullYear().toString().substring(2));
aFormat = aFormat.replace(/%m/,(now.getMonth()+1).toString().padStart(2, '0'));
aFormat = aFormat.replace(/%d/,now.getDate().toString().padStart(2, '0'));
aFormat = aFormat.replace(/%H/,now.getHours().toString().padStart(2, '0'));
aFormat = aFormat.replace(/%M/,now.getMinutes().toString().padStart(2, '0'));
aFormat = aFormat.replace(/%S/,now.getSeconds().toString().padStart(2, '0'));
aFormat = aFormat.replace(/%t/,tab.title || "");
aFormat = aFormat.replace(/%u/,tab.url.replace(/:/g, ".").replace(/[\/\?]/g, "-"));
aFormat = aFormat.replace(/%h/,(new URL(tab.url)).hostname);
return aFormat;
}
// "Sanitizes" given string to be used as file name.
function SanitizeFileName(aFileName) {
// http://www.mtu.edu/umc/services/digital/writing/characters-avoid/
aFileName = aFileName.replace(/[<\{]+/g, "(");
aFileName = aFileName.replace(/[>\}]+/g, ")");
aFileName = aFileName.replace(/[#$%!&*\'?\"\/:\\@|]/g, "");
// Remove leading spaces, "." and "-"
aFileName = aFileName.replace(/^[\s-.]+/, "");
// Remove trailing spaces and "."
aFileName = aFileName.replace(/[\s.]+$/, "");
// Replace all groups of spaces with just one space character
aFileName = aFileName.replace(/\s+/g, " ");
return aFileName;
}
// Migrates old "only one possible" preferences to new "multi select" model
async function MigrateSettings() {
const prefs = await Storage.get();
const newprefs = {};
if ("region" in prefs) {
if (prefs.region == "manual")
newprefs.regions = ["full", "viewport", "selection"];
else
newprefs.regions = [prefs.region];
await Storage.remove("region");
}
if ("format" in prefs) {
if (prefs.format == "manual")
newprefs.formats = ["png", "jpg", "copy"];
else
newprefs.formats = [prefs.format];
await Storage.remove("format");
}
await Storage.set(newprefs);
}
async function Startup() {
await MigrateSettings();
// Android: Change defaults to reflect supported options on Android
if ((await browser.runtime.getPlatformInfo()).os === "android") {
const prefs = await Storage.get();
// Only save method "open" supported (downloads API behaves ugly)
prefs["savemethod"] = "open";
// "browser.contextMenus" is undefined on Android
prefs["show_contextmenu"] = false;
// No image copy to clipboard supported
const copyindex = prefs["formats"].indexOf("copy");
if (copyindex !== -1)
prefs["formats"].splice(copyindex, 1);
await Storage.set(prefs);
}
await UpdateUI();
}
// Register event listeners
if (browser.contextMenus !== undefined) // If not on Android
browser.contextMenus.onClicked.addListener(ContextMenuClicked);
browser.browserAction.onClicked.addListener(ToolbarButtonClicked);
if (browser.commands !== undefined) // If not on Android
browser.commands.onCommand.addListener(CommandPressed);
Startup();
IconUpdater.Init("icons/savescreenshot.svg");