Browse Source

implemented "save selected links" and "Add URLs" (fix #333)

Gildas 6 years ago
parent
commit
44d3796f55

+ 20 - 0
_locales/de/messages.json

@@ -7,6 +7,10 @@
 		"message": "Speichern der Webseite mit SingleFile",
 		"description": "Menu entry: 'Save page with SingleFile'"
 	},
+	"menuSaveSelectedLinks": {
+		"message": "Speichern ausgewählter Links",
+		"description": "Menu entry: 'Save selected links'"
+	},
 	"menuEditAndSavePage": {
 		"message": "Annotieren und Speichern der Webseite...",
 		"description": "Menu entry: 'Annotate and save the page...'"
@@ -590,5 +594,21 @@
 	"pendingsNoPendings": {
 		"message": "No pending saves",
 		"description": "Label displayed when they are no pending saves"
+	},
+	"pendingsAddUrlsButton": {
+		"message": "Add URLs",
+		"description": "button 'Add URLs'"
+	},
+	"pendingsAddUrls": {
+		"message": "Enter a list of URLs separated by a new line",
+		"description": "Label of the add URLs input"
+	},
+	"pendingsAddUrlsOKButton": {
+		"message": "OK",
+		"description": "Add URLs popup confirm button: 'OK'"
+	},
+	"pendingsAddUrlsCancelButton": {
+		"message": "Abbrechen",
+		"description": "Add URLs popup cancel button: 'Cancel'"
 	}
 }

+ 20 - 0
_locales/en/messages.json

@@ -11,6 +11,10 @@
 		"message": "Annotate and save the page...",
 		"description": "Menu entry: 'Annotate and save the page...'"
 	},
+	"menuSaveSelectedLinks": {
+		"message": "Save selected links",
+		"description": "Menu entry: 'Save selected links'"
+	},
 	"menuViewPendingSaves": {
 		"message": "View pending saves...",
 		"description": "Menu entry: 'View pending saves...'"
@@ -590,5 +594,21 @@
 	"pendingsNoPendings": {
 		"message": "No pending saves",
 		"description": "Label displayed when they are no pending saves"
+	},
+	"pendingsAddUrlsButton": {
+		"message": "Add URLs",
+		"description": "button 'Add URLs'"
+	},
+	"pendingsAddUrls": {
+		"message": "Enter a list of URLs separated by a new line",
+		"description": "Label of the add URLs input"
+	},
+	"pendingsAddUrlsOKButton": {
+		"message": "OK",
+		"description": "Add URLs popup confirm button: 'OK'"
+	},
+	"pendingsAddUrlsCancelButton": {
+		"message": "Cancel",
+		"description": "Add URLs popup cancel button: 'Cancel'"
 	}
 }

+ 20 - 0
_locales/es/messages.json

@@ -7,6 +7,10 @@
 		"message": "Guardar página con SingleFile",
 		"description": "Menu entry: 'Save page with SingleFile'"
 	},
+	"menuSaveSelectedLinks": {
+		"message": "Guardar los links seleccionados",
+		"description": "Menu entry: 'Save selected links'"
+	},
 	"menuEditAndSavePage": {
 		"message": "Anotar y guardar la página",
 		"description": "Menu entry: 'Annotate and save the page...'"
@@ -590,5 +594,21 @@
 	"pendingsNoPendings": {
 		"message": "No pending saves",
 		"description": "Label displayed when they are no pending saves"
+	},
+	"pendingsAddUrlsButton": {
+		"message": "Add URLs",
+		"description": "button 'Add URLs'"
+	},
+	"pendingsAddUrls": {
+		"message": "Enter a list of URLs separated by a new line",
+		"description": "Label of the add URLs input"
+	},
+	"pendingsAddUrlsOKButton": {
+		"message": "OK",
+		"description": "Add URLs popup confirm button: 'OK'"
+	},
+	"pendingsAddUrlsCancelButton": {
+		"message": "Cancel",
+		"description": "Add URLs popup cancel button: 'Cancel'"
 	}
 }

+ 20 - 0
_locales/fr/messages.json

@@ -7,6 +7,10 @@
 		"message": "Sauver la page avec SingleFile",
 		"description": "Menu entry: 'Save page with SingleFile'"
 	},
