Переглянути джерело

implemented "pending saves" view

Former-commit-id: cd04962aeaec038ce857600df9a5d5aacfeb6ab1
Gildas 6 роки тому
батько
коміт
46ca461618

+ 36 - 0
_locales/de/messages.json

@@ -11,6 +11,10 @@
 		"message": "Annotieren und Speichern der Webseite...",
 		"description": "Menu entry: 'Annotate and save the page...'"
 	},
+	"menuViewPendingSaves": {
+		"message": "Anzeigen der aktuellen Speichern...",
+		"description": "Menu entry: 'View pending saves...'"
+	},
 	"menuSelectProfile": {
 		"message": "Wählen Sie das Standardprofil aus",
 		"description": "Menu entry: 'Select the default profile'"
@@ -530,5 +534,37 @@
 	"editorSavePage": {
 		"message": "Speichern der Webseite",
 		"description": "Title of the button 'Save the page' in the editor"
+	},
+	"pendingsTitle": {
+		"message": "Pending saves",
+		"description": "Title of the pending save page 'Pending saves' in the editor"
+	},
+	"pendingsCancelAllButton": {
+		"message": "Cancel all",
+		"description": "Pending saves button 'Cancel all'"
+	},
+	"pendingsURLTitle": {
+		"message": "URL",
+		"description": "Title of the first column in the table of the pending saves 'URL'"
+	},
+	"pendingsStatusTitle": {
+		"message": "status",
+		"description": "Title of the second column in the table of the pending saves 'status'"
+	},
+	"pendingsPendingStatus": {
+		"message": "pending",
+		"description": "Value of 'status' for pending saves"
+	},
+	"pendingsProcessingStatus": {
+		"message": "processing",
+		"description": "Value of 'status' for current saves"
+	},
+	"pendingsCancellingStatus": {
+		"message": "cancelling",
+		"description": "Value of 'status' for cancelled saves"
+	},
+	"pendingsNoPendings": {
+		"message": "No pending saves",
+		"description": "Label displayed when they are no pending saves"
 	}
 }

+ 36 - 0
_locales/en/messages.json

@@ -11,6 +11,10 @@
 		"message": "Annotate and save the page...",
 		"description": "Menu entry: 'Annotate and save the page...'"
 	},
+	"menuViewPendingSaves": {
+		"message": "View pending saves...",
+		"description": "Menu entry: 'View pending saves...'"
+	},
 	"menuSelectProfile": {
 		"message": "Select the default profile",
 		"description": "Menu entry: 'Select the default profile'"
@@ -530,5 +534,37 @@
 	"editorSavePage": {
 		"message": "Save the page",
 		"description": "Title of the button 'Save the page' in the editor"
+	},
+	"pendingsTitle": {
+		"message": "Pending saves",
+		"description": "Title of the pending save page 'Pending saves' in the editor"
+	},
+	"pendingsCancelAllButton": {
+		"message": "Cancel all",
+		"description": "Pending saves button 'Cancel all'"
+	},
+	"pendingsURLTitle": {
+		"message": "URL",
+		"description": "Title of the first column in the table of the pending saves 'URL'"
+	},
+	"pendingsStatusTitle": {
+		"message": "status",
+		"description": "Title of the second column in the table of the pending saves 'status'"
+	},
+	"pendingsPendingStatus": {
+		"message": "pending",
+		"description": "Value of 'status' for pending saves"
+	},
+	"pendingsProcessingStatus": {
+		"message": "processing",
+		"description": "Value of 'status' for current saves"
+	},
+	"pendingsCancellingStatus": {
+		"message": "cancelling",
+		"description": "Value of 'status' for cancelled saves"
+	},
+	"pendingsNoPendings": {
+		"message": "No pending saves",
+		"description": "Label displayed when they are no pending saves"
 	}
 }

+ 36 - 0
_locales/es/messages.json

@@ -11,6 +11,10 @@
 		"message": "Anotar y guardar la página",
 		"description": "Menu entry: 'Annotate and save the page...'"
 	},
