Browse Source

added options to customize the infobar content

Gildas 7 years ago
parent
commit
e2a470caab

+ 11 - 3
_locales/en/messages.json

@@ -91,13 +91,21 @@
         "message": "add entry in the context menu of the webpage",
         "description": "Options page label: 'add entry in the context menu of the webpage'"
     },
+    "optionDisplayShadow": {
+        "message": "overlay a shadow on the page during processing",
+        "description": "Options page label: 'overlay a shadow on the page during processing'"
+    },
     "optionDisplayInfobar": {
         "message": "display an infobar when viewing a saved page",
         "description": "Options page label: 'display an infobar when viewing a saved page'"
     },
-    "optionDisplayShadow": {
-        "message": "overlay a shadow on the page during processing",
-        "description": "Options page label: 'overlay a shadow on the page during processing'"
+    "optionInfobarTemplate": {
+        "message": "template of the infobar content",
+        "description": "Options page label: 'template of the infobar content'"
+    },
+    "optionConfirmInfobar": {
+        "message": "open a prompt dialog to edit the infobar content",
+        "description": "Options page label: 'open a prompt dialog to edit the infobar content'"
     },
     "optionsFileNameSubTitle": {
         "message": "File name",

+ 11 - 3
_locales/fr/messages.json

@@ -91,13 +91,21 @@
         "message": "ajouter une entrée dans le menu contextuel de la page web",
         "description": "Options page label: 'add entry in the context menu of the webpage'"
     },
+    "optionDisplayShadow": {
+        "message": "assombrir la page pendant la sauvegarde",
+        "description": "Options page label: 'overlay a shadow on the page during processing'"
+    },
     "optionDisplayInfobar": {
         "message": "afficher une barre d'information quand une sauvegarde est affichée",
         "description": "Options page label: 'display an infobar when viewing a saved page'"
     },
-    "optionDisplayShadow": {
-        "message": "assombrir la page pendant la sauvegarde",
-        "description": "Options page label: 'overlay a shadow on the page during processing'"
+    "optionInfobarTemplate": {
+        "message": "modèle du contenu de la barre d'information",
+        "description": "Options page label: 'template of the infobar content'"
+    },
+    "optionConfirmInfobar": {
+        "message": "ouvrir une boite de dialogue pour éditer le contenu de la barre d'information",
+        "description": "Options page label: 'open a prompt dialog to edit the infobar content'"
     },
     "optionsFileNameSubTitle": {
         "message": "Nom de fichier",

+ 11 - 3
_locales/ja/messages.json

@@ -91,13 +91,21 @@
         "message": "ウェブページのコンテキストメニューにエントリを追加する",
         "description": "オプションのページラベル: 'ウェブページのコンテキストメニューにエントリを追加する'"
     },
+    "optionDisplayShadow": {
+        "message": "処理中のページに影を重ねる",
+        "description": "オプションのページラベル: '処理中のページに影を重ねる'"
+    },
     "optionDisplayInfobar": {
         "message": "保存したページを表示するときに情報バーを表示する",
         "description": "オプションのページラベル: '保存したページを表示するときに情報バーを表示する'"
     },
-    "optionDisplayShadow": {
-        "message": "処理中のページに影を重ねる",
-        "description": "オプションのページラベル: '処理中のページに影を重ねる'"
+    "optionInfobarTemplate": {
+        "message": "template of the infobar content",
+        "description": "Options page label: 'template of the infobar content'"
+    },
+    "optionConfirmInfobar": {
+        "message": "open a prompt dialog to edit the infobar content",
+        "description": "Options page label: 'open a prompt dialog to edit the infobar content'"
     },
     "optionsFileNameSubTitle": {
         "message": "ファイル名",

+ 11 - 3
_locales/pl/messages.json

@@ -91,13 +91,21 @@
         "message": "dodawaj wpis w menu kontekstowym strony",
         "description": "Options page label: 'add entry in the context menu of the webpage'"
     },
+    "optionDisplayShadow": {
+        "message": "nakładaj cień na stronę podczas przetwarzania",
+        "description": "Options page label: 'overlay a shadow on the page during processing'"
+    },
     "optionDisplayInfobar": {
         "message": "wyświetlaj pasek informacyjny podczas przeglądania zapisanej strony",
         "description": "Options page label: 'display an infobar when viewing a saved page'"
     },
-    "optionDisplayShadow": {
-        "message": "nakładaj cień na stronę podczas przetwarzania",
-        "description": "Options page label: 'overlay a shadow on the page during processing'"
+    "optionInfobarTemplate": {
+        "message": "template of the infobar content",
+        "description": "Options page label: 'template of the infobar content'"
+    },
+    "optionConfirmInfobar": {
+        "message": "open a prompt dialog to edit the infobar content",
+        "description": "Options page label: 'open a prompt dialog to edit the infobar content'"
     },
     "optionsFileNameSubTitle": {
         "message": "Nazwa pliku",

+ 11 - 3
_locales/ru/messages.json

@@ -91,13 +91,21 @@
         "message": "добавить пункт в контекстное меню веб-страницы",
         "description": "Options page label: 'add entry in the context menu of the webpage'"
     },
+    "optionDisplayShadow": {
+        "message": "накладывать тень на страницу во время обработки",
+        "description": "Options page label: 'overlay a shadow on the page during processing'"
+    },
     "optionDisplayInfobar": {
         "message": "отображать информационную панель при просмотре сохраненной страницы",
         "description": "Options page label: 'display an infobar when viewing a saved page'"
     },
-    "optionDisplayShadow": {
-        "message": "накладывать тень на страницу во время обработки",
-        "description": "Options page label: 'overlay a shadow on the page during processing'"
+    "optionInfobarTemplate": {
+        "message": "template of the infobar content",
+        "description": "Options page label: 'template of the infobar content'"
+    },
+    "optionConfirmInfobar": {
+        "message": "open a prompt dialog to edit the infobar content",
+        "description": "Options page label: 'open a prompt dialog to edit the infobar content'"
     },
     "optionsFileNameSubTitle": {
         "message": "Имя файла",

+ 5 - 0
extension/core/bg/config.js

@@ -33,6 +33,8 @@ singlefile.config = (() => {
 		compressCSS: true,
 		lazyLoadImages: true,
 		filenameTemplate: "{page-title} ({date-iso} {time-locale}).html",
+		infobarTemplate: "",
+		confirmInfobar: false,
 		confirmFilename: false,
 		contextMenuEnabled: true,
 		shadowEnabled: true,
@@ -99,6 +101,9 @@ singlefile.config = (() => {
 			}
 			delete config.appendSaveDate;
 		}
+		if (config.infobarTemplate === undefined) {
+			config.infobarTemplate = "";
+		}
 		if (config.removeImports === undefined) {
 			config.removeImports = true;
 		}

+ 3 - 0
extension/core/content/content.js

@@ -114,6 +114,9 @@ this.singlefile.top = this.singlefile.top || (() => {
 		[options.framesData] = await Promise.all(preInitializationPromises);
 		await processor.initialize();
 		await processor.run();
+		if (options.confirmInfobar) {
+			options.infobarContent = prompt("Infobar content", options.infobarContent) || "";
+		}
 		const page = await processor.getPageData();
 		if (options.selected) {
 			unmarkSelectedContent();

+ 12 - 2
extension/ui/bg/options.js

@@ -59,6 +59,8 @@
 	const otherResourcesLabel = document.getElementById("otherResourcesLabel");
 	const autoSaveLabel = document.getElementById("autoSaveLabel");
 	const groupDuplicateImagesLabel = document.getElementById("groupDuplicateImagesLabel");
+	const confirmInfobarLabel = document.getElementById("confirmInfobarLabel");
+	const infobarTemplateLabel = document.getElementById("infobarTemplateLabel");
 	const miscLabel = document.getElementById("miscLabel");
 	const helpLabel = document.getElementById("helpLabel");
 	const resetButton = document.getElementById("resetButton");
@@ -91,6 +93,8 @@
 	const removeAlternativeImagesInput = document.getElementById("removeAlternativeImagesInput");
 	const removeAlternativeMediasInput = document.getElementById("removeAlternativeMediasInput");
 	const groupDuplicateImagesInput = document.getElementById("groupDuplicateImagesInput");
+	const infobarTemplateInput = document.getElementById("infobarTemplateInput");
+	const confirmInfobarInput = document.getElementById("confirmInfobarInput");
 	const expandAllButton = document.getElementById("expandAllButton");
 	let pendingSave = Promise.resolve();
 	resetButton.addEventListener("click", async () => {
@@ -159,7 +163,9 @@
 	otherResourcesLabel.textContent = browser.i18n.getMessage("optionsOtherResourcesSubTitle");
 	autoSaveLabel.textContent = browser.i18n.getMessage("optionsAutoSaveSubTitle");
 	miscLabel.textContent = browser.i18n.getMessage("optionsMiscSubTitle");
-	helpLabel.textContent = browser.i18n.getMessage("optionsHelpLink");	
+	helpLabel.textContent = browser.i18n.getMessage("optionsHelpLink");
+	infobarTemplateLabel.textContent = browser.i18n.getMessage("optionInfobarTemplate");
+	confirmInfobarLabel.textContent = browser.i18n.getMessage("optionConfirmInfobar");
 	resetButton.textContent = browser.i18n.getMessage("optionsResetButton");
 	resetButton.title = browser.i18n.getMessage("optionsResetTooltip");
 
@@ -199,6 +205,8 @@
 		removeAlternativeImagesInput.checked = config.removeAlternativeImages;
 		groupDuplicateImagesInput.checked = config.groupDuplicateImages;
 		removeAlternativeMediasInput.checked = config.removeAlternativeMedias;
+		infobarTemplateInput.value = config.infobarTemplate;
+		confirmInfobarInput.checked = config.confirmInfobar;
 	}
 
 	async function update() {
@@ -231,7 +239,9 @@
 			removeAlternativeFonts: removeAlternativeFontsInput.checked,
 			removeAlternativeImages: removeAlternativeImagesInput.checked,
 			removeAlternativeMedias: removeAlternativeMediasInput.checked,
-			groupDuplicateImages: groupDuplicateImagesInput.checked
+			groupDuplicateImages: groupDuplicateImagesInput.checked,
+			infobarTemplate: infobarTemplateInput.value,
+			confirmInfobar: confirmInfobarInput.checked
 		});
 		await pendingSave;
 		await bgPage.singlefile.ui.menu.refresh();

+ 21 - 7
extension/ui/content/infobar.js

@@ -25,7 +25,7 @@ this.singlefile.infobar = this.singlefile.infobar || (() => {
 	const INFOBAR_TAGNAME = "singlefile-infobar";
 	const LINK_ICON = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAABmJLR0QABQDuAACS38mlAAAACXBIWXMAACfuAAAn7gExzuVDAAAAB3RJTUUH4ggCDDcMnYqGGAAAATtJREFUOMvNk19LwlAYxp+zhOoqpxJ1la3patFVINk/oRDBLuyreiPFMmcj/QQRSOOwpEINDCpwRr7d1HBMc4sufO7Oe877e5/zcA4wbWLDi8urGr2+vXsOFfJZdnPboDtuueoRcQEH6RQDgNBP8bxcpfvmA0QxPHF6u/MMInLVHFDP7kMUwyjks2xU8+ZGkgGAbtSp1e5gRhBc+0KQHHSjTg2TY0tVEItF/wYqV6+pYXKoiox0atvjOuQXYnILqiJj/ztceXUlGEirGGRyC0pCciDDmfm6mlYxiFtNKAkJmb0dV2OxpFGxpNFE0NmFTtxqQpbiHsgojQX1bBuyFMfR4S7zk+PYjE5PcizI0xD+6685jubnZvH41MJwgL+p233B8tKiF7SeXMPnYIB+/8OXg2hERO44wzC1+gJYGGpVbtoqiAAAAABJRU5ErkJggg==";
 	const IMAGE_ICON = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAABIUlEQVQ4y+2TsarCMBSGvxTBRdqiUZAWOrhJB9EXcPKFfCvfQYfulUKHDqXg4CYUJSioYO4mSDX3ttzt3n87fMlHTpIjlsulxpDZbEYYhgghSNOUOI5Ny2mZYBAELBYLer0eAJ7ncTweKYri4x7LJJRS0u12n7XrukgpjSc0CpVSXK/XZ32/31FKNW85z3PW6zXT6RSAJEnIsqy5UGvNZrNhu90CcDqd+C6tT6J+v//2Th+PB2VZ1hN2Oh3G4zGTyQTbtl/YbrdjtVpxu91+Ljyfz0RRhG3bzOfzF+Y4TvNXvlwuaK2pE4tfzr/wzwsty0IIURlL0998KxRCMBqN8H2/wlzXJQxD2u12vVkeDoeUZUkURRU+GAw4HA7s9/sK+wK6CWHasQ/S/wAAAABJRU5ErkJggg==";
-	const SINGLEFILE_COMMENT = "Archive processed by SingleFile";
+	const SINGLEFILE_COMMENT = "SingleFile";
 
 	if (window == top && location && location.href && location.href.startsWith("file:///")) {
 		if (document.readyState == "loading") {
@@ -43,10 +43,10 @@ this.singlefile.infobar = this.singlefile.infobar || (() => {
 		}
 		if (singleFileComment) {
 			const info = singleFileComment.textContent.split("\n");
-			const [, , url, saveDate] = info;
+			const [, , url, saveDate, ...infoData] = info;
 			const config = await browser.runtime.sendMessage({ getConfig: true });
 			if (config.displayInfobar) {
-				initInfobar(url, saveDate);
+				initInfobar(url, saveDate, infoData);
 			}
 		}
 	}
@@ -59,10 +59,20 @@ this.singlefile.infobar = this.singlefile.infobar || (() => {
 		return node.nodeType == Node.COMMENT_NODE && node.textContent.includes(SINGLEFILE_COMMENT);
 	}
 
-	function initInfobar(url, saveDate) {
+	function initInfobar(url, saveDate, infoData) {
 		let infobarElement = document.querySelector(INFOBAR_TAGNAME);
 		if (!infobarElement) {
 			url = url.split("url: ")[1];
+			saveDate = saveDate.split("saved date: ")[1];
+			if (infoData && infoData.length > 1) {
+				let content = infoData[0].split("info: ")[1].trim();
+				for (let indexLine = 1; indexLine < infoData.length - 1; indexLine++) {
+					content += "\n" + infoData[indexLine].trim();
+				}
+				infoData = content.trim();
+			} else {
+				infoData = saveDate;
+			}
 			infobarElement = createElement(INFOBAR_TAGNAME, document.body);
 			infobarElement.style.setProperty("background-color", "#f9f9f9", "important");
 			infobarElement.style.setProperty("display", "block", "important");
@@ -78,11 +88,14 @@ this.singlefile.infobar = this.singlefile.infobar || (() => {
 			infobarElement.style.setProperty("z-index", 2147483647, "important");
 			infobarElement.style.setProperty("text-align", "center", "important");
 			infobarElement.style.setProperty("will-change", "opacity, padding-left, padding-right, width, background-color, color", "important");
+			infobarElement.style.setProperty("margin", "0 0 0 16px", "important");
 			const infoElement = createElement("span", infobarElement);
 			infoElement.style.setProperty("font-family", "Arial", "important");
 			infoElement.style.setProperty("color", "#9aa0a6", "important");
 			infoElement.style.setProperty("line-height", "28px", "important");
-			infoElement.textContent = saveDate.split("saved date: ")[1];
+			infoElement.style.setProperty("word-break", "break-word", "important");
+			infoElement.style.setProperty("white-space", "pre-wrap", "important");
+			infoElement.textContent = infoData;
 			const linkElement = createElement("a", infobarElement);
 			linkElement.style.setProperty("display", "inline-block", "important");
 			linkElement.style.setProperty("padding-left", "8px", "important");
@@ -100,8 +113,9 @@ this.singlefile.infobar = this.singlefile.infobar || (() => {
 			imgElement.style.setProperty("padding-left", "2px", "important");
 			imgElement.style.setProperty("-webkit-padding-start", "2px", "important");
 			imgElement.style.setProperty("cursor", "pointer", "important");
+			infobarElement.style.setProperty("text-align", "right", "important");
 			imgElement.src = LINK_ICON;
-			hideInfobar(infobarElement, linkElement, infoElement, saveDate);
+			hideInfobar(infobarElement, linkElement, infoElement);
 			infobarElement.onmouseover = () => infobarElement.style.setProperty("opacity", 1, "important");
 			document.addEventListener("click", event => {
 				if (event.button === 0) {
@@ -110,7 +124,7 @@ this.singlefile.infobar = this.singlefile.infobar || (() => {
 						element = element.parentElement;
 					}
 					if (element != infobarElement) {
-						hideInfobar(infobarElement, linkElement, infoElement, saveDate);
+						hideInfobar(infobarElement, linkElement, infoElement);
 					}
 				}
 			});

+ 67 - 46
extension/ui/pages/help.html

@@ -19,6 +19,7 @@
 			<a href="#general-notes">Additional notes</a> -
 			<a href="#options">Options description</a> -
 			<a href="#notes">Technical notes</a> -
+			<a href="#template-variables">Template variables</a> -
 			<a href="#known-issues">Known issues</a> -
 			<a href="#unknown-issues">Troubleshooting unknown issues</a> -
 			<a href="#contributors">Contributors</a>
@@ -117,58 +118,30 @@
 						<p class="notice">It is recommended to
 							<u>check</u> this option</p>
 					</li>
+
+					<li>
+						<span class="option">template of the infobar content</span>
+						<p>The template allows you to customize the content displayed in the infobar. You can use any valid character,
+							"\n" for new lines or "\t" for tabs. You can also use any variables from <a href="template-variables">this list</a>
+							anywhere in the template. If the template or the resulting content is empty, the infobar will display the saved
+							date by default.
+						</p>
+					</li>
+
+					<li>
+						<span class="option">open a prompt dialog to edit the infobar content</span>
+						<p>Check this option to display a prompt dialog in order to confirm the infobar content before saving the page.
+						</p>
+					</li>
 				</ul>
 				<p>File name</p>
 				<ul>
 					<li>
 						<span class="option">template</span>
 						<p>The template allows you to customize the file name of saved pages. You can use any valid character and "/" to
-							create sub-folders (with the option "save pages in background" active). You can also use the variables in
-							the list below anywhere in the template.</p>
-						<ul>
-							<li><code>{page-title}</code>: the title of the page</li>
-							<li><code>{page-language}</code>: the language of the page</li>
-							<li><code>{page-description}</code>: the description of the page</li>
-							<li><code>{page-author}</code>: the author of the page</li>
-							<li><code>{page-creator}</code>: the creator of the page</li>
-							<li><code>{page-publisher}</code>: the publisher of the page</li>
-							<li><code>{datetime-iso}</code>: the save date and time in the ISO format (e.g. "2018-09-15T22_38_26_317Z")</li>
-							<li><code>{datetime-utc}</code>: the save date and time in UTC format (e.g. "Sat, 15 Sep 2018 22_38_26 GMT")</li>
-							<li><code>{datetime-locale}</code>: the localized value of the date and time (e.g. "9_16_2018, 12_54_31 AM")</li>
-							<li><code>{date-iso}</code>: the save date in the ISO format (e.g. "2018-09-15")</li>
-							<li><code>{date-locale}</code>: the localized value of the save date (e.g. "16_09_2018")</li>
-							<li><code>{time-iso}</code>: the save time in the ISO format (e.g. "22_38_26_317")</li>
-							<li><code>{time-locale}</code>: the localized value of the save time (e.g. "00_38_26")</li>
-							<li><code>{day-utc}</code>: the day of the save date in UTC format (e.g. "15")</li>
-							<li><code>{day-locale}</code>: the localized value of the day (e.g. "15")</li>
-							<li><code>{month-utc}</code>: the month of the save date in UTC format (e.g. "09")</li>
-							<li><code>{month-locale}</code>: the localized value of the month (e.g. "09")</li>
-							<li><code>{year-utc}</code>: the year of the save date in UTC format (e.g. "2018")</li>
-							<li><code>{year-locale}</code>: the localized value of the year (e.g. "2018")</li>
-							<li><code>{hours-utc}</code>: the hour of the save date in UTC format (e.g. "20")</li>
-							<li><code>{hours-locale}</code>: the localized value of the hour (e.g. "22")</li>
-							<li><code>{minutes-utc}</code>: the minutes of the save date in UTC format (e.g. "38")</li>
-							<li><code>{minutes-locale}</code>: the localized value of the minutes (e.g. "38")</li>
-							<li><code>{seconds-utc}</code>: the seconds of the save date in UTC format (e.g. "31")</li>
-							<li><code>{seconds-locale}</code>: the localized value of the seconds (e.g. "31")</li>
-							<li><code>{url-href}</code>: the URL of the page (e.g. "http_example.com")</li>
-							<li><code>{url-pathname}</code>: the path name of the URL (e.g. "category_index.html")</li>
-							<li><code>{url-last-segment}</code>: the last part of the pathname (without the extension) or the host if not
-								found (e.g. "index")</li>
-							<li><code>{url-protocol}</code>: the protocol of the URL (e.g. "https")</li>
-							<li><code>{url-host}</code>: the host name + the port of the URL (e.g. "example.com_8080")</li>
-							<li><code>{url-hostname}</code>: the host name of the URL (e.g. "example.com")</li>
-							<li><code>{url-port}</code>: the port of the URL (e.g. "8080")</li>
-							<li><code>{url-username}</code>: the user name of the URL (e.g. "john_doe")</li>
-							<li><code>{url-password}</code>: the password of the URL (e.g. "qwerty123")</li>
-							<li><code>{url-search}</code>: the search string of the URL (e.g. "filter-date=today")</li>
-							<li><code>{url-hash}</code>: the hash of the URL (e.g. "chapter-2")</li>
-							<li><code>{tab-id}</code>: the unique identifier of the tab (e.g. "326")</li>
-							<li><code>{digest-sha-256}</code>: the SHA-256 hash value of the entire page content (e.g.
-								e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855)</li>
-							<li><code>{digest-sha-384}</code>: the SHA-384 hash value of the entire page content</li>
-							<li><code>{digest-sha-512}</code>: the SHA-512 hash value of the entire page content</li>
-						</ul>
+							create sub-folders (with the option "save pages in background" active). You can also use any variables from
+							<a href="template-variables">this list</a> anywhere in the template.</p>
+
 						<p>Note that invalid characters are automatically transformed and collapsed to underscores. File names cannot
 							exceed 192 characters and are truncated if longer. The invalid characters are: [, ~, \, ?, %, *, :, |, ", &lt;,
 							&gt;, and control characters from 0 to 31 in the ASCII table.</p>
@@ -401,6 +374,54 @@
 				</ul>
 			</li>
 
+			<li>
+				<a id="template-variables">Template variables</a>
+				<ul>
+					<li><code>{page-title}</code>: the title of the page</li>
+					<li><code>{page-language}</code>: the language of the page</li>
+					<li><code>{page-description}</code>: the description of the page</li>
+					<li><code>{page-author}</code>: the author of the page</li>
+					<li><code>{page-creator}</code>: the creator of the page</li>
+					<li><code>{page-publisher}</code>: the publisher of the page</li>
+					<li><code>{datetime-iso}</code>: the save date and time in the ISO format (e.g. "2018-09-15T22_38_26_317Z")</li>
+					<li><code>{datetime-utc}</code>: the save date and time in UTC format (e.g. "Sat, 15 Sep 2018 22_38_26 GMT")</li>
+					<li><code>{datetime-locale}</code>: the localized value of the date and time (e.g. "9_16_2018, 12_54_31 AM")</li>
+					<li><code>{date-iso}</code>: the save date in the ISO format (e.g. "2018-09-15")</li>
+					<li><code>{date-locale}</code>: the localized value of the save date (e.g. "16_09_2018")</li>
+					<li><code>{time-iso}</code>: the save time in the ISO format (e.g. "22_38_26_317")</li>
+					<li><code>{time-locale}</code>: the localized value of the save time (e.g. "00_38_26")</li>
+					<li><code>{day-utc}</code>: the day of the save date in UTC format (e.g. "15")</li>
+					<li><code>{day-locale}</code>: the localized value of the day (e.g. "15")</li>
+					<li><code>{month-utc}</code>: the month of the save date in UTC format (e.g. "09")</li>
+					<li><code>{month-locale}</code>: the localized value of the month (e.g. "09")</li>
+					<li><code>{year-utc}</code>: the year of the save date in UTC format (e.g. "2018")</li>
+					<li><code>{year-locale}</code>: the localized value of the year (e.g. "2018")</li>
+					<li><code>{hours-utc}</code>: the hour of the save date in UTC format (e.g. "20")</li>
+					<li><code>{hours-locale}</code>: the localized value of the hour (e.g. "22")</li>
+					<li><code>{minutes-utc}</code>: the minutes of the save date in UTC format (e.g. "38")</li>
+					<li><code>{minutes-locale}</code>: the localized value of the minutes (e.g. "38")</li>
+					<li><code>{seconds-utc}</code>: the seconds of the save date in UTC format (e.g. "31")</li>
+					<li><code>{seconds-locale}</code>: the localized value of the seconds (e.g. "31")</li>
+					<li><code>{url-href}</code>: the URL of the page (e.g. "http_example.com")</li>
+					<li><code>{url-pathname}</code>: the path name of the URL (e.g. "category_index.html")</li>
+					<li><code>{url-last-segment}</code>: the last part of the pathname (without the extension) or the host if not
+						found (e.g. "index")</li>
+					<li><code>{url-protocol}</code>: the protocol of the URL (e.g. "https")</li>
+					<li><code>{url-host}</code>: the host name + the port of the URL (e.g. "example.com_8080")</li>
+					<li><code>{url-hostname}</code>: the host name of the URL (e.g. "example.com")</li>
+					<li><code>{url-port}</code>: the port of the URL (e.g. "8080")</li>
+					<li><code>{url-username}</code>: the user name of the URL (e.g. "john_doe")</li>
+					<li><code>{url-password}</code>: the password of the URL (e.g. "qwerty123")</li>
+					<li><code>{url-search}</code>: the search string of the URL (e.g. "filter-date=today")</li>
+					<li><code>{url-hash}</code>: the hash of the URL (e.g. "chapter-2")</li>
+					<li><code>{tab-id}</code>: the unique identifier of the tab (e.g. "326")</li>
+					<li><code>{digest-sha-256}</code>: the SHA-256 hash value of the entire page content (e.g.
+						e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855)</li>
+					<li><code>{digest-sha-384}</code>: the SHA-384 hash value of the entire page content</li>
+					<li><code>{digest-sha-512}</code>: the SHA-512 hash value of the entire page content</li>
+				</ul>
+			</li>
+
 			<li>
 				<a id="notes">Technical notes</a>
 				<ul>

+ 16 - 2
extension/ui/pages/options.css

@@ -80,6 +80,20 @@ a {
     min-height: 24px;
 }
 
+.option.vertical {
+    flex-direction: column;
+}
+
+.option.vertical label {
+    align-self: flex-start;
+}
+
+.option.vertical input {
+    margin-top: 3px;
+    align-self: flex-end;
+    width: calc(100% - 3px);
+}
+
 .option:last-of-type {
     padding-bottom: 12px;
 }
@@ -102,13 +116,13 @@ a {
 }
 
 #expandAllButton::after {
-    content: '▷';    
+    content: '▷';
 }
 
 #expandAllButton.opened::after {
     content: '▽';
     position: relative;
-    top: 1px;    
+    top: 1px;
 }
 
 #expandAllButton:hover {

+ 8 - 0
extension/ui/pages/options.html

@@ -23,6 +23,14 @@
 			<label for="displayInfobarInput" id="displayInfobarLabel"></label>
 			<input type="checkbox" id="displayInfobarInput">
 		</div>
+		<div class="option vertical">
+			<label for="infobarTemplateInput" id="infobarTemplateLabel"></label>
+			<input type="text" id="infobarTemplateInput">
+		</div>
+		<div class="option">
+			<label for="confirmInfobarInput" id="confirmInfobarLabel"></label>
+			<input type="checkbox" id="confirmInfobarInput">
+		</div>
 	</details>
 	<details>
 		<summary id="filenameLabel"></summary>

+ 83 - 79
lib/single-file/single-file-core.js

@@ -79,7 +79,7 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 	const RESOLVE_URLS_STAGE = 0;
 	const REPLACE_DATA_STAGE = 1;
 	const REPLACE_DOCS_STAGE = 2;
-	const CLEANUP_STAGE = 3;
+	const POST_PROCESS_STAGE = 3;
 	const STAGES = [{
 		sequential: [
 			{ action: "preProcessPage" },
@@ -129,9 +129,7 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 		]
 	}, {
 		sequential: [
-			{ option: "compressHTML", action: "compressHTML" },
-			{ option: "insertSingleFileComment", action: "insertSingleFileComment" },
-			{ action: "cleanup" }
+			{ option: "compressHTML", action: "compressHTML" }
 		]
 	}];
 
@@ -183,7 +181,8 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 			}, this.options);
 			await this.pendingPromises;
 			await this.executeStage(REPLACE_DOCS_STAGE);
-			await this.executeStage(CLEANUP_STAGE);
+			await this.executeStage(POST_PROCESS_STAGE);
+			await this.processor.end();
 		}
 
 		async getPageData() {
@@ -313,19 +312,15 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 
 		async getPageData() {
 			DOM.postProcessDoc(this.doc, this.options);
-			const titleElement = this.doc.querySelector("title");
-			this.options.title = titleElement ? titleElement.textContent.trim() : "";
-			this.options.info = {};
-			const descriptionElement = this.doc.querySelector("meta[name=description]");
-			this.options.info.description = descriptionElement ? descriptionElement.content.trim() : "";
-			this.options.info.lang = this.doc.documentElement.lang;
-			const authorElement = this.doc.querySelector("meta[name=author]");
-			this.options.info.author = authorElement ? authorElement.content.trim() : "";
-			const creatorElement = this.doc.querySelector("meta[name=creator]");
-			this.options.info.creator = creatorElement ? creatorElement.content.trim() : "";
-			const publisherElement = this.doc.querySelector("meta[name=publisher]");
-			this.options.info.publisher = publisherElement ? publisherElement.content.trim() : "";
 			const url = new URL(this.baseURI);
+			if (this.options.insertSingleFileComment) {
+				let infobarContent = (this.options.infobarContent || "").replace(/\\n/g, "\n").replace(/\\t/g, "\t");
+				const commentNode = this.doc.createComment("\n Page saved with SingleFile" +
+					" \n url: " + this.options.url +
+					" \n saved date: " + new Date() +
+					(infobarContent ? " \n info: " + infobarContent : "") + "\n");
+				this.doc.documentElement.insertBefore(commentNode, this.doc.documentElement.firstChild);
+			}
 			let size;
 			if (this.options.displayStats) {
 				size = DOM.getContentSize(this.doc.documentElement.outerHTML);
@@ -336,7 +331,20 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 				this.stats.set("processed", "HTML bytes", contentSize);
 				this.stats.add("discarded", "HTML bytes", size - contentSize);
 			}
-			const filename = await DomProcessorHelper.getFilename(this.options, content);
+			let filename = await DomProcessorHelper.evalTemplate(this.options.filenameTemplate, this.options, content);
+			filename = filename.replace(/[~\\?%*:|"<>\x00-\x1f\x7F]+/g, "_"); // eslint-disable-line no-control-regex
+			filename = filename.replace(/\.\.\//g, "").replace(/^\/+/, "").replace(/\/+/g, "/").replace(/\/$/, "");
+			if (!this.options.backgroundSave) {
+				filename = filename.replace(/\//g, "_");
+			}
+			if (filename.length > 192) {
+				const extensionMatch = filename.match(/(\.[^.]{3,4})$/);
+				const extension = extensionMatch && extensionMatch[0] && extensionMatch[0].length > 1 ? extensionMatch[0] : "";
+				filename = filename.substring(0, 192 - extension.length) + "…" + extension;
+			}
+			if (!filename) {
+				filename = "Unnamed page";
+			}
 			const matchTitle = this.baseURI.match(/([^/]*)\/?(\.html?.*)$/) || this.baseURI.match(/\/\/([^/]*)\/?$/);
 			return {
 				stats: this.stats.data,
@@ -420,7 +428,7 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 			}
 		}
 
-		cleanup() {
+		async end() {
 			const metaCharset = this.doc.head.querySelector("meta[charset]");
 			if (metaCharset) {
 				this.doc.head.insertBefore(metaCharset, this.doc.head.firstChild);
@@ -430,6 +438,19 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 			if (this.doc.head.querySelectorAll("*").length == 1 && metaCharset && this.doc.body.childNodes.length == 0) {
 				this.doc.head.querySelector("meta[charset]").remove();
 			}
+			const titleElement = this.doc.querySelector("title");
+			this.options.title = titleElement ? titleElement.textContent.trim() : "";
+			this.options.info = {};
+			const descriptionElement = this.doc.querySelector("meta[name=description]");
+			this.options.info.description = descriptionElement ? descriptionElement.content.trim() : "";
+			this.options.info.lang = this.doc.documentElement.lang;
+			const authorElement = this.doc.querySelector("meta[name=author]");
+			this.options.info.author = authorElement ? authorElement.content.trim() : "";
+			const creatorElement = this.doc.querySelector("meta[name=creator]");
+			this.options.info.creator = creatorElement ? creatorElement.content.trim() : "";
+			const publisherElement = this.doc.querySelector("meta[name=publisher]");
+			this.options.info.publisher = publisherElement ? publisherElement.content.trim() : "";
+			this.options.infobarContent = await DomProcessorHelper.evalTemplate(this.options.infobarTemplate, this.options, null, true);
 		}
 
 		preProcessPage() {
@@ -588,11 +609,6 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 			});
 		}
 
-		insertSingleFileComment() {
-			const commentNode = this.doc.createComment("\n Archive processed by SingleFile \n url: " + this.options.url + " \n saved date: " + new Date() + " \n");
-			this.doc.documentElement.insertBefore(commentNode, this.doc.documentElement.firstChild);
-		}
-
 		replaceCanvasElements() {
 			if (this.options.canvasData) {
 				this.doc.querySelectorAll("canvas").forEach((canvasElement, indexCanvasElement) => {
@@ -890,64 +906,52 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 	const SINGLE_FILE_VARIABLE_NAME_PREFIX = "--sf-img-";
 
 	class DomProcessorHelper {
-		static async getFilename(options, content) {
-			let filename = options.filenameTemplate;
+		static async evalTemplate(template, options, content, dontReplaceSlash) {
 			const date = new Date();
 			const url = new URL(options.url);
-			filename = await DomUtil.evalTemplateVariable(filename, "page-title", () => options.title || "No title");
-			filename = await DomUtil.evalTemplateVariable(filename, "page-language", () => options.info.lang || "No language");
-			filename = await DomUtil.evalTemplateVariable(filename, "page-description", () => options.info.description || "No description");
-			filename = await DomUtil.evalTemplateVariable(filename, "page-author", () => options.info.author || "No author");
-			filename = await DomUtil.evalTemplateVariable(filename, "page-creator", () => options.info.creator || "No creator");
-			filename = await DomUtil.evalTemplateVariable(filename, "page-publisher", () => options.info.publisher || "No publisher");
-			filename = await DomUtil.evalTemplateVariable(filename, "datetime-iso", () => date.toISOString());
-			filename = await DomUtil.evalTemplateVariable(filename, "date-iso", () => date.toISOString().split("T")[0]);
-			filename = await DomUtil.evalTemplateVariable(filename, "time-iso", () => date.toISOString().split("T")[1].split("Z")[0]);
-			filename = await DomUtil.evalTemplateVariable(filename, "date-locale", () => date.toLocaleDateString());
-			filename = await DomUtil.evalTemplateVariable(filename, "time-locale", () => date.toLocaleTimeString());
-			filename = await DomUtil.evalTemplateVariable(filename, "day-locale", () => String(date.getDate()).padStart(2, "0"));
-			filename = await DomUtil.evalTemplateVariable(filename, "month-locale", () => String(date.getMonth() + 1).padStart(2, "0"));
-			filename = await DomUtil.evalTemplateVariable(filename, "year-locale", () => String(date.getFullYear()));
-			filename = await DomUtil.evalTemplateVariable(filename, "datetime-locale", () => date.toLocaleString());
-			filename = await DomUtil.evalTemplateVariable(filename, "datetime-utc", () => date.toUTCString());
-			filename = await DomUtil.evalTemplateVariable(filename, "day-utc", () => String(date.getUTCDate()).padStart(2, "0"));
-			filename = await DomUtil.evalTemplateVariable(filename, "month-utc", () => String(date.getUTCMonth() + 1).padStart(2, "0"));
-			filename = await DomUtil.evalTemplateVariable(filename, "year-utc", () => String(date.getUTCFullYear()));
-			filename = await DomUtil.evalTemplateVariable(filename, "hours-locale", () => String(date.getHours()).padStart(2, "0"));
-			filename = await DomUtil.evalTemplateVariable(filename, "minutes-locale", () => String(date.getMinutes()).padStart(2, "0"));
-			filename = await DomUtil.evalTemplateVariable(filename, "seconds-locale", () => String(date.getSeconds()).padStart(2, "0"));
-			filename = await DomUtil.evalTemplateVariable(filename, "hours-utc", () => String(date.getUTCHours()).padStart(2, "0"));
-			filename = await DomUtil.evalTemplateVariable(filename, "minutes-utc", () => String(date.getUTCMinutes()).padStart(2, "0"));
-			filename = await DomUtil.evalTemplateVariable(filename, "seconds-utc", () => String(date.getUTCSeconds()).padStart(2, "0"));
-			filename = await DomUtil.evalTemplateVariable(filename, "url-hash", () => url.hash.substring(1));
-			filename = await DomUtil.evalTemplateVariable(filename, "url-host", () => url.host.replace(/\/$/, ""));
-			filename = await DomUtil.evalTemplateVariable(filename, "url-hostname", () => url.hostname.replace(/\/$/, ""));
-			filename = await DomUtil.evalTemplateVariable(filename, "url-href", () => url.href);
-			filename = await DomUtil.evalTemplateVariable(filename, "url-password", () => url.password);
-			filename = await DomUtil.evalTemplateVariable(filename, "url-pathname", () => url.pathname.replace(/^\//, "").replace(/\/$/, ""), true);
-			filename = await DomUtil.evalTemplateVariable(filename, "url-port", () => url.port);
-			filename = await DomUtil.evalTemplateVariable(filename, "url-protocol", () => url.protocol);
-			filename = await DomUtil.evalTemplateVariable(filename, "url-search", () => url.search.substring(1));
-			filename = await DomUtil.evalTemplateVariable(filename, "url-username", () => url.username);
-			filename = await DomUtil.evalTemplateVariable(filename, "tab-id", () => String(options.tabId || "No tab id"));
-			filename = await DomUtil.evalTemplateVariable(filename, "url-last-segment", () => DomUtil.getLastSegment(url));
-			filename = await DomUtil.evalTemplateVariable(filename, "digest-sha-256", async () => DOM.digest("SHA-256", content));
-			filename = await DomUtil.evalTemplateVariable(filename, "digest-sha-384", async () => DOM.digest("SHA-384", content));
-			filename = await DomUtil.evalTemplateVariable(filename, "digest-sha-512", async () => DOM.digest("SHA-512", content));
-			filename = filename.replace(/[~\\?%*:|"<>\x00-\x1f\x7F]+/g, "_"); // eslint-disable-line no-control-regex
-			filename = filename.replace(/\.\.\//g, "").replace(/^\/+/, "").replace(/\/+/g, "/").replace(/\/$/, "");
-			if (!options.backgroundSave) {
-				filename = filename.replace(/\//g, "_");
-			}
-			if (filename.length > 192) {
-				const extensionMatch = filename.match(/(\.[^.]{3,4})$/);
-				const extension = extensionMatch && extensionMatch[0] && extensionMatch[0].length > 1 ? extensionMatch[0] : "";
-				filename = filename.substring(0, 192 - extension.length) + "…" + extension;
-			}
-			if (!filename) {
-				filename = "Unnamed page";
+			template = await DomUtil.evalTemplateVariable(template, "page-title", () => options.title || "No title", dontReplaceSlash);
+			template = await DomUtil.evalTemplateVariable(template, "page-language", () => options.info.lang || "No language", dontReplaceSlash);
+			template = await DomUtil.evalTemplateVariable(template, "page-description", () => options.info.description || "No description", dontReplaceSlash);
+			template = await DomUtil.evalTemplateVariable(template, "page-author", () => options.info.author || "No author", dontReplaceSlash);
+			template = await DomUtil.evalTemplateVariable(template, "page-creator", () => options.info.creator || "No creator", dontReplaceSlash);
+			template = await DomUtil.evalTemplateVariable(template, "page-publisher", () => options.info.publisher || "No publisher", dontReplaceSlash);
+			template = await DomUtil.evalTemplateVariable(template, "datetime-iso", () => date.toISOString(), dontReplaceSlash);
+			template = await DomUtil.evalTemplateVariable(template, "date-iso", () => date.toISOString().split("T")[0], dontReplaceSlash);
+			template = await DomUtil.evalTemplateVariable(template, "time-iso", () => date.toISOString().split("T")[1].split("Z")[0], dontReplaceSlash);
+			template = await DomUtil.evalTemplateVariable(template, "date-locale", () => date.toLocaleDateString(), dontReplaceSlash);
+			template = await DomUtil.evalTemplateVariable(template, "time-locale", () => date.toLocaleTimeString(), dontReplaceSlash);
+			template = await DomUtil.evalTemplateVariable(template, "day-locale", () => String(date.getDate()).padStart(2, "0"), dontReplaceSlash);
+			template = await DomUtil.evalTemplateVariable(template, "month-locale", () => String(date.getMonth() + 1).padStart(2, "0"), dontReplaceSlash);
+			template = await DomUtil.evalTemplateVariable(template, "year-locale", () => String(date.getFullYear()), dontReplaceSlash);
+			template = await DomUtil.evalTemplateVariable(template, "datetime-locale", () => date.toLocaleString(), dontReplaceSlash);
+			template = await DomUtil.evalTemplateVariable(template, "datetime-utc", () => date.toUTCString(), dontReplaceSlash);
+			template = await DomUtil.evalTemplateVariable(template, "day-utc", () => String(date.getUTCDate()).padStart(2, "0"), dontReplaceSlash);
+			template = await DomUtil.evalTemplateVariable(template, "month-utc", () => String(date.getUTCMonth() + 1).padStart(2, "0"), dontReplaceSlash);
+			template = await DomUtil.evalTemplateVariable(template, "year-utc", () => String(date.getUTCFullYear()), dontReplaceSlash);
+			template = await DomUtil.evalTemplateVariable(template, "hours-locale", () => String(date.getHours()).padStart(2, "0"), dontReplaceSlash);
+			template = await DomUtil.evalTemplateVariable(template, "minutes-locale", () => String(date.getMinutes()).padStart(2, "0"), dontReplaceSlash);
+			template = await DomUtil.evalTemplateVariable(template, "seconds-locale", () => String(date.getSeconds()).padStart(2, "0"), dontReplaceSlash);
+			template = await DomUtil.evalTemplateVariable(template, "hours-utc", () => String(date.getUTCHours()).padStart(2, "0"), dontReplaceSlash);
+			template = await DomUtil.evalTemplateVariable(template, "minutes-utc", () => String(date.getUTCMinutes()).padStart(2, "0"), dontReplaceSlash);
+			template = await DomUtil.evalTemplateVariable(template, "seconds-utc", () => String(date.getUTCSeconds()).padStart(2, "0"), dontReplaceSlash);
+			template = await DomUtil.evalTemplateVariable(template, "url-hash", () => url.hash.substring(1), dontReplaceSlash);
+			template = await DomUtil.evalTemplateVariable(template, "url-host", () => url.host.replace(/\/$/, ""), dontReplaceSlash);
+			template = await DomUtil.evalTemplateVariable(template, "url-hostname", () => url.hostname.replace(/\/$/, ""), dontReplaceSlash);
+			template = await DomUtil.evalTemplateVariable(template, "url-href", () => url.href, dontReplaceSlash);
+			template = await DomUtil.evalTemplateVariable(template, "url-password", () => url.password, dontReplaceSlash);
+			template = await DomUtil.evalTemplateVariable(template, "url-pathname", () => url.pathname.replace(/^\//, "").replace(/\/$/, ""), dontReplaceSlash === undefined ? true : dontReplaceSlash);
+			template = await DomUtil.evalTemplateVariable(template, "url-port", () => url.port, dontReplaceSlash);
+			template = await DomUtil.evalTemplateVariable(template, "url-protocol", () => url.protocol, dontReplaceSlash);
+			template = await DomUtil.evalTemplateVariable(template, "url-search", () => url.search.substring(1), dontReplaceSlash);
+			template = await DomUtil.evalTemplateVariable(template, "url-username", () => url.username, dontReplaceSlash);
+			template = await DomUtil.evalTemplateVariable(template, "tab-id", () => String(options.tabId || "No tab id"), dontReplaceSlash);
+			template = await DomUtil.evalTemplateVariable(template, "url-last-segment", () => DomUtil.getLastSegment(url), dontReplaceSlash);
+			if (content) {
+				template = await DomUtil.evalTemplateVariable(template, "digest-sha-256", async () => DOM.digest("SHA-256", content), dontReplaceSlash);
+				template = await DomUtil.evalTemplateVariable(template, "digest-sha-384", async () => DOM.digest("SHA-384", content), dontReplaceSlash);
+				template = await DomUtil.evalTemplateVariable(template, "digest-sha-512", async () => DOM.digest("SHA-512", content), dontReplaceSlash);
 			}
-			return filename;
+			return template;
 		}
 
 		static setBackgroundImage(element, url, style) {