+	"menuSaveSelectedLinks": {
+		"message": "Sauver les liens selectionnés",
+		"description": "Menu entry: 'Save selected links'"
+	},
 	"menuEditAndSavePage": {
 		"message": "Annoter et sauver la page...",
 		"description": "Menu entry: 'Annotate and save the page...'"
@@ -590,5 +594,21 @@
 	"pendingsNoPendings": {
 		"message": "Aucune sauvegarde en cours",
 		"description": "Label displayed when they are no pending saves"
+	},
+	"pendingsAddUrlsButton": {
+		"message": "Ajouter URLs",
+		"description": "button 'Add URLs'"
+	},
+	"pendingsAddUrls": {
+		"message": "Entrez une liste d'URLs séparées par une nouvelle ligne",
+		"description": "Label of the add URLs input"
+	},
+	"pendingsAddUrlsOKButton": {
+		"message": "OK",
+		"description": "Add URLs popup confirm button: 'OK'"
+	},
+	"pendingsAddUrlsCancelButton": {
+		"message": "Annuler",
+		"description": "Add URLs popup cancel button: 'Cancel'"
 	}
 }

+ 20 - 0
_locales/ja/messages.json

@@ -7,6 +7,10 @@
 		"message": "SingleFile でページを保存",
 		"description": "Menu entry: 'Save page with SingleFile'"
 	},
+	"menuSaveSelectedLinks": {
+		"message": "Save selected links",
+		"description": "Menu entry: 'Save selected links'"
+	},
 	"menuEditAndSavePage": {
 		"message": "ページに注釈を付けて保存する...",
 		"description": "Menu entry: 'Annotate and save the page...'"
@@ -590,5 +594,21 @@
 	"pendingsNoPendings": {
 		"message": "保留中の保存はありません",
 		"description": "Label displayed when they are no pending saves"
+	},
+	"pendingsAddUrlsButton": {
+		"message": "Add URLs",
+		"description": "button 'Add URLs'"
+	},
+	"pendingsAddUrls": {
+		"message": "Enter a list of URLs separated by a new line",
+		"description": "Label of the add URLs input"
+	},
+	"pendingsAddUrlsOKButton": {
+		"message": "OK",
+		"description": "Add URLs popup confirm button: 'OK'"
+	},
+	"pendingsAddUrlsCancelButton": {
+		"message": "Cancel",
+		"description": "Add URLs popup cancel button: 'Cancel'"
 	}
 }

+ 20 - 0
_locales/pl/messages.json

@@ -7,6 +7,10 @@
 		"message": "Zapisz stronę z SingleFile",
 		"description": "Menu entry: 'Save page with SingleFile'"
 	},
+	"menuSaveSelectedLinks": {
+		"message": "Save selected links",
+		"description": "Menu entry: 'Save selected links'"
+	},
 	"menuEditAndSavePage": {
 		"message": "Adnotuj i zapisz stronę...",
 		"description": "Menu entry: 'Annotate and save the page...'"
@@ -590,5 +594,21 @@
 	"pendingsNoPendings": {
 		"message": "Brak oczekujących zapisów",
 		"description": "Label displayed when they are no pending saves"
+	},
+	"pendingsAddUrlsButton": {
+		"message": "Add URLs",
+		"description": "button 'Add URLs'"
+	},
+	"pendingsAddUrls": {
+		"message": "Enter a list of URLs separated by a new line",
+		"description": "Label of the add URLs input"
+	},
+	"pendingsAddUrlsOKButton": {
+		"message": "OK",
+		"description": "Add URLs popup confirm button: 'OK'"
+	},
+	"pendingsAddUrlsCancelButton": {
+		"message": "Cancel",
+		"description": "Add URLs popup cancel button: 'Cancel'"
 	}
 }

+ 20 - 0
_locales/ru/messages.json

@@ -7,6 +7,10 @@
 		"message": "Сохранить страницу с помощью SingleFile",
 		"description": "Menu entry: 'Save page with SingleFile'"
 	},