+	"menuViewPendingSaves": {
+		"message": "View pending saves...",
+		"description": "Menu entry: 'View pending saves...'"
+	},
 	"menuSelectProfile": {
 		"message": "Seleccionar el perfil predeterminado",
 		"description": "Menu entry: 'Select the default profile'"
@@ -530,5 +534,37 @@
 	"editorSavePage": {
 		"message": "Save the page",
 		"description": "Title of the button 'Save the page' in the editor"
+	},
+	"pendingsTitle": {
+		"message": "Pending saves",
+		"description": "Title of the pending save page 'Pending saves' in the editor"
+	},
+	"pendingsCancelAllButton": {
+		"message": "Cancel all",
+		"description": "Pending saves button 'Cancel all'"
+	},
+	"pendingsURLTitle": {
+		"message": "URL",
+		"description": "Title of the first column in the table of the pending saves 'URL'"
+	},
+	"pendingsStatusTitle": {
+		"message": "status",
+		"description": "Title of the second column in the table of the pending saves 'status'"
+	},
+	"pendingsPendingStatus": {
+		"message": "pending",
+		"description": "Value of 'status' for pending saves"
+	},
+	"pendingsProcessingStatus": {
+		"message": "processing",
+		"description": "Value of 'status' for current saves"
+	},
+	"pendingsCancellingStatus": {
+		"message": "cancelling",
+		"description": "Value of 'status' for cancelled saves"
+	},
+	"pendingsNoPendings": {
+		"message": "No pending saves",
+		"description": "Label displayed when they are no pending saves"
 	}
 }

+ 36 - 0
_locales/fr/messages.json

@@ -11,6 +11,10 @@
 		"message": "Annoter et sauver la page...",
 		"description": "Menu entry: 'Annotate and save the page...'"
 	},
+	"menuViewPendingSaves": {
+		"message": "Afficher les sauvegardes en cours...",
+		"description": "Menu entry: 'View pending saves...'"
+	},
 	"menuSelectProfile": {
 		"message": "Sélectionner le profil par défaut",
 		"description": "Menu entry: 'Select the default profile'"
@@ -530,5 +534,37 @@
 	"editorSavePage": {
 		"message": "Sauver la page",
 		"description": "Title of the button 'Save the page' in the editor"
+	},
+	"pendingsTitle": {
+		"message": "Sauvegardes en cours",
+		"description": "Title of the pending save page 'Pending saves' in the editor"
+	},
+	"pendingsCancelAllButton": {
+		"message": "Annuler tout",
+		"description": "Pending saves button 'Cancel all'"
+	},
+	"pendingsURLTitle": {
+		"message": "URL",
+		"description": "Title of the first column in the table of the pending saves 'URL'"
+	},
+	"pendingsStatusTitle": {
+		"message": "statut",
+		"description": "Title of the second column in the table of the pending saves 'status'"
+	},
+	"pendingsPendingStatus": {
+		"message": "en attente",
+		"description": "Value of 'status' for pending saves"
+	},
+	"pendingsProcessingStatus": {
+		"message": "en cours",
+		"description": "Value of 'status' for current saves"
+	},
+	"pendingsCancellingStatus": {
+		"message": "annulation",
+		"description": "Value of 'status' for cancelled saves"
+	},
+	"pendingsNoPendings": {
+		"message": "Aucune sauvegarde en cours",
+		"description": "Label displayed when they are no pending saves"
 	}
 }

+ 36 - 0
_locales/ja/messages.json

@@ -11,6 +11,10 @@
 		"message": "ページに注釈を付けて保存する...",
 		"description": "Menu entry: 'Annotate and save the page...'"
 	},
+	"menuViewPendingSaves": {
+		"message": "View pending saves...",
+		"description": "Menu entry: 'View pending saves...'"
+	},
 	"menuSelectProfile": {
 		"message": "規定(default)のプロファイルを選択",
 		"description": "Menu entry: 'Select the default profile'"
@@ -530,5 +534,37 @@
 	"editorSavePage": {
 		"message": "Save the page",
 		"description": "Title of the button 'Save the page' in the editor"
+	},
+	"pendingsTitle": {
+		"message": "Pending saves",
+		"description": "Title of the pending save page 'Pending saves' in the editor"
+	},
+	"pendingsCancelAllButton": {
+		"message": "Cancel all",
+		"description": "Pending saves button 'Cancel all'"
+	},
+	"pendingsURLTitle": {
+		"message": "URL",
+		"description": "Title of the first column in the table of the pending saves 'URL'"
+	},
+	"pendingsStatusTitle": {
+		"message": "status",
+		"description": "Title of the second column in the table of the pending saves 'status'"
+	},
+	"pendingsPendingStatus": {
+		"message": "pending",
+		"description": "Value of 'status' for pending saves"
+	},
+	"pendingsProcessingStatus": {
+		"message": "processing",
+		"description": "Value of 'status' for current saves"
+	},
+	"pendingsCancellingStatus": {
+		"message": "cancelling",
+		"description": "Value of 'status' for cancelled saves"
+	},
+	"pendingsNoPendings": {
+		"message": "No pending saves",
+		"description": "Label displayed when they are no pending saves"
 	}
 }

+ 36 - 0
_locales/pl/messages.json

@@ -11,6 +11,10 @@
 		"message": "Adnotuj i zapisz stronę...",
 		"description": "Menu entry: 'Annotate and save the page...'"
 	},
+	"menuViewPendingSaves": {
+		"message": "View pending saves...",
+		"description": "Menu entry: 'View pending saves...'"
+	},
 	"menuSelectProfile": {
 		"message": "Wybierz profil domyślny",
 		"description": "Menu entry: 'Select the default profile'"
@@ -530,5 +534,37 @@
 	"editorSavePage": {
 		"message": "Zapisz stronę",
 		"description": "Title of the button 'Save the page' in the editor"
+	},
+	"pendingsTitle": {
+		"message": "Pending saves",
+		"description": "Title of the pending save page 'Pending saves' in the editor"
+	},
+	"pendingsCancelAllButton": {
+		"message": "Cancel all",
+		"description": "Pending saves button 'Cancel all'"
+	},
+	"pendingsURLTitle": {
+		"message": "URL",
+		"description": "Title of the first column in the table of the pending saves 'URL'"
+	},
+	"pendingsStatusTitle": {
+		"message": "status",
+		"description": "Title of the second column in the table of the pending saves 'status'"
+	},
+	"pendingsPendingStatus": {
+		"message": "pending",
+		"description": "Value of 'status' for pending saves"
+	},
+	"pendingsProcessingStatus": {
+		"message": "processing",
+		"description": "Value of 'status' for current saves"
+	},
+	"pendingsCancellingStatus": {
+		"message": "cancelling",
+		"description": "Value of 'status' for cancelled saves"
+	},
+	"pendingsNoPendings": {
+		"message": "No pending saves",
+		"description": "Label displayed when they are no pending saves"
 	}
 }

+ 36 - 0
_locales/ru/messages.json

@@ -11,6 +11,10 @@
 		"message": "Аннотировать и сохранить страницу...",
 		"description": "Menu entry: 'Annotate and save the page...'"
 	},
+	"menuViewPendingSaves": {
+		"message": "View pending saves...",
+		"description": "Menu entry: 'View pending saves...'"
+	},
 	"menuSelectProfile": {
 		"message": "Выберите профиль по умолчанию",
 		"description": "Menu entry: 'Select the default profile'"
@@ -530,5 +534,37 @@
 	"editorSavePage": {
 		"message": "Save the page",
 		"description": "Title of the button 'Save the page' in the editor"
+	},
+	"pendingsTitle": {
+		"message": "Pending saves",
+		"description": "Title of the pending save page 'Pending saves' in the editor"
+	},
+	"pendingsCancelAllButton": {
+		"message": "Cancel all",
+		"description": "Pending saves button 'Cancel all'"
+	},
+	"pendingsURLTitle": {
+		"message": "URL",
+		"description": "Title of the first column in the table of the pending saves 'URL'"
+	},
+	"pendingsStatusTitle": {
+		"message": "status",
+		"description": "Title of the second column in the table of the pending saves 'status'"
+	},
+	"pendingsPendingStatus": {
+		"message": "pending",
+		"description": "Value of 'status' for pending saves"
+	},
+	"pendingsProcessingStatus": {
+		"message": "processing",
+		"description": "Value of 'status' for current saves"
+	},
+	"pendingsCancellingStatus": {
+		"message": "cancelling",
+		"description": "Value of 'status' for cancelled saves"
+	},
+	"pendingsNoPendings": {
+		"message": "No pending saves",
+		"description": "Label displayed when they are no pending saves"
 	}
 }

+ 37 - 1
_locales/uk/messages.json

@@ -11,6 +11,10 @@
 		"message": "Анотувати і зберегти сторінку...",
 		"description": "Menu entry: 'Annotate and save the page...'"
 	},