+	"menuSaveSelectedLinks": {
+		"message": "Save selected links",
+		"description": "Menu entry: 'Save selected links'"
+	},
 	"menuEditAndSavePage": {
 		"message": "Аннотировать и сохранить страницу...",
 		"description": "Menu entry: 'Annotate and save the page...'"
@@ -590,5 +594,21 @@
 	"pendingsNoPendings": {
 		"message": "Нет отложенных сохранений",
 		"description": "Label displayed when they are no pending saves"
+	},
+	"pendingsAddUrlsButton": {
+		"message": "Add URLs",
+		"description": "button 'Add URLs'"
+	},
+	"pendingsAddUrls": {
+		"message": "Enter a list of URLs separated by a new line",
+		"description": "Label of the add URLs input"
+	},
+	"pendingsAddUrlsOKButton": {
+		"message": "OK",
+		"description": "Add URLs popup confirm button: 'OK'"
+	},
+	"pendingsAddUrlsCancelButton": {
+		"message": "Cancel",
+		"description": "Add URLs popup cancel button: 'Cancel'"
 	}
 }

+ 20 - 0
_locales/uk/messages.json

@@ -7,6 +7,10 @@
 		"message": "Зберегти сторінку з допомогою SingleFile",
 		"description": "Menu entry: 'Save page with SingleFile'"
 	},
+	"menuSaveSelectedLinks": {
+		"message": "Save selected links",
+		"description": "Menu entry: 'Save selected links'"
+	},
 	"menuEditAndSavePage": {
 		"message": "Анотувати і зберегти сторінку...",
 		"description": "Menu entry: 'Annotate and save the page...'"
@@ -590,5 +594,21 @@
 	"pendingsNoPendings": {
 		"message": "No pending saves",
 		"description": "Label displayed when they are no pending saves"
+	},
+	"pendingsAddUrlsButton": {
+		"message": "Add URLs",
+		"description": "button 'Add URLs'"
+	},
+	"pendingsAddUrls": {
+		"message": "Enter a list of URLs separated by a new line",
+		"description": "Label of the add URLs input"
+	},
+	"pendingsAddUrlsOKButton": {
+		"message": "OK",
+		"description": "Add URLs popup confirm button: 'OK'"
+	},
+	"pendingsAddUrlsCancelButton": {
+		"message": "Cancel",
+		"description": "Add URLs popup cancel button: 'Cancel'"
 	}
 }

+ 20 - 0
_locales/zh_CN/messages.json

@@ -7,6 +7,10 @@
 		"message": "使用 SingleFile 保存页面",
 		"description": "菜单项: '使用 SingleFile 保存页面'"
 	},
+	"menuSaveSelectedLinks": {
+		"message": "Save selected links",
+		"description": "Menu entry: 'Save selected links'"
+	},
 	"menuEditAndSavePage": {
 		"message": "评论并保存页面...",
 		"description": "Menu entry: 'Annotate and save the page...'"
@@ -590,5 +594,21 @@
 	"pendingsNoPendings": {
 		"message": "No pending saves",
 		"description": "Label displayed when they are no pending saves"
+	},
+	"pendingsAddUrlsButton": {
+		"message": "Add URLs",
+		"description": "button 'Add URLs'"
+	},
+	"pendingsAddUrls": {
+		"message": "Enter a list of URLs separated by a new line",
+		"description": "Label of the add URLs input"
+	},
+	"pendingsAddUrlsOKButton": {
+		"message": "OK",
+		"description": "Add URLs popup confirm button: 'OK'"
+	},
+	"pendingsAddUrlsCancelButton": {
+		"message": "Cancel",
+		"description": "Add URLs popup cancel button: 'Cancel'"
 	}
 }

+ 20 - 0
_locales/zh_TW/messages.json

@@ -7,6 +7,10 @@
 		"message": "使用 SingleFile 保存頁面",
 		"description": "菜單項: '使用 SingleFile 保存頁面'"
 	},
+	"menuSaveSelectedLinks": {
+		"message": "Save selected links",
+		"description": "Menu entry: 'Save selected links'"
+	},
 	"menuEditAndSavePage": {
 		"message": "評論並保存頁面...",
 		"description": "Menu entry: 'Annotate and save the page...'"
@@ -590,5 +594,21 @@
 	"pendingsNoPendings": {
 		"message": "No pending saves",
 		"description": "Label displayed when they are no pending saves"
+	},
+	"pendingsAddUrlsButton": {
+		"message": "Add URLs",
+		"description": "button 'Add URLs'"
+	},
+	"pendingsAddUrls": {
+		"message": "Enter a list of URLs separated by a new line",
+		"description": "Label of the add URLs input"
+	},
+	"pendingsAddUrlsOKButton": {
+		"message": "OK",
+		"description": "Add URLs popup confirm button: 'OK'"
+	},
+	"pendingsAddUrlsCancelButton": {
+		"message": "Cancel",
+		"description": "Add URLs popup cancel button: 'Cancel'"
 	}
 }