+	"menuViewPendingSaves": {
+		"message": "View pending saves...",
+		"description": "Menu entry: 'View pending saves...'"
+	},
 	"menuSelectProfile": {
 		"message": "Виберіть профіль за замовчуванням ",
 		"description": "Menu entry: 'Select the default profile'"
@@ -530,5 +534,37 @@
 	"editorSavePage": {
 		"message": "Зберегти сторінку",
 		"description": "Title of the button 'Save the page' in the editor"
+	},
+	"pendingsTitle": {
+		"message": "Pending saves",
+		"description": "Title of the pending save page 'Pending saves' in the editor"
+	},
+	"pendingsCancelAllButton": {
+		"message": "Cancel all",
+		"description": "Pending saves button 'Cancel all'"
+	},
+	"pendingsURLTitle": {
+		"message": "URL",
+		"description": "Title of the first column in the table of the pending saves 'URL'"
+	},
+	"pendingsStatusTitle": {
+		"message": "status",
+		"description": "Title of the second column in the table of the pending saves 'status'"
+	},
+	"pendingsPendingStatus": {
+		"message": "pending",
+		"description": "Value of 'status' for pending saves"
+	},
+	"pendingsProcessingStatus": {
+		"message": "processing",
+		"description": "Value of 'status' for current saves"
+	},
+	"pendingsCancellingStatus": {
+		"message": "cancelling",
+		"description": "Value of 'status' for cancelled saves"
+	},
+	"pendingsNoPendings": {
+		"message": "No pending saves",
+		"description": "Label displayed when they are no pending saves"
 	}
-}
+}

+ 36 - 0
_locales/zh_CN/messages.json

@@ -11,6 +11,10 @@
 		"message": "评论并保存页面...",
 		"description": "Menu entry: 'Annotate and save the page...'"
 	},
+	"menuViewPendingSaves": {
+		"message": "View pending saves...",
+		"description": "Menu entry: 'View pending saves...'"
+	},
 	"menuSelectProfile": {
 		"message": "选择默认配置文件",
 		"description": "Menu entry: 'Select the default profile'"
@@ -530,5 +534,37 @@
 	"editorSavePage": {
 		"message": "Save the page",
 		"description": "Title of the button 'Save the page' in the editor"
+	},
+	"pendingsTitle": {
+		"message": "Pending saves",
+		"description": "Title of the pending save page 'Pending saves' in the editor"
+	},
+	"pendingsCancelAllButton": {
+		"message": "Cancel all",
+		"description": "Pending saves button 'Cancel all'"
+	},
+	"pendingsURLTitle": {
+		"message": "URL",
+		"description": "Title of the first column in the table of the pending saves 'URL'"
+	},
+	"pendingsStatusTitle": {
+		"message": "status",
+		"description": "Title of the second column in the table of the pending saves 'status'"
+	},
+	"pendingsPendingStatus": {
+		"message": "pending",
+		"description": "Value of 'status' for pending saves"
+	},
+	"pendingsProcessingStatus": {
+		"message": "processing",
+		"description": "Value of 'status' for current saves"
+	},
+	"pendingsCancellingStatus": {
+		"message": "cancelling",
+		"description": "Value of 'status' for cancelled saves"
+	},
+	"pendingsNoPendings": {
+		"message": "No pending saves",
+		"description": "Label displayed when they are no pending saves"
 	}
 }

+ 36 - 0
_locales/zh_TW/messages.json

@@ -11,6 +11,10 @@
 		"message": "評論並保存頁面...",
 		"description": "Menu entry: 'Annotate and save the page...'"
 	},
+	"menuViewPendingSaves": {
+		"message": "View pending saves...",
+		"description": "Menu entry: 'View pending saves...'"
+	},
 	"menuSelectProfile": {
 		"message": "選擇默認配置文件",
 		"description": "Menu entry: 'Select the default profile'"
@@ -530,5 +534,37 @@
 	"editorSavePage": {
 		"message": "Save the page",
 		"description": "Title of the button 'Save the page' in the editor"
+	},
+	"pendingsTitle": {
+		"message": "Pending saves",
+		"description": "Title of the pending save page 'Pending saves' in the editor"
+	},
+	"pendingsCancelAllButton": {
+		"message": "Cancel all",
+		"description": "Pending saves button 'Cancel all'"
+	},
+	"pendingsURLTitle": {
+		"message": "URL",
+		"description": "Title of the first column in the table of the pending saves 'URL'"
+	},
+	"pendingsStatusTitle": {
+		"message": "status",
+		"description": "Title of the second column in the table of the pending saves 'status'"
+	},
+	"pendingsPendingStatus": {
+		"message": "pending",
+		"description": "Value of 'status' for pending saves"
+	},
+	"pendingsProcessingStatus": {
+		"message": "processing",
+		"description": "Value of 'status' for current saves"
+	},
+	"pendingsCancellingStatus": {
+		"message": "cancelling",
+		"description": "Value of 'status' for cancelled saves"
+	},
+	"pendingsNoPendings": {
+		"message": "No pending saves",
+		"description": "Label displayed when they are no pending saves"
 	}
 }