+ 58 - 17
extension/core/bg/business.js

@@ -48,7 +48,8 @@ singlefile.extension.core.bg.business = (() => {
 	return {
 		isSavingTab: tab => Boolean(tasks.find(taskInfo => taskInfo.tab.id == tab.id)),
 		saveTabs,
-		saveLink,
+		saveUrls,
+		saveSelectedLinks,
 		cancelTab,
 		cancelTask: taskId => cancelTask(tasks.find(taskInfo => taskInfo.taskId == taskId)),
 		cancelAllTasks: () => Array.from(tasks).forEach(cancelTask),
@@ -70,9 +71,34 @@ singlefile.extension.core.bg.business = (() => {
 		onTabRemoved: cancelTab
 	};
 
+	async function saveSelectedLinks(tab) {
+		const tabs = singlefile.extension.core.bg.tabs;
+		const tabOptions = { extensionScriptFiles, tabId: tab.id, tabIndex: tab.index };
+		const scriptsInjected = await singlefile.extension.injectScript(tab.id, tabOptions);
+		if (scriptsInjected) {
+			const response = await tabs.sendMessage(tab.id, { method: "content.getSelectedLinks" });
+			if (response.urls && response.urls.length) {
+				await saveUrls(response.urls);
+			}
+		} else {
+			singlefile.extension.ui.bg.main.onForbiddenDomain(tab);
+		}
+	}
+
+	async function saveUrls(urls, options = {}) {
+		await Promise.all(urls.map(async url => {
+			const tabOptions = await singlefile.extension.core.bg.config.getOptions(url);
+			Object.keys(options).forEach(key => tabOptions[key] = options[key]);
+			tabOptions.autoClose = true;
+			tabOptions.extensionScriptFiles = extensionScriptFiles;
+			tasks.push({ id: currentTaskId, status: "pending", tab: { url }, options: tabOptions, method: "content.save" });
+			currentTaskId++;
+		}));
+		await runTasks();
+	}
+
 	async function saveTabs(tabs, options = {}) {
 		const config = singlefile.extension.core.bg.config;
-		const maxParallelWorkers = (await config.get()).maxParallelWorkers;
 		const autosave = singlefile.extension.core.bg.autosave;
 		const ui = singlefile.extension.ui.bg.main;
 		await Promise.all(tabs.map(async tab => {
@@ -99,23 +125,27 @@ singlefile.extension.core.bg.business = (() => {
 				}
 			}
 		}));
+		await runTasks();
+	}
+
+	async function runTasks() {
+		const config = singlefile.extension.core.bg.config;
+		const maxParallelWorkers = (await config.get()).maxParallelWorkers;
 		const processingCount = tasks.filter(taskInfo => taskInfo.status == "processing").length;
-		for (let index = 0; index < Math.min(tabs.length, (maxParallelWorkers - processingCount)); index++) {
+		for (let index = 0; index < Math.min(tasks.length - processingCount, (maxParallelWorkers - processingCount)); index++) {
 			runTask();
 		}
 	}
 
 	function runTask() {
+		const ui = singlefile.extension.ui.bg.main;
+		const tabs = singlefile.extension.core.bg.tabs;
 		const taskInfo = tasks.find(taskInfo => taskInfo.status == "pending");
 		if (taskInfo) {
-			const tabId = taskInfo.tab.id;
 			const taskId = taskInfo.id;
-			return new Promise((resolve, reject) => {
+			return new Promise(async (resolve, reject) => {
 				taskInfo.status = "processing";
 				taskInfo.resolve = async () => {
-					if (taskInfo.options.autoClose && !taskInfo.cancelled) {
-						singlefile.extension.core.bg.tabs.remove(taskInfo.tab.id);
-					}
 					tasks.splice(tasks.findIndex(taskInfo => taskInfo.id == taskId), 1);
 					resolve();
 					await runTask();
@@ -125,12 +155,30 @@ singlefile.extension.core.bg.business = (() => {
 					reject(error);
 					await runTask();
 				};
+				if (!taskInfo.tab.id) {
+					const tab = await tabs.create({ url: taskInfo.tab.url, active: false });
+					taskInfo.tab.id = taskInfo.options.tabId = tab.id;
+					taskInfo.tab.index = taskInfo.options.tabIndex = tab.index;
+					ui.onStart(taskInfo.tab.id, INJECT_SCRIPTS_STEP);
+					const scriptsInjected = await singlefile.extension.injectScript(taskInfo.tab.id, taskInfo.options);
+					if (scriptsInjected) {
+						ui.onStart(taskInfo.tab.id, EXECUTE_SCRIPTS_STEP);
+					} else {
+						taskInfo.reject();
+						return;
+					}
+				}
 				taskInfo.options.taskId = taskId;
-				singlefile.extension.core.bg.tabs.sendMessage(tabId, { method: taskInfo.method, options: taskInfo.options })
+				tabs.sendMessage(taskInfo.tab.id, { method: taskInfo.method, options: taskInfo.options })
+					.then(() => {
+						if (taskInfo.options.autoClose && !taskInfo.cancelled) {
+							tabs.remove(taskInfo.tab.id);
+						}
+					})
 					.catch(error => {
 						if (error && (!error.message || (error.message != ERROR_CONNECTION_LOST_CHROMIUM && error.message != ERROR_CONNECTION_ERROR_CHROMIUM && error.message != ERROR_CONNECTION_LOST_GECKO))) {
 							console.log(error); // eslint-disable-line no-console
-							singlefile.extension.ui.bg.main.onError(tabId);
+							ui.onError(taskInfo.tab.id);
 							taskInfo.reject(error);
 						}
 					});
@@ -138,13 +186,6 @@ singlefile.extension.core.bg.business = (() => {
 		}
 	}
 
-	async function saveLink(url, options = {}) {
-		const tabs = singlefile.extension.core.bg.tabs;
-		const tab = await tabs.create({ url, active: false });
-		options.autoClose = true;
-		await saveTabs([tab], options);
-	}
-
 	function cancelTab(tabId) {
 		Array.from(tasks).filter(taskInfo => taskInfo.tab.id == tabId && !taskInfo.options.autoSave).forEach(cancelTask);
 	}

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

@@ -77,6 +77,10 @@ singlefile.extension.core.bg.downloads = (() => {
 			singlefile.extension.core.bg.business.cancelAllTasks();
 			return {};
 		}
+		if (message.method.endsWith(".saveUrls")) {
+			singlefile.extension.core.bg.business.saveUrls(message.urls);
+			return {};
+		}
 	}
 
 	async function downloadTabPage(message, tab) {

+ 6 - 1
extension/core/content/content-main.js

@@ -35,7 +35,7 @@ this.singlefile.extension.core.content.main = this.singlefile.extension.core.con
 		frameFetch: singlefile.extension.lib.fetch.content.resources.frameFetch
 	});
 	browser.runtime.onMessage.addListener(message => {
-		if (message.method == "content.save" || message.method == "content.cancelSave") {
+		if (message.method == "content.save" || message.method == "content.cancelSave" || message.method == "content.getSelectedLinks") {
 			return onMessage(message);
 		}
 	});
@@ -58,6 +58,11 @@ this.singlefile.extension.core.content.main = this.singlefile.extension.core.con
 				}
 				return {};
 			}
+			if (message.method == "content.getSelectedLinks") {
+				return {
+					urls: ui.getSelectedLinks()
+				};
+			}
 		}
 	}
 

+ 12 - 2
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_SAVE_SELECTED_LINKS = "save-selectec-links";
 	const MENU_ID_VIEW_PENDINGS = "view-pendings";
 	const MENU_ID_SELECT_PROFILE = "select-profile";
 	const MENU_ID_SELECT_PROFILE_PREFIX = "select-profile-";
@@ -51,6 +52,7 @@ singlefile.extension.ui.bg.menus = (() => {
 	const MENU_CREATE_DOMAIN_RULE_MESSAGE = browser.i18n.getMessage("menuCreateDomainRule");
 	const MENU_UPDATE_RULE_MESSAGE = browser.i18n.getMessage("menuUpdateRule");
 	const MENU_SAVE_PAGE_MESSAGE = browser.i18n.getMessage("menuSavePage");
+	const MENU_SAVE_SELECTED_LINKS = browser.i18n.getMessage("menuSaveSelectedLinks");
 	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");
@@ -125,6 +127,11 @@ singlefile.extension.ui.bg.menus = (() => {
 					title: MENU_EDIT_AND_SAVE_PAGE_MESSAGE
 				});
 			}
+			menus.create({
+				id: MENU_ID_SAVE_SELECTED_LINKS,
+				contexts: options.contextMenuEnabled ? defaultContextsDisabled.concat(["selection"]) : defaultContextsDisabled.concat(["selection"]),
+				title: MENU_SAVE_SELECTED_LINKS
+			});
 			menus.create({
 				id: "separator-1",
 				contexts: defaultContexts,
@@ -342,18 +349,21 @@ singlefile.extension.ui.bg.menus = (() => {
 			menus.onClicked.addListener(async (event, tab) => {
 				if (event.menuItemId == MENU_ID_SAVE_PAGE) {
 					if (event.linkUrl) {
-						business.saveLink(event.linkUrl);
+						business.saveUrls([event.linkUrl]);
 					} else {
 						business.saveTabs([tab]);
 					}
 				}
 				if (event.menuItemId == MENU_ID_EDIT_AND_SAVE_PAGE) {
 					if (event.linkUrl) {
-						business.saveLink(event.linkUrl, { openEditor: true });
+						business.saveUrls([event.linkUrl], { openEditor: true });
 					} else {
 						business.saveTabs([tab], { openEditor: true });
 					}
 				}
+				if (event.menuItemId == MENU_ID_SAVE_SELECTED_LINKS) {
+					business.saveSelectedLinks(tab);
+				}
 				if (event.menuItemId == MENU_ID_VIEW_PENDINGS) {
 					await tabs.create({ active: true, url: "/extension/ui/pages/pendings.html" });
 				}

+ 37 - 1
extension/ui/bg/ui-pendings.js

@@ -21,7 +21,7 @@
  *   Source.
  */
 
-/* global browser, document, setInterval, location */
+/* global browser, window, document, setInterval, location */
 
 (async () => {
 
@@ -29,8 +29,16 @@
 	const titleLabel = document.getElementById("titleLabel");
 	const resultsTable = document.getElementById("resultsTable");
 	const cancelAllButton = document.getElementById("cancelAllButton");
+	const addUrlsButton = document.getElementById("addUrlsButton");
+	const addUrlsInput = document.getElementById("addUrlsInput");
+	const addUrlsCancelButton = document.getElementById("addUrlsCancelButton");
+	const addUrlsOKButton = document.getElementById("addUrlsOKButton");
 	document.title = browser.i18n.getMessage("pendingsTitle");
 	cancelAllButton.textContent = browser.i18n.getMessage("pendingsCancelAllButton");
+	addUrlsButton.textContent = browser.i18n.getMessage("pendingsAddUrlsButton");
+	addUrlsCancelButton.textContent = browser.i18n.getMessage("pendingsAddUrlsCancelButton");
+	addUrlsOKButton.textContent = browser.i18n.getMessage("pendingsAddUrlsOKButton");
+	document.getElementById("addUrlsLabel").textContent = browser.i18n.getMessage("pendingsAddUrls");
 	URLLabel.textContent = browser.i18n.getMessage("pendingsURLTitle");
 	titleLabel.textContent = browser.i18n.getMessage("pendingsTitleTitle");
 	document.getElementById("statusLabel").textContent = browser.i18n.getMessage("pendingsStatusTitle");
@@ -44,6 +52,7 @@
 		await browser.runtime.sendMessage({ method: "downloads.cancelAll" });
 		await refresh();
 	};
+	addUrlsButton.onclick = displayAddUrlsPopup;
 	if (location.href.endsWith("#side-panel")) {
 		document.documentElement.classList.add("side-panel");
 	}
@@ -105,6 +114,33 @@
 		await refresh();
 	}
 
+	async function displayAddUrlsPopup() {
+		document.getElementById("formAddUrls").style.setProperty("display", "flex");
+		document.querySelector("#formAddUrls .popup-content").style.setProperty("align-self", "center");
+		addUrlsInput.value = "";
+		addUrlsInput.focus();
+		document.body.style.setProperty("overflow-y", "hidden");
+		const urls = await new Promise(resolve => {
+			addUrlsOKButton.onclick = event => hideAndResolve(event, addUrlsInput.value);
+			addUrlsCancelButton.onclick = event => hideAndResolve(event);
+			window.onkeyup = event => {
+				if (event.key == "Escape") {
+					hideAndResolve(event);
+				}
+			};
+
+			function hideAndResolve(event, value = "") {
+				event.preventDefault();
+				document.getElementById("formAddUrls").style.setProperty("display", "none");
+				document.body.style.setProperty("overflow-y", "");
+				resolve(value.split("\n").map(url => url.trim()).filter(url => url));
+			}
+		});
+		if (urls.length) {
+			await browser.runtime.sendMessage({ method: "downloads.saveUrls", urls });
+		}
+	}
+
 	async function refresh(force) {
 		const results = await browser.runtime.sendMessage({ method: "downloads.getInfo" });
 		const currentState = JSON.stringify(results);

+ 39 - 0
extension/ui/content/content-ui-main.js

@@ -48,6 +48,7 @@ this.singlefile.extension.ui.content.main = this.singlefile.extension.ui.content
 	Array.from(getComputedStyle(document.body)).forEach(property => allProperties.add(property));
 
 	return {
+		getSelectedLinks,
 		markSelection,
 		unmarkSelection,
 		prompt(message, defaultValue) {
@@ -116,6 +117,44 @@ this.singlefile.extension.ui.content.main = this.singlefile.extension.ui.content
 		onEndStageTask() { }
 	};
 
+	function getSelectedLinks() {
+		let selectionFound;
+		const links = [];
+		const selection = getSelection();
+		for (let indexRange = 0; indexRange < selection.rangeCount; indexRange++) {
+			let range = selection.getRangeAt(indexRange);
+			if (range && range.commonAncestorContainer) {
+				const treeWalker = document.createTreeWalker(range.commonAncestorContainer);
+				let rangeSelectionFound = false;
+				let finished = false;
+				while (!finished) {
+					if (rangeSelectionFound || treeWalker.currentNode == range.startContainer || treeWalker.currentNode == range.endContainer) {
+						rangeSelectionFound = true;
+						if (range.startContainer != range.endContainer || range.startOffset != range.endOffset) {
+							selectionFound = true;
+							if (treeWalker.currentNode.tagName == "A" && treeWalker.currentNode.href) {
+								links.push(treeWalker.currentNode.href);
+							}
+						}
+					}
+					if (treeWalker.currentNode == range.endContainer) {
+						finished = true;
+					} else {
+						treeWalker.nextNode();
+					}
+				}
+				if (selectionFound && treeWalker.currentNode == range.endContainer && treeWalker.currentNode.querySelectorAll) {
+					treeWalker.currentNode.querySelectorAll("*").forEach(descendantElement => {
+						if (descendantElement.tagName == "A" && descendantElement.href) {
+							links.push(treeWalker.currentNode.href);
+						}
+					});
+				}
+			}
+		}
+		return links;
+	}
+
 	async function markSelection(optionallySelected) {
 		let selectionFound = markSelectedContent();
 		if (selectionFound || optionallySelected) {

+ 105 - 18
extension/ui/pages/pendings.css

@@ -7,12 +7,16 @@ body {
 }
 
 main {
+    font-family: sans-serif;
+}
+
+body>main {
     background-color: #fff;
     border: solid 1px rgb(191, 191, 191);
 }
 
-main,
-header {
+body>main,
+body>header {
     font-size: 12px;
     font-family: sans-serif;
     margin: 0;
@@ -21,9 +25,14 @@ header {
     max-width: 1024px;
 }
 
-header {
+body>header {
     display: flex;
     flex-direction: column;
+    align-items: flex-end;
+}
+
+.header-buttons {
+    flex-direction: row;
 }
 
 button {
@@ -39,13 +48,14 @@ button:not([disabled]):hover {
     background-color: #ededed;
 }
 
-header button {
+body>header button {
     margin-top: 5px;
     margin-bottom: 5px;
     align-self: flex-end;
     padding: 5px;
     padding-left: 10px;
     padding-right: 10px;
+    margin-left: 8px;
 }
 
 .result-row {
@@ -112,17 +122,17 @@ header button {
 
 html.side-panel,
 .side-panel .result-head,
-.side-panel main {
+.side-panel body>main {
     background-color: #fbfbfb;
 }
 
-.side-panel header button {
+.side-panel .header-buttons {
     position: absolute;
     top: 4px;
     right: 12px;
 }
 
-.side-panel main {
+.side-panel body>main {
     border: 0;
     margin-left: 8px;
     margin-right: 12px;
@@ -137,11 +147,68 @@ html.side-panel,
     padding: 0px;
 }
 
+.popup {
+    width: 100%;
+    height: 100%;
+    position: fixed;
+    top: 0;
+    left: 0;
+    background-color: rgba(191, 191, 191, .75);
+    display: none;
+}
+
+.popup-content {
+    align-self: center;
+    width: 90%;
+    max-width: 320px;
+    margin-top: 100px;
+    margin-bottom: 100px;
+    margin-left: auto;
+    margin-right: auto;
+    background-color: white;
+    box-shadow: 5px 5px #ababab;
+}
+
+.popup-content header {
+    padding-top: 10px;
+    padding-left: 10px;
+    padding-bottom: 20px;
+    font-size: 14px;
+    line-height: 20px;
+}
+
+.popup-content main {
+    padding-top: 10px;
+    padding-left: 10px;
+    padding-bottom: 10px;
+}
+
+.popup-content main textarea {
+    margin-bottom: 10px;
+    padding: 2px;
+    font-size: 13px;
+    margin-left: 0px;
+    width: calc(100% - 16px);
+    min-width: calc(100% - 16px);
+    max-width: calc(100% - 16px);
+    height: 60px;
+}
+
+.popup-content footer {
+    text-align: right;
+    padding-left: 10px;
+    padding-right: 10px;
+}
+
+.popup-content footer button {
+    margin-bottom: 10px;
+}
+
 @media (max-width:400px) {
 
-    main,
-    header,
-    header>button {
+    body>main,
+    body>header,
+    .header-buttons>button {
         font-size: 11px;
     }
 
@@ -151,9 +218,9 @@ html.side-panel,
         min-height: 30px;
     }
 
-    .side-panel header button {
-        top: 0px;        
-    }    
+    .side-panel .header-buttons {
+        top: 0px;
+    }
 }
 
 @media (prefers-color-scheme: dark) {
@@ -162,12 +229,13 @@ html.side-panel,
         color: #fdfdfd;
     }
 
-    main {
+    body>main {
         border-color: rgb(81, 81, 81);
     }
 
-    main,
-    button {
+    body>main,
+    button,
+    .popup-content {
         background-color: #202023;
         color: #fdfdfd;
     }
@@ -197,10 +265,29 @@ html.side-panel,
         color: #2A2A2E;
     }
 
+    textarea {
+        background-color: #fff;
+    }
+
+    button:focus {
+        color: black;
+        border-color: black;
+        background-color: #d2d2d2;
+    }
+
+    .popup {
+        background-color: rgba(59, 59, 59, 0.95);
+    }
+
+    .popup-content {
+        box-shadow: 5px 5px #000000;
+    }
+
     html.side-panel,
     .side-panel .result-head,
-    .side-panel main,
-    .side-panel button {
+    .side-panel body>main,
+    .side-panel button,
+    .side-panel .popup-content {
         background-color: #38383d;
     }
 

+ 16 - 1
extension/ui/pages/pendings.html

@@ -10,7 +10,10 @@
 
 <body>
 	<header>
-		<button id="cancelAllButton"></button>
+		<div class="header-buttons">
+			<button id="addUrlsButton"></button>
+			<button id="cancelAllButton"></button>
+		</div>
 	</header>
 	<main>
 		<div class="result-row result-head">
@@ -23,6 +26,18 @@
 		<div id="resultsTable">
 		</div>
 	</main>
+	<div id="formAddUrls" class="popup">
+		<form class="popup-content">
+			<header>
+				<span id="addUrlsLabel"></span>
+			</header>
+			<main><textarea id="addUrlsInput" type="text"></textarea></main>
+			<footer>
+				<button type="submit" id="addUrlsOKButton"></button>
+				<button id="addUrlsCancelButton"></button>
+			</footer>
+		</form>
+	</div>
 	<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>