+ 25 - 15
extension/core/bg/business.js

@@ -50,7 +50,8 @@ singlefile.extension.core.bg.business = (() => {
 		isSavingTab: tab => currentSaves.has(tab.id),
 		saveTabs,
 		saveLink,
-		cancelTab
+		cancelTab,
+		getInfo: () => ({ pending: Array.from(pendingSaves), processing: Array.from(currentSaves) })
 	};
 
 	async function saveTabs(tabs, options = {}) {
@@ -67,7 +68,7 @@ singlefile.extension.core.bg.business = (() => {
 				if (options.autoSave) {
 					const tabOptions = await config.getOptions(tab.url, true);
 					if (autosave.isEnabled(tab)) {
-						await requestSaveTab(tabId, "content.autosave", tabOptions);
+						await requestSaveTab(tab, "content.autosave", tabOptions);
 					}
 				} else {
 					ui.onStart(tabId, INJECT_SCRIPTS_STEP);
@@ -78,7 +79,7 @@ singlefile.extension.core.bg.business = (() => {
 					let promiseSaveTab;
 					if (scriptsInjected) {
 						ui.onStart(tabId, EXECUTE_SCRIPTS_STEP);
-						promiseSaveTab = requestSaveTab(tabId, "content.save", tabOptions);
+						promiseSaveTab = requestSaveTab(tab, "content.save", tabOptions);
 					} else {
 						ui.onForbiddenDomain(tab);
 						promiseSaveTab = Promise.resolve();
@@ -102,39 +103,48 @@ singlefile.extension.core.bg.business = (() => {
 		await saveTabs([tab], options);
 	}
 
-	async function cancelTab(tab) {
+	async function cancelTab(tabId) {
 		try {
-			singlefile.extension.core.bg.tabs.sendMessage(tab.id, { method: "content.cancelSave" });
+			if (currentSaves.has(tabId)) {
+				const data = currentSaves.get(tabId);
+				data.cancelled = true;
+				singlefile.extension.core.bg.tabs.sendMessage(tabId, { method: "content.cancelSave" });
+			}
+			if (pendingSaves.has(tabId)) {
+				const data = pendingSaves.get(tabId);
+				pendingSaves.delete(tabId);
+				singlefile.extension.ui.bg.main.onCancelled(data.tab);
+			}
 		} catch (error) {
-			// ignored;
+			// ignored
 		}
 	}
 
-	function requestSaveTab(tabId, method, options) {
-		return new Promise((resolve, reject) => requestSaveTab(tabId, method, options, resolve, reject));
+	function requestSaveTab(tab, method, options) {
+		return new Promise((resolve, reject) => requestSaveTab(tab, method, options, resolve, reject));
 
-		async function requestSaveTab(tabId, method, options, resolve, reject) {
+		async function requestSaveTab(tab, method, options, resolve, reject) {
 			if (currentSaves.size < maxParallelWorkers) {
-				currentSaves.set(tabId, { options, resolve, reject });
+				currentSaves.set(tab.id, { tab, options, resolve, reject });
 				try {
-					await singlefile.extension.core.bg.tabs.sendMessage(tabId, { method, options });
+					await singlefile.extension.core.bg.tabs.sendMessage(tab.id, { method, options });
 					resolve();
 				} catch (error) {
 					reject(error);
 				} finally {
-					currentSaves.delete(tabId);
+					currentSaves.delete(tab.id);
 					next();
 				}
 			} else {
-				pendingSaves.set(tabId, { options, resolve, reject });
+				pendingSaves.set(tab.id, { tab, options, resolve, reject });
 			}
 		}
 
 		function next() {
 			if (pendingSaves.size) {
-				const [tabId, { resolve, reject, options }] = Array.from(pendingSaves)[0];
+				const [tabId, { tab, resolve, reject, options }] = Array.from(pendingSaves)[0];
 				pendingSaves.delete(tabId);
-				requestSaveTab(tabId, method, options, resolve, reject);
+				requestSaveTab(tab, method, options, resolve, reject);
 			}
 		}
 	}

+ 7 - 0
extension/core/bg/downloads.js

@@ -65,6 +65,13 @@ singlefile.extension.core.bg.downloads = (() => {
 			}
 			return {};
 		}
+		if (message.method.endsWith(".getInfo")) {
+			return singlefile.extension.core.bg.business.getInfo();
+		}
+		if (message.method.endsWith(".cancel")) {
+			await singlefile.extension.core.bg.business.cancelTab(message.tabId);
+			return {};
+		}
 	}
 
 	async function downloadTabPage(message, tab) {

+ 3 - 0
extension/core/bg/tabs.js

@@ -126,6 +126,9 @@ singlefile.extension.core.bg.tabs = (() => {
 		if (message.method.endsWith(".getOptions")) {
 			return singlefile.extension.core.bg.config.getOptions(message.url);
 		}
+		if (message.method.endsWith(".activate")) {
+			await browser.tabs.update(message.tabId, { active: true });
+		}
 	}
 
 	function onTabCreated(tab) {

+ 9 - 0
extension/lib/single-file/browser-polyfill/chrome-browser-polyfill.js

@@ -385,6 +385,15 @@
 							resolve();
 						}
 					});
+				}),
+				update: (tabId, updateProperties) => new Promise((resolve, reject) => {
+					nativeAPI.tabs.update(tabId, updateProperties, tab => {
+						if (nativeAPI.runtime.lastError) {
+							reject(nativeAPI.runtime.lastError);
+						} else {
+							resolve(tab);
+						}
+					});
 				})
 			},
 			devtools: nativeAPI.devtools && {

+ 1 - 0
extension/ui/bg/ui-button.js

@@ -140,6 +140,7 @@ singlefile.extension.ui.bg.button = (() => {
 		onError,
 		onEdit,
 		onEnd,
+		onCancelled,
 		refreshTab
 	};
 

+ 3 - 0
extension/ui/bg/ui-main.js

@@ -51,6 +51,9 @@ singlefile.extension.ui.bg.main = (() => {
 		onEnd(tabId, autoSave) {
 			singlefile.extension.ui.bg.button.onEnd(tabId, autoSave);
 		},
+		onCancelled(tabId) {
+			singlefile.extension.ui.bg.button.onCancelled(tabId);
+		},
 		onTabCreated(tab) {
 			singlefile.extension.ui.bg.menus.onTabCreated(tab);
 		},

+ 10 - 0
extension/ui/bg/ui-menus.js

@@ -29,6 +29,7 @@ singlefile.extension.ui.bg.menus = (() => {
 	const BROWSER_MENUS_API_SUPPORTED = menus && menus.onClicked && menus.create && menus.update && menus.removeAll;
 	const MENU_ID_SAVE_PAGE = "save-page";
 	const MENU_ID_EDIT_AND_SAVE_PAGE = "edit-and-save-page";
+	const MENU_ID_VIEW_PENDINGS = "view-pendings";
 	const MENU_ID_SELECT_PROFILE = "select-profile";
 	const MENU_ID_SELECT_PROFILE_PREFIX = "select-profile-";
 	const MENU_ID_ASSOCIATE_WITH_PROFILE = "associate-with-profile";
@@ -51,6 +52,7 @@ singlefile.extension.ui.bg.menus = (() => {
 	const MENU_UPDATE_RULE_MESSAGE = browser.i18n.getMessage("menuUpdateRule");
 	const MENU_SAVE_PAGE_MESSAGE = browser.i18n.getMessage("menuSavePage");
 	const MENU_EDIT_AND_SAVE_PAGE_MESSAGE = browser.i18n.getMessage("menuEditAndSavePage");
+	const MENU_VIEW_PENDINGS_MESSAGE = browser.i18n.getMessage("menuViewPendingSaves");
 	const MENU_SAVE_SELECTION_MESSAGE = browser.i18n.getMessage("menuSaveSelection");
 	const MENU_SAVE_FRAME_MESSAGE = browser.i18n.getMessage("menuSaveFrame");
 	const MENU_SAVE_TABS_MESSAGE = browser.i18n.getMessage("menuSaveTabs");
@@ -123,6 +125,11 @@ singlefile.extension.ui.bg.menus = (() => {
 					title: MENU_EDIT_AND_SAVE_PAGE_MESSAGE
 				});
 			}
+			menus.create({
+				id: MENU_ID_VIEW_PENDINGS,
+				contexts: defaultContexts,
+				title: MENU_VIEW_PENDINGS_MESSAGE
+			});
 			if (options.contextMenuEnabled) {
 				menus.create({
 					id: "separator-1",
@@ -342,6 +349,9 @@ singlefile.extension.ui.bg.menus = (() => {
 						business.saveTabs([tab], { openEditor: true });
 					}
 				}
+				if (event.menuItemId == MENU_ID_VIEW_PENDINGS) {
+					await tabs.create({ active: true, url: "/extension/ui/pages/pendings.html" });
+				}
 				if (event.menuItemId == MENU_ID_SAVE_SELECTED) {
 					business.saveTabs([tab], { selected: true });
 				}

+ 118 - 0
extension/ui/bg/ui-pendings.js

@@ -0,0 +1,118 @@
+/*
+ * Copyright 2010-2019 Gildas Lormeau
+ * contact : gildas.lormeau <at> gmail.com
+ * 
+ * This file is part of SingleFile.
+ *
+ *   The code in this file is free software: you can redistribute it and/or 
+ *   modify it under the terms of the GNU Affero General Public License 
+ *   (GNU AGPL) as published by the Free Software Foundation, either version 3
+ *   of the License, or (at your option) any later version.
+ * 
+ *   The code in this file 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 Affero 
+ *   General Public License for more details.
+ *
+ *   As additional permission under GNU AGPL version 3 section 7, you may 
+ *   distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 
+ *   AGPL normally required by section 4, provided you include this license 
+ *   notice and a URL through which recipients can access the Corresponding 
+ *   Source.
+ */
+
+/* global browser, document, setInterval */
+
+(async () => {
+
+	const URLLabel = document.getElementById("URLLabel");
+	const statusLabel = document.getElementById("statusLabel");
+	const resultsTable = document.getElementById("resultsTable");
+	const cancelAllButton = document.getElementById("cancelAllButton");
+	document.title = browser.i18n.getMessage("pendingsTitle");
+	cancelAllButton.textContent = browser.i18n.getMessage("pendingsCancelAllButton");
+	URLLabel.textContent = browser.i18n.getMessage("pendingsURLTitle");
+	statusLabel.textContent = browser.i18n.getMessage("pendingsStatusTitle");
+	const statusText = {
+		pending: browser.i18n.getMessage("pendingsPendingStatus"),
+		processing: browser.i18n.getMessage("pendingsProcessingStatus"),
+		cancelling: browser.i18n.getMessage("pendingsCancellingStatus")
+	};
+	const noPendingsText = browser.i18n.getMessage("pendingsNoPendings");
+	cancelAllButton.onclick = async () => {
+		const results = await browser.runtime.sendMessage({ method: "downloads.getInfo" });
+		await Promise.all(results.pending.concat(results.processing).map(([tabId]) => browser.runtime.sendMessage({ method: "downloads.cancel", tabId })));
+		await refresh();
+	};
+	let previousState;
+	setInterval(refresh, 1000);
+	await refresh();
+
+	function resetTable() {
+		resultsTable.innerHTML = "";
+	}
+
+	function updateTable(results, type) {
+		const data = results[type];
+		if (data.length) {
+			data.forEach(([tabId, saveInfo]) => {
+				const row = document.createElement("div");
+				const cellURL = document.createElement("span");
+				const cellStatus = document.createElement("span");
+				const cellCancel = document.createElement("span");
+				const buttonCancel = document.createElement("button");
+				row.dataset.tabId = tabId;
+				row.className = "result-row result-type-" + type;
+				cellURL.textContent = saveInfo.tab.url;
+				cellURL.className = "result-url";
+				cellURL.onclick = () => selectTab(type, tabId);
+				if (saveInfo.cancelled) {
+					cellStatus.textContent = statusText.cancelling;
+				} else {
+					cellStatus.textContent = statusText[type];
+					buttonCancel.textContent = "×";
+					buttonCancel.onclick = () => cancel(type, tabId);
+					cellCancel.appendChild(buttonCancel);
+				}
+				cellStatus.className = "result-status";
+				cellCancel.className = "result-cancel";
+				row.appendChild(cellURL);
+				row.appendChild(cellStatus);
+				row.appendChild(cellCancel);
+				resultsTable.appendChild(row);
+			});
+		}
+	}
+
+	async function cancel(type, tabId) {
+		await browser.runtime.sendMessage({ method: "downloads.cancel", tabId });
+		await refresh();
+	}
+
+	async function selectTab(type, tabId) {
+		await browser.runtime.sendMessage({ method: "tabs.activate", tabId });
+		await refresh();
+	}
+
+	async function refresh() {
+		const results = await browser.runtime.sendMessage({ method: "downloads.getInfo" });
+		const currentState = JSON.stringify(results);
+		if (previousState != currentState) {
+			previousState = currentState;
+			resetTable();
+			updateTable(results, "processing");
+			updateTable(results, "pending");
+			if (!results.pending.length && !results.processing.length) {
+				const row = document.createElement("div");
+				row.className = "result-row";
+				const cell = document.createElement("span");
+				cell.colSpan = 3;
+				cell.className = "no-result";
+				cell.textContent = noPendingsText;
+				row.appendChild(cell);
+				resultsTable.appendChild(row);
+			}
+		}
+	}
+
+})();

+ 132 - 0
extension/ui/pages/pendings.css

@@ -0,0 +1,132 @@
+html {
+    background-color: #f0f0f0;
+}
+
+main {
+    background-color: #fff;
+    border: solid 1px rgb(191, 191, 191);
+}
+
+main,
+header {
+    font-size: 12px;
+    font-family: sans-serif;
+    margin: 0;
+    margin-left: auto;
+    margin-right: auto;
+    max-width: 1024px;
+}
+
+header {
+    display: flex;
+    flex-direction: column;
+}
+
+button {
+    background-color: #fbfbfb;
+    border-color: rgb(191, 191, 191);
+    border-style: solid;
+    border-radius: 2px;
+    border-width: 1px;
+    color: black;
+}
+
+button:not([disabled]):hover {
+    background-color: #ededed;
+}
+
+header button {
+    margin-bottom: 10px;
+    align-self: flex-end;
+    padding: 5px;
+    padding-left: 10px;
+    padding-right: 10px;
+}
+
+.result-row {
+    display: flex;
+    flex-direction: row;
+    padding-top: 5px;
+    padding-bottom: 5px;
+    min-height: 40px;
+}
+
+.result-row:not(:first-child) {
+    border-top: #bfbfbf 1px dashed;
+}
+
+.result-head {
+    background-color: #ececec;
+}
+
+.result-row span {
+    padding: 10px;
+    align-self: center;
+}
+
+.result-url {
+    flex: 1;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    display: inline-block;
+    white-space: nowrap;
+}
+
+.result-row:not(:first-child) .result-url {
+    cursor: pointer;
+}
+
+.result-status {
+    min-width: 120px;
+}
+
+.result-cancel {
+    text-align: right;
+    width: 19px;
+}
+
+.result-cancel button {
+    background-color: #fbfbfb;
+}
+
+.no-result {
+    color: #888;
+}
+
+@media (prefers-color-scheme: dark) {
+    html {
+        background-color: #373737;
+        color: #fdfdfd;
+    }
+
+    main {
+        border-color: rgb(81, 81, 81);
+    }
+
+    main,
+    button {
+        background-color: #202023;
+        color: #fdfdfd;
+    }
+
+    .result-head {
+        background-color: #191919;
+        color: white;
+    }
+
+    .result-row {
+        color: #fdfdfd;
+    }
+
+    .no-result {
+        color: #888;
+    }
+
+    .result-cancel button {
+        color: #fdfdfd;
+    }
+
+    button:not([disabled]):hover {
+        color: #2A2A2E;
+    }
+}

+ 29 - 0
extension/ui/pages/pendings.html

@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+	<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+	<title>&nbsp;</title>
+	<link rel="stylesheet" href="pendings.css">
+	<meta name="viewport" content="width=device-width,initial-scale=1">
+</head>
+
+<body>
+	<header>
+		<button id="cancelAllButton"></button>
+	</header>
+	<main>
+		<div class="result-row result-head">
+			<span class="result-url" id="URLLabel"></span>
+			<span class="result-status" id="statusLabel"></span>
+			<span class="result-cancel">&nbsp;</span>
+		</div>
+		<div id="resultsTable">
+		</div>
+	</main>
+	<script type="text/javascript"
+		src="/extension/lib/single-file/browser-polyfill/chrome-browser-polyfill.js"></script>
+	<script type="text/javascript" src="../bg/ui-pendings.js"></script>
+</body>
+
+</html>