浏览代码

add option `Destination > share page` option
(only on Safari today, fix #1381)

Gildas 2 年之前
父节点
当前提交
9fab279990

+ 8 - 0
_locales/de/messages.json

@@ -615,6 +615,10 @@
 		"message": "Speichern im Dateisystem",
 		"description": "Options page label: 'save to filesystem'"
 	},
+	"optionSharePage": {
+		"message": "Seite teilen",
+		"description": "Options page label: 'share page'"
+	},
 	"optionAddProof": {
 		"message": "Existenzberechtigung hinzufügen",
 		"description": "Options page label: 'add proof of existence'"
@@ -763,6 +767,10 @@
 		"message": "Bild öffnen...",
 		"description": "Top panel button 'Open image...' when embedding an image"
 	},
+	"topPanelSharePageButton": {
+		"message": "Seite teilen...",
+		"description": "Top panel button 'Share page...' when sharing a page"
+	},
 	"profileDefaultSettings": {
 		"message": "Standardeinstellungen",
 		"description": "Label 'Default settings' of the default settings in the options page"

+ 8 - 0
_locales/en/messages.json

@@ -615,6 +615,10 @@
 		"message": "save to filesystem",
 		"description": "Options page label: 'save to filesystem'"
 	},
+	"optionSharePage": {
+		"message": "share page",
+		"description": "Options page label: 'share page'"
+	},
 	"optionAddProof": {
 		"message": "add proof of existence",
 		"description": "Options page label: 'add proof of existence'"
@@ -763,6 +767,10 @@
 		"message": "Open image...",
 		"description": "Top panel button 'Open image...' when embedding an image"
 	},
+	"topPanelSharePageButton": {
+		"message": "Share page...",
+		"description": "Top panel button 'Share page...' when sharing a page"
+	},
 	"profileDefaultSettings": {
 		"message": "Default settings",
 		"description": "Label 'Default settings' of the default settings in the options page"

+ 8 - 0
_locales/es/messages.json

@@ -615,6 +615,10 @@
 		"message": "guardar en el sistema de archivos",
 		"description": "Options page label: 'save to filesystem'"
 	},
+	"optionSharePage": {
+		"message": "compartir página",
+		"description": "Options page label: 'share page'"
+	},
 	"optionAddProof": {
 		"message": "añadir prueba de existencia",
 		"description": "Options page label: 'add proof of existence'"
@@ -763,6 +767,10 @@
 		"message": "Abrir imagen...",
 		"description": "Top panel button 'Open image...' when embedding an image"
 	},
+	"topPanelSharePageButton": {
+		"message": "Compartir página...",
+		"description": "Top panel button 'Share page...' when sharing a page"
+	},
 	"profileDefaultSettings": {
 		"message": "Configuración predeterminada",
 		"description": "Label 'Default settings' of the default settings in the options page"

+ 8 - 0
_locales/fr/messages.json

@@ -615,6 +615,10 @@
 		"message": "enregistrer dans le système de fichiers",
 		"description": "Options page label: 'save to filesystem'"
 	},
+	"optionSharePage": {
+		"message": "partager la page",
+		"description": "Options page label: 'share page'"
+	},
 	"optionAddProof": {
 		"message": "ajouter une preuve d'existence",
 		"description": "Options page label: 'add proof of existence'"
@@ -763,6 +767,10 @@
 		"message": "Ouvrir image...",
 		"description": "Top panel button 'Open image...' when embedding an image"
 	},
+	"topPanelSharePageButton": {
+		"message": "Partager la page...",
+		"description": "Top panel button 'Share page...' when sharing a page"
+	},
 	"profileDefaultSettings": {
 		"message": "Configuration par défaut",
 		"description": "Label 'Default settings' of the default settings in the options page"

+ 8 - 0
_locales/it/messages.json

@@ -615,6 +615,10 @@
 		"message": "salva nel filesystem",
 		"description": "Options page label: 'save to filesystem'"
 	},
+	"optionSharePage": {
+		"message": "condividi pagina",
+		"description": "Options page label: 'share page'"
+	},
 	"optionAddProof": {
 		"message": "aggiungi una prova di esistenza",
 		"description": "Options page label: 'add proof of existence'"
@@ -763,6 +767,10 @@
 		"message": "Apri immagine...",
 		"description": "Top panel button 'Open image...' when embedding an image"
 	},
+	"topPanelSharePageButton": {
+		"message": "Condividi pagina...",
+		"description": "Top panel button 'Share page...' when sharing a page"
+	},
 	"profileDefaultSettings": {
 		"message": "Impostazioni predefinite",
 		"description": "Label 'Default settings' of the default settings in the options page"

+ 8 - 0
_locales/ja/messages.json

@@ -615,6 +615,10 @@
 		"message": "ファイルシステムに保存する",
 		"description": "Options page label: 'save to filesystem'"
 	},
+	"optionSharePage": {
+		"message": "ページを共有する",
+		"description": "Options page label: 'share page'"
+	},
 	"optionAddProof": {
 		"message": "ブロックチェーン証明を追加する",
 		"description": "Options page label: 'add proof of existence'"
@@ -763,6 +767,10 @@
 		"message": "画像を開く...",
 		"description": "Top panel button 'Open image...' when embedding an image"
 	},
+	"topPanelSharePageButton": {
+		"message": "ページを共有...",
+		"description": "Top panel button 'Share page...' when sharing a page"
+	},
 	"profileDefaultSettings": {
 		"message": "デフォルトの設定",
 		"description": "Label 'Default settings' of the default settings in the options page"

+ 8 - 0
_locales/pl/messages.json

@@ -615,6 +615,10 @@
 		"message": "zapisuj do systemu plików",
 		"description": "Options page label: 'save to filesystem'"
 	},
+	"optionSharePage": {
+		"message": "share page",
+		"description": "Options page label: 'share page'"
+	},
 	"optionAddProof": {
 		"message": "dodawaj dowód istnienia",
 		"description": "Options page label: 'add proof of existence'"
@@ -763,6 +767,10 @@
 		"message": "Otwórz obraz...",
 		"description": "Top panel button 'Open image...' when embedding an image"
 	},
+	"topPanelSharePageButton": {
+		"message": "Share page...",
+		"description": "Top panel button 'Share page...' when sharing a page"
+	},
 	"profileDefaultSettings": {
 		"message": "Ustawienia domyślne",
 		"description": "Label 'Default settings' of the default settings in the options page"

+ 8 - 0
_locales/pt_PT/messages.json

@@ -615,6 +615,10 @@
 		"message": "guardar no sistema de ficheiros",
 		"description": "Options page label: 'save to filesystem'"
 	},
+	"optionSharePage": {
+		"message": "partilhar página",
+		"description": "Options page label: 'share page'"
+	},
 	"optionAddProof": {
 		"message": "adicionar prova de existência",
 		"description": "Options page label: 'add proof of existence'"
@@ -763,6 +767,10 @@
 		"message": "Abrir imagem...",
 		"description": "Top panel button 'Open image...' when embedding an image"
 	},
+	"topPanelSharePageButton": {
+		"message": "Partilhar página...",
+		"description": "Top panel button 'Share page...' when sharing a page"
+	},
 	"profileDefaultSettings": {
 		"message": "Definições predefinidas",
 		"description": "Label 'Default settings' of the default settings in the options page"

+ 8 - 0
_locales/pt_br/messages.json

@@ -615,6 +615,10 @@
 		"message": "salvar no sistema de arquivos",
 		"description": "Options page label: 'save to filesystem'"
 	},
+	"optionSharePage": {
+		"message": "partilhar página",
+		"description": "Options page label: 'share page'"
+	},
 	"optionAddProof": {
 		"message": "adicionar prova de existência",
 		"description": "Options page label: 'add proof of existence'"
@@ -763,6 +767,10 @@
 		"message": "Abrir imagem...",
 		"description": "Top panel button 'Open image...' when embedding an image"
 	},
+	"topPanelSharePageButton": {
+		"message": "Partilhar página...",
+		"description": "Top panel button 'Share page...' when sharing a page"
+	},
 	"profileDefaultSettings": {
 		"message": "Configurações padrão",
 		"description": "Label 'Default settings' of the default settings in the options page"

+ 8 - 0
_locales/ru/messages.json

@@ -615,6 +615,10 @@
 		"message": "сохранить как файл",
 		"description": "Options page label: 'save to filesystem'"
 	},
+	"optionSharePage": {
+		"message": "share page",
+		"description": "Options page label: 'share page'"
+	},
 	"optionAddProof": {
 		"message": "добавить доказательство существования",
 		"description": "Options page label: 'add proof of existence'"
@@ -763,6 +767,10 @@
 		"message": "Открыть изображение...",
 		"description": "Top panel button 'Open image...' when embedding an image"
 	},
+	"topPanelSharePageButton": {
+		"message": "Share page...",
+		"description": "Top panel button 'Share page...' when sharing a page"
+	},
 	"profileDefaultSettings": {
 		"message": "Настройки по умолчанию",
 		"description": "Label 'Default settings' of the default settings in the options page"

+ 8 - 0
_locales/tr/messages.json

@@ -615,6 +615,10 @@
 		"message": "dosya sistemine kaydet",
 		"description": "Options page label: 'save to filesystem'"
 	},
+	"optionSharePage": {
+		"message": "sayfayı paylaş",
+		"description": "Options page label: 'share page'"
+	},
 	"optionAddProof": {
 		"message": "varlığın kanıtını ekle",
 		"description": "Options page label: 'add proof of existence'"
@@ -763,6 +767,10 @@
 		"message": "Resmi aç...",
 		"description": "Top panel button 'Open image...' when embedding an image"
 	},
+	"topPanelSharePageButton": {
+		"message": "Sayfayı paylaş...",
+		"description": "Top panel button 'Share page...' when sharing a page"
+	},
 	"profileDefaultSettings": {
 		"message": "Varsayılan ayarlar",
 		"description": "Label 'Default settings' of the default settings in the options page"

+ 8 - 0
_locales/uk/messages.json

@@ -615,6 +615,10 @@
 		"message": "збереження у файлову систему",
 		"description": "Options page label: 'save to filesystem'"
 	},
+	"optionSharePage": {
+		"message": "поділитися сторінкою",
+		"description": "Options page label: 'share page'"
+	},
 	"optionAddProof": {
 		"message": "додати докази існування",
 		"description": "Options page label: 'add proof of existence'"
@@ -763,6 +767,10 @@
 		"message": "Відкрити зображення..",
 		"description": "Top panel button 'Open image...' when embedding an image"
 	},
+	"topPanelSharePageButton": {
+		"message": "Поділитися сторінкою...",
+		"description": "Top panel button 'Share page...' when sharing a page"
+	},
 	"profileDefaultSettings": {
 		"message": "Типові налаштування",
 		"description": "Label 'Default settings' of the default settings in the options page"

+ 8 - 0
_locales/zh_CN/messages.json

@@ -615,6 +615,10 @@
 		"message": "保存到文件系统",
 		"description": "Options page label: 'save to filesystem'"
 	},
+	"optionSharePage": {
+		"message": "分享页面",
+		"description": "Options page label: 'share page'"
+	},
 	"optionAddProof": {
 		"message": "添加证明文件存在的指纹",
 		"description": "Options page label: 'add proof of existence'"
@@ -763,6 +767,10 @@
 		"message": "打开图片...",
 		"description": "Top panel button 'Open image...' when embedding an image"
 	},
+	"topPanelSharePageButton": {
+		"message": "分享页面...",
+		"description": "Top panel button 'Share page...' when sharing a page"
+	},
 	"profileDefaultSettings": {
 		"message": "默认设置",
 		"description": "Label 'Default settings' of the default settings in the options page"

+ 8 - 0
_locales/zh_TW/messages.json

@@ -615,6 +615,10 @@
 		"message": "保存到文件系統",
 		"description": "Options page label: 'save to filesystem'"
 	},
+	"optionSharePage": {
+		"message": "分享頁面",
+		"description": "Options page label: 'share page'"
+	},
 	"optionAddProof": {
 		"message": "添加證明文件存在的指紋",
 		"description": "Options page label: 'add proof of existence'"
@@ -763,6 +767,10 @@
 		"message": "開啟圖片...",
 		"description": "Top panel button 'Open image...' when embedding an image"
 	},
+	"topPanelSharePageButton": {
+		"message": "分享頁面...",
+		"description": "Top panel button 'Share page...' when sharing a page"
+	},
 	"profileDefaultSettings": {
 		"message": "默認設置",
 		"description": "Label 'Default settings' of the default settings in the options page"

+ 6 - 2
src/core/bg/config.js

@@ -21,7 +21,7 @@
  *   Source.
  */
 
-/* global browser, navigator, URL, Blob */
+/* global browser, navigator, URL, Blob, File */
 
 import { download } from "./download-util.js";
 import * as tabsData from "./tabs-data.js";
@@ -44,6 +44,7 @@ const IDENTITY_API_SUPPORTED = IS_NOT_SAFARI;
 const CLIPBOARD_API_SUPPORTED = IS_NOT_SAFARI;
 const NATIVE_API_API_SUPPORTED = IS_NOT_SAFARI;
 const WEB_BLOCKING_API_SUPPORTED = IS_NOT_SAFARI;
+const SHARE_API_SUPPORTED = navigator.canShare && navigator.canShare({ files: [new File([new Blob([""], { type: "text/html" })], "test.html")] });
 
 const DEFAULT_CONFIG = {
 	removeHiddenElements: true,
@@ -113,6 +114,7 @@ const DEFAULT_CONFIG = {
 	githubRepository: "SingleFile-Archives",
 	githubBranch: "main",
 	saveWithCompanion: false,
+	sharePage: false,
 	forceWebAuthFlow: false,
 	resolveFragmentIdentifierURLs: false,
 	userScriptEnabled: false,
@@ -216,6 +218,7 @@ export {
 	CLIPBOARD_API_SUPPORTED,
 	NATIVE_API_API_SUPPORTED,
 	WEB_BLOCKING_API_SUPPORTED,
+	SHARE_API_SUPPORTED,
 	getConfig as get,
 	getRule,
 	getOptions,
@@ -364,7 +367,8 @@ async function onMessage(message) {
 			IDENTITY_API_SUPPORTED,
 			CLIPBOARD_API_SUPPORTED,
 			NATIVE_API_API_SUPPORTED,
-			WEB_BLOCKING_API_SUPPORTED
+			WEB_BLOCKING_API_SUPPORTED,
+			SHARE_API_SUPPORTED
 		};
 	}
 	if (message.method.endsWith(".getRules")) {

+ 22 - 20
src/core/bg/downloads.js

@@ -250,7 +250,7 @@ async function downloadCompressedContent(message, tab) {
 	const tabId = tab.id;
 	try {
 		let skipped;
-		if (message.backgroundSave && !message.saveToGDrive && !message.saveToDropbox && !message.saveWithWebDAV && !message.saveToGitHub) {
+		if (message.backgroundSave && !message.saveToGDrive && !message.saveToDropbox && !message.saveWithWebDAV && !message.saveToGitHub && !message.sharePage) {
 			const testSkip = await testSkipSave(message.filename, message);
 			message.filenameConflictAction = testSkip.filenameConflictAction;
 			skipped = testSkip.skipped;
@@ -288,8 +288,14 @@ async function downloadCompressedContent(message, tab) {
 					insertMetaCSP: message.insertMetaCSP,
 					embeddedImage: message.embeddedImage
 				});
-			} else if (message.foregroundSave) {
-				await downloadPageForeground(message.taskId, message.filename, blob, tabId, message.foregroundSave);
+			} else if (message.foregroundSave || !message.backgroundSave || message.sharePage) {
+				const response = await downloadPageForeground(message.taskId, message.filename, blob, tabId, {
+					foregroundSave: true,
+					sharePage: message.sharePage
+				});
+				if (response.error) {
+					throw new Error(response.error);
+				}
 			} else if (message.saveWithWebDAV) {
 				response = await saveWithWebDAV(message.taskId, encodeSharpCharacter(message.filename), blob, message.webDAVURL, message.webDAVUser, message.webDAVPassword, { filenameConflictAction: message.filenameConflictAction, prompt });
 			} else if (message.saveToGDrive) {
@@ -313,20 +319,16 @@ async function downloadCompressedContent(message, tab) {
 				});
 				await response.pushPromise;
 			} else {
-				if (message.backgroundSave) {
-					message.url = URL.createObjectURL(blob);
-					response = await downloadPage(message, {
-						confirmFilename: message.confirmFilename,
-						incognito: tab.incognito,
-						filenameConflictAction: message.filenameConflictAction,
-						filenameReplacementCharacter: message.filenameReplacementCharacter,
-						bookmarkId: message.bookmarkId,
-						replaceBookmarkURL: message.replaceBookmarkURL,
-						includeInfobar: message.includeInfobar
-					});
-				} else {
-					await downloadPageForeground(message.taskId, message.filename, blob, tabId);
-				}
+				message.url = URL.createObjectURL(blob);
+				response = await downloadPage(message, {
+					confirmFilename: message.confirmFilename,
+					incognito: tab.incognito,
+					filenameConflictAction: message.filenameConflictAction,
+					filenameReplacementCharacter: message.filenameReplacementCharacter,
+					bookmarkId: message.bookmarkId,
+					replaceBookmarkURL: message.replaceBookmarkURL,
+					includeInfobar: message.includeInfobar
+				});
 			}
 			if (message.bookmarkId && message.replaceBookmarkURL && response && response.url) {
 				await bookmarks.update(message.bookmarkId, { url: response.url });
@@ -544,13 +546,13 @@ function saveToClipboard(pageData) {
 	}
 }
 
-async function downloadPageForeground(taskId, filename, content, tabId, foregroundSave) {
-	const serializer = yabson.getSerializer({ filename, taskId, foregroundSave, content: await content.arrayBuffer() });
+async function downloadPageForeground(taskId, filename, content, tabId, { foregroundSave, sharePage }) {
+	const serializer = yabson.getSerializer({ filename, taskId, foregroundSave, sharePage, content: await content.arrayBuffer() });
 	for await (const data of serializer) {
 		await browser.tabs.sendMessage(tabId, {
 			method: "content.download",
 			data: Array.from(data)
 		});
 	}
-	await browser.tabs.sendMessage(tabId, { method: "content.download" });
+	return browser.tabs.sendMessage(tabId, { method: "content.download" });
 }

+ 55 - 13
src/core/common/download.js

@@ -21,14 +21,33 @@
  *   Source.
  */
 
-/* global browser, document, URL, Blob, MouseEvent, setTimeout, open */
+/* global browser, document, URL, Blob, MouseEvent, setTimeout, open, navigator, File */
 
 import * as yabson from "./../../lib/yabson/yabson.js";
+import { getSharePageBar, setLabels } from "./../../ui/common/common-content-ui.js";
 
 const MAX_CONTENT_SIZE = 16 * (1024 * 1024);
 
+let EMBEDDED_IMAGE_BUTTON_MESSAGE, SHARE_PAGE_BUTTON_MESSAGE, ERROR_TITLE_MESSAGE;
+
+try {
+	EMBEDDED_IMAGE_BUTTON_MESSAGE = browser.i18n.getMessage("topPanelEmbeddedImageButton");
+	SHARE_PAGE_BUTTON_MESSAGE = browser.i18n.getMessage("topPanelSharePageButton");
+	ERROR_TITLE_MESSAGE = browser.i18n.getMessage("topPanelError");
+} catch (error) {
+	// ignored
+}
+
+let sharePageBar;
+setLabels({
+	EMBEDDED_IMAGE_BUTTON_MESSAGE,
+	SHARE_PAGE_BUTTON_MESSAGE,
+	ERROR_TITLE_MESSAGE
+});
+
 export {
-	downloadPage
+	downloadPage,
+	downloadPageForeground
 };
 
 async function downloadPage(pageData, options) {
@@ -78,7 +97,8 @@ async function downloadPage(pageData, options) {
 		insertMetaCSP: options.insertMetaCSP,
 		password: options.password,
 		compressContent: options.compressContent,
-		foregroundSave: options.foregroundSave
+		foregroundSave: options.foregroundSave,
+		sharePage: options.sharePage
 	};
 	if (options.compressContent) {
 		const blob = new Blob([await yabson.serialize(pageData)], { type: "application/octet-stream" });
@@ -110,7 +130,7 @@ async function downloadPage(pageData, options) {
 			await browser.runtime.sendMessage({ method: "downloads.end", taskId: options.taskId });
 		}
 	} else {
-		if (options.backgroundSave || options.openEditor || options.saveToGDrive || options.saveToGitHub || options.saveWithCompanion || options.saveWithWebDAV || options.saveToDropbox) {
+		if ((options.backgroundSave && !options.sharePage) || options.openEditor || options.saveToGDrive || options.saveToGitHub || options.saveWithCompanion || options.saveWithWebDAV || options.saveToDropbox) {
 			const blobURL = URL.createObjectURL(new Blob([pageData.content], { type: "text/html" }));
 			message.blobURL = blobURL;
 			const result = await browser.runtime.sendMessage(message);
@@ -132,7 +152,7 @@ async function downloadPage(pageData, options) {
 			if (options.saveToClipboard) {
 				saveToClipboard(pageData);
 			} else {
-				await downloadPageForeground(pageData);
+				await downloadPageForeground(pageData, options);
 			}
 			if (options.openSavedPage) {
 				open(URL.createObjectURL(new Blob([pageData.content], { type: "text/html" })));
@@ -143,15 +163,37 @@ async function downloadPage(pageData, options) {
 	}
 }
 
-async function downloadPageForeground(pageData) {
-	if (pageData.filename && pageData.filename.length) {
-		const link = document.createElement("a");
-		link.download = pageData.filename;
-		link.href = URL.createObjectURL(new Blob([pageData.content], { type: "text/html" }));
-		link.dispatchEvent(new MouseEvent("click"));
-		setTimeout(() => URL.revokeObjectURL(link.href), 1000);
+async function downloadPageForeground(pageData, options) {
+	if (options.sharePage && navigator.share) {
+		await sharePage(pageData);
+	} else {
+		if (pageData.filename && pageData.filename.length) {
+			const link = document.createElement("a");
+			link.download = pageData.filename;
+			link.href = URL.createObjectURL(new Blob([pageData.content], { type: "text/html" }));
+			link.dispatchEvent(new MouseEvent("click"));
+			return new Promise(resolve => setTimeout(() => { URL.revokeObjectURL(link.href); resolve(); }, 1000));
+		}
+	}
+}
+
+async function sharePage(pageData) {
+	sharePageBar = getSharePageBar();
+	const cancelled = await sharePageBar.display();
+	if (!cancelled) {
+		const data = { files: [new File([pageData.content], pageData.filename)] };
+		try {
+			await navigator.share(data);
+			sharePageBar.hide();
+		} catch (error) {
+			sharePageBar.hide();
+			if (error.name === "AbortError") {
+				await sharePage(pageData);
+			} else {
+				throw error;
+			}
+		}
 	}
-	return new Promise(resolve => setTimeout(resolve, 1));
 }
 
 function saveToClipboard(page) {

+ 23 - 8
src/core/content/content.js

@@ -21,21 +21,30 @@
  *   Source.
  */
 
-/* global browser, document, globalThis, location, URL, Blob, MouseEvent, setTimeout */
+/* global browser, document, globalThis, location, setTimeout */
 
 import * as download from "./../common/download.js";
 import { fetch, frameFetch } from "./../../lib/single-file/fetch/content/content-fetch.js";
 import * as ui from "./../../ui/content/content-ui.js";
-import { onError, getOpenFileBar, openFile } from "./../../ui/common/common-content-ui.js";
+import { onError, getOpenFileBar, openFile, setLabels } from "./../../ui/common/common-content-ui.js";
 import * as yabson from "./../../lib/yabson/yabson.js";
 
 const singlefile = globalThis.singlefile;
 const bootstrap = globalThis.singlefileBootstrap;
 
 const MOZ_EXTENSION_PROTOCOL = "moz-extension:";
+const EMBEDDED_IMAGE_BUTTON_MESSAGE = browser.i18n.getMessage("topPanelEmbeddedImageButton");
+const SHARE_PAGE_BUTTON_MESSAGE = browser.i18n.getMessage("topPanelSharePageButton"); browser.i18n.getMessage("topPanelSharePageButton");
+const ERROR_TITLE_MESSAGE = browser.i18n.getMessage("topPanelError");
 
 let processor, processing, downloadParser, openFileInfobar;
 
+setLabels({
+	EMBEDDED_IMAGE_BUTTON_MESSAGE,
+	SHARE_PAGE_BUTTON_MESSAGE,
+	ERROR_TITLE_MESSAGE
+});
+
 if (!bootstrap || !bootstrap.initializedSingleFile) {
 	singlefile.init({ fetch, frameFetch });
 	browser.runtime.onMessage.addListener(message => {
@@ -83,12 +92,18 @@ async function onMessage(message) {
 			const result = await downloadParser.next(message.data);
 			if (result.done) {
 				downloadParser = null;
-				const link = document.createElement("a");
-				link.download = result.value.filename;
-				link.href = URL.createObjectURL(new Blob([result.value.content]), "text/html");
-				link.dispatchEvent(new MouseEvent("click"));
-				URL.revokeObjectURL(link.href);
-				await browser.runtime.sendMessage({ method: "downloads.end", taskId: result.value.taskId });
+				try {
+					await download.downloadPageForeground(result.value, {
+						foregroundSave: result.value.foregroundSave,
+						sharePage: result.value.sharePage,
+					});
+				} catch (error) {
+					return {
+						error: error.toString()
+					};
+				} finally {
+					await browser.runtime.sendMessage({ method: "downloads.end", taskId: result.value.taskId });
+				}
 			}
 			return {};
 		}

+ 18 - 3
src/ui/bg/ui-editor.js

@@ -28,6 +28,10 @@ import { onError } from "./../common/common-content-ui.js";
 import * as zip from "./../../../lib/single-file-zip.js";
 import * as yabson from "./../../lib/yabson/yabson.js";
 
+const EMBEDDED_IMAGE_BUTTON_MESSAGE = browser.i18n.getMessage("topPanelEmbeddedImageButton");
+const SHARE_PAGE_BUTTON_MESSAGE = browser.i18n.getMessage("topPanelSharePageButton");
+const ERROR_TITLE_MESSAGE = browser.i18n.getMessage("topPanelError");
+
 const FOREGROUND_SAVE = /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent) && !/Vivaldi/.test(navigator.userAgent) && !/OPR/.test(navigator.userAgent);
 const SHADOWROOT_ATTRIBUTE_NAME = "shadowrootmode";
 const INFOBAR_TAGNAME = "single-file-infobar";
@@ -348,6 +352,10 @@ addEventListener("message", event => {
 			}
 		}
 	}
+	if (message.method == "onError") {
+		browser.runtime.sendMessage({ method: "ui.processError", error: message.error });
+		onError(message.error);
+	}
 	if (message.method == "savePage") {
 		savePage();
 	}
@@ -416,11 +424,12 @@ async function downloadContent(message) {
 	const result = await downloadParser.next(message.data);
 	if (result.done) {
 		downloadParser = null;
-		if (result.value.foregroundSave) {
+		if (result.value.foregroundSave || result.value.sharePage) {
 			editorElement.contentWindow.postMessage(JSON.stringify({
 				method: "download",
 				filename: result.value.filename,
-				content: Array.from(new Uint8Array(result.value.content))
+				content: Array.from(new Uint8Array(result.value.content)),
+				sharePage: result.value.sharePage
 			}), "*");
 		} else {
 			const link = document.createElement("a");
@@ -519,7 +528,13 @@ function savePage() {
 		backgroundSave: tabData.options.backgroundSave,
 		updatedResources,
 		filename: tabData.filename,
-		foregroundSave: FOREGROUND_SAVE
+		foregroundSave: FOREGROUND_SAVE,
+		sharePage: tabData.options.sharePage,
+		labels: {
+			EMBEDDED_IMAGE_BUTTON_MESSAGE,
+			SHARE_PAGE_BUTTON_MESSAGE,
+			ERROR_TITLE_MESSAGE
+		}
 	}), "*");
 }
 

+ 5 - 0
src/ui/bg/ui-help.js

@@ -32,6 +32,7 @@ let BACKGROUND_SAVE_SUPPORTED,
 	CLIPBOARD_API_SUPPORTED,
 	NATIVE_API_API_SUPPORTED,
 	WEB_BLOCKING_API_SUPPORTED,
+	SHARE_API_SUPPORTED,
 	SELECTABLE_TABS_SUPPORTED;
 browser.runtime.sendMessage({ method: "config.getConstants" }).then(data => {
 	({
@@ -44,6 +45,7 @@ browser.runtime.sendMessage({ method: "config.getConstants" }).then(data => {
 		CLIPBOARD_API_SUPPORTED,
 		NATIVE_API_API_SUPPORTED,
 		WEB_BLOCKING_API_SUPPORTED,
+		SHARE_API_SUPPORTED,
 		SELECTABLE_TABS_SUPPORTED
 	} = data);
 	init();
@@ -85,6 +87,9 @@ function init() {
 	if (!WEB_BLOCKING_API_SUPPORTED) {
 		document.getElementById("passReferrerOnErrorOption").hidden = true;
 	}
+	if (!SHARE_API_SUPPORTED) {
+		document.getElementById("sharePageOption").hidden = true;
+	}
 	if (!SELECTABLE_TABS_SUPPORTED) {
 		document.getElementById("selectableTabsMenu").hidden = true;
 		document.getElementById("shortcutsSection").hidden = true;

+ 14 - 3
src/ui/bg/ui-options.js

@@ -36,7 +36,8 @@ let DEFAULT_PROFILE_NAME,
 	IDENTITY_API_SUPPORTED,
 	CLIPBOARD_API_SUPPORTED,
 	NATIVE_API_API_SUPPORTED,
-	WEB_BLOCKING_API_SUPPORTED;
+	WEB_BLOCKING_API_SUPPORTED,
+	SHARE_API_SUPPORTED;
 browser.runtime.sendMessage({ method: "config.getConstants" }).then(data => {
 	({
 		DEFAULT_PROFILE_NAME,
@@ -50,7 +51,8 @@ browser.runtime.sendMessage({ method: "config.getConstants" }).then(data => {
 		IDENTITY_API_SUPPORTED,
 		CLIPBOARD_API_SUPPORTED,
 		NATIVE_API_API_SUPPORTED,
-		WEB_BLOCKING_API_SUPPORTED
+		WEB_BLOCKING_API_SUPPORTED,
+		SHARE_API_SUPPORTED
 	} = data);
 	init();
 });
@@ -75,6 +77,7 @@ const saveRawPageLabel = document.getElementById("saveRawPageLabel");
 const insertMetaCSPLabel = document.getElementById("insertMetaCSPLabel");
 const saveToClipboardLabel = document.getElementById("saveToClipboardLabel");
 const saveToFilesystemLabel = document.getElementById("saveToFilesystemLabel");
+const sharePageLabel = document.getElementById("sharePageLabel");
 const addProofLabel = document.getElementById("addProofLabel");
 const woleetKeyLabel = document.getElementById("woleetKeyLabel");
 const saveToGDriveLabel = document.getElementById("saveToGDriveLabel");
@@ -228,6 +231,7 @@ const githubUserInput = document.getElementById("githubUserInput");
 const githubRepositoryInput = document.getElementById("githubRepositoryInput");
 const githubBranchInput = document.getElementById("githubBranchInput");
 const saveWithCompanionInput = document.getElementById("saveWithCompanionInput");
+const sharePageInput = document.getElementById("sharePageInput");
 const saveToFilesystemInput = document.getElementById("saveToFilesystemInput");
 const compressHTMLInput = document.getElementById("compressHTMLInput");
 const insertTextBodyInput = document.getElementById("insertTextBodyInput");
@@ -528,6 +532,7 @@ saveWithCompanionInput.addEventListener("click", () => disableDestinationPermiss
 saveToGDriveInput.addEventListener("click", () => disableDestinationPermissions(["clipboardWrite", "nativeMessaging"], false), false);
 saveToDropboxInput.addEventListener("click", () => disableDestinationPermissions(["clipboardWrite", "nativeMessaging"], true, false), false);
 saveWithWebDAVInput.addEventListener("click", () => disableDestinationPermissions(["clipboardWrite", "nativeMessaging"]), false);
+sharePageInput.addEventListener("click", () => disableDestinationPermissions(["clipboardWrite", "nativeMessaging"]), false);
 saveCreatedBookmarksInput.addEventListener("click", saveCreatedBookmarks, false);
 passReferrerOnErrorInput.addEventListener("click", passReferrerOnError, false);
 autoSaveExternalSaveInput.addEventListener("click", () => enableExternalSave(autoSaveExternalSaveInput), false);
@@ -612,6 +617,7 @@ saveRawPageLabel.textContent = browser.i18n.getMessage("optionSaveRawPage");
 insertMetaCSPLabel.textContent = browser.i18n.getMessage("optionInsertMetaCSP");
 saveToClipboardLabel.textContent = browser.i18n.getMessage("optionSaveToClipboard");
 saveToFilesystemLabel.textContent = browser.i18n.getMessage("optionSaveToFilesystem");
+sharePageLabel.textContent = browser.i18n.getMessage("optionSharePage");
 addProofLabel.textContent = browser.i18n.getMessage("optionAddProof");
 woleetKeyLabel.textContent = browser.i18n.getMessage("optionWoleetKey");
 saveToGDriveLabel.textContent = browser.i18n.getMessage("optionSaveToGDrive");
@@ -785,6 +791,9 @@ function init() {
 	if (!WEB_BLOCKING_API_SUPPORTED) {
 		document.getElementById("passReferrerOnErrorOption").hidden = true;
 	}
+	if (!SHARE_API_SUPPORTED) {
+		document.getElementById("sharePageOption").hidden = true;
+	}
 }
 
 async function refresh(profileName) {
@@ -921,7 +930,8 @@ async function refresh(profileName) {
 	githubBranchInput.value = profileOptions.githubBranch;
 	githubBranchInput.disabled = !profileOptions.saveToGitHub;
 	saveWithCompanionInput.checked = profileOptions.saveWithCompanion;
-	saveToFilesystemInput.checked = !profileOptions.saveToGDrive && !profileOptions.saveToGitHub && !profileOptions.saveWithCompanion && !profileOptions.saveToClipboard && !profileOptions.saveWithWebDAV && !profileOptions.saveToDropbox;
+	sharePageInput.checked = profileOptions.sharePage;
+	saveToFilesystemInput.checked = !profileOptions.saveToGDrive && !profileOptions.saveToGitHub && !profileOptions.saveWithCompanion && !profileOptions.saveToClipboard && !profileOptions.saveWithWebDAV && !profileOptions.saveToDropbox && !profileOptions.sharePage;
 	compressHTMLInput.checked = profileOptions.compressHTML;
 	compressCSSInput.checked = profileOptions.compressCSS;
 	moveStylesInHeadInput.checked = profileOptions.moveStylesInHead;
@@ -1056,6 +1066,7 @@ async function update() {
 			githubRepository: githubRepositoryInput.value,
 			githubBranch: githubBranchInput.value,
 			saveWithCompanion: saveWithCompanionInput.checked,
+			sharePage: sharePageInput.checked,
 			compressHTML: compressHTMLInput.checked,
 			insertTextBody: insertTextBodyInput.checked,
 			insertEmbeddedImage: insertEmbeddedImageInput.checked,

+ 40 - 6
src/ui/common/common-content-ui.js

@@ -21,7 +21,7 @@
  *   Source.
  */
 
-/* global browser, document, globalThis, getComputedStyle, FileReader, Image, OffscreenCanvas, createImageBitmap */
+/* global document, globalThis, getComputedStyle, FileReader, Image, OffscreenCanvas, createImageBitmap */
 
 const singlefile = globalThis.singlefile;
 
@@ -30,17 +30,23 @@ const CLOSE_ICON = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAA
 const SINGLE_FILE_UI_ELEMENT_CLASS = singlefile.helper.SINGLE_FILE_UI_ELEMENT_CLASS;
 const ERROR_BAR_TAGNAME = "singlefile-error-bar";
 const OPEN_FILE_BAR_TAGNAME = "singlefile-open-file-bar";
-const EMBEDDED_IMAGE_BUTTON_MESSAGE = browser.i18n.getMessage("topPanelEmbeddedImageButton");
-const ERROR_TITLE_MESSAGE = browser.i18n.getMessage("topPanelError");
+const SHARE_PAGE_BAR_TAGNAME = "singlefile-share-page-bar";
+let EMBEDDED_IMAGE_BUTTON_MESSAGE, SHARE_PAGE_BUTTON_MESSAGE, ERROR_TITLE_MESSAGE;
 
 const CSS_PROPERTIES = new Set(Array.from(getComputedStyle(document.documentElement)));
 
 export {
+	setLabels,
 	openFile,
 	getOpenFileBar,
+	getSharePageBar,
 	onError
 };
 
+function setLabels(labels) {
+	({ EMBEDDED_IMAGE_BUTTON_MESSAGE, SHARE_PAGE_BUTTON_MESSAGE, ERROR_TITLE_MESSAGE } = labels);
+}
+
 function onError(message, link) {
 	console.error("SingleFile", message, link); // eslint-disable-line no-console
 	displayBar(ERROR_BAR_TAGNAME, ERROR_TITLE_MESSAGE + message, { link });
@@ -70,6 +76,30 @@ function getOpenFileBar() {
 	};
 }
 
+function getSharePageBar() {
+	let resolvePromise;
+	return {
+		display: async function () {
+			return new Promise(resolve => {
+				resolvePromise = resolve;
+				displayBar(SHARE_PAGE_BAR_TAGNAME, "", { buttonLabel: SHARE_PAGE_BUTTON_MESSAGE, buttonOnclick: resolve });
+			});
+		},
+		hide: function () {
+			const barElement = document.querySelector(SHARE_PAGE_BAR_TAGNAME);
+			if (barElement) {
+				barElement.remove();
+			}
+		},
+		cancel: function () {
+			this.hide();
+			if (resolvePromise) {
+				resolvePromise(true);
+			}
+		}
+	};
+}
+
 function openFile({ accept } = { accept: "image/*" }) {
 	const inputElement = document.createElement("input");
 	inputElement.type = "file";
@@ -143,8 +173,9 @@ function displayBar(tagName, message, { link, buttonLabel, buttonOnclick } = {})
 					padding: 2px;
 					font-family: Arial;
 				}
-				.singlefile-open-file-bar.container {
-					background-color: whitesmoke;
+				.singlefile-open-file-bar.container, .singlefile-share-page-bar.container {
+					background-color: gainsboro;
+					border-block-end: gray 1px solid;
 				}
 				.text {
 					flex: 1;
@@ -172,7 +203,10 @@ function displayBar(tagName, message, { link, buttonLabel, buttonOnclick } = {})
 					font-size: .8rem;
 					align-self: center;
 				}
-				.singlefile-open-file-bar .close-button {
+				.singlefile-open-file-bar button, .singlefile-share-page-bar button{
+					background-color: dimgrey;
+				}
+				.singlefile-open-file-bar .close-button, .singlefile-share-page-bar .close-button{
 					filter: invert(1);
 				}
 				a {

+ 21 - 13
src/ui/content/content-ui-editor-web.js

@@ -21,7 +21,10 @@
  *   Source.
  */
 
-/* global globalThis, window, document, fetch, DOMParser, getComputedStyle, setTimeout, clearTimeout, NodeFilter, Readability, isProbablyReaderable, matchMedia, TextDecoder, Node, URL, MouseEvent, Blob, prompt, MutationObserver, FileReader, Worker, navigator */
+/* global globalThis, window, document, fetch, DOMParser, getComputedStyle, setTimeout, clearTimeout, NodeFilter, Readability, isProbablyReaderable, matchMedia, TextDecoder, Node, URL, prompt, MutationObserver, FileReader, Worker, navigator */
+
+import { setLabels } from "./../../ui/common/common-content-ui.js";
+import { downloadPageForeground } from "../../core/common/download.js";
 
 (globalThis => {
 
@@ -1086,6 +1089,9 @@ pre code {
 					pageOptions.visitDate = new Date(pageOptions.visitDate);
 					filename = await singlefile.helper.formatFilename(content, document, pageOptions);
 				}
+				if (message.sharePage) {
+					setLabels(message.labels);
+				}
 				if (pageCompressContent) {
 					const viewport = document.head.querySelector("meta[name=viewport]");
 					window.parent.postMessage(JSON.stringify({
@@ -1097,17 +1103,17 @@ pre code {
 						url: pageUrl,
 						viewport: viewport ? viewport.content : null,
 						compressContent: true,
-						foregroundSave: message.foregroundSave
+						foregroundSave: message.foregroundSave,
+						sharePage: message.sharePage
 					}), "*");
 				} else {
-					if (message.foregroundSave) {
-						if (filename || (message.filename && message.filename.length)) {
-							const link = document.createElement("a");
-							link.download = filename || message.filename;
-							link.href = URL.createObjectURL(new Blob([content], { type: "text/html" }));
-							link.dispatchEvent(new MouseEvent("click"));
+					if (message.foregroundSave || message.sharePage) {
+						try {
+							await downloadPageForeground({ content, filename: filename || message.filename }, { sharePage: message.sharePage });
+						} catch (error) {
+							console.log(error); // eslint-disable-line no-console
+							window.parent.postMessage(JSON.stringify({ method: "onError", error: error.message }), "*");
 						}
-						return new Promise(resolve => setTimeout(resolve, 1));
 					} else {
 						window.parent.postMessage(JSON.stringify({
 							method: "setContent",
@@ -1132,10 +1138,12 @@ pre code {
 				}), "*");
 			}
 			if (message.method == "download") {
-				const link = document.createElement("a");
-				link.download = message.filename;
-				link.href = URL.createObjectURL(new Blob([new Uint8Array(message.content)], { type: "text/html" }));
-				link.dispatchEvent(new MouseEvent("click"));
+				try {
+					await downloadPageForeground({ content: message.content, filename: message.filename }, { sharePage: message.sharePage });
+				} catch (error) {
+					console.log(error); // eslint-disable-line no-console
+					window.parent.postMessage(JSON.stringify({ method: "onError", error: error.message }), "*");
+				}
 			}
 		};
 		window.onresize = reflowNotes;

+ 1 - 0
src/ui/pages/editor.html

@@ -69,6 +69,7 @@
 	</div>
 	<div class="editor-container">
 		<iframe class="editor"
+			allow="web-share *"
 			srcdoc="&lt;!DOCTYPE html&gt;&lt;body&gt;&lt;script src=/lib/web-stream.js&gt;&lt;/script&gt;&lt;script src=/lib/single-file-extension-editor-helper.js&gt;&lt;/script&gt;&lt;script src=/lib/single-file-extension-editor.js&gt;&lt;/script&gt;&lt;script src=/src/lib/readability/Readability.js&gt;&lt;/script&gt;&lt;script src=/src/lib/readability/Readability-readerable.js&gt;&lt;/script&gt;&lt;/body&gt;"
 			sandbox="allow-scripts allow-modals allow-downloads"></iframe>
 	</div>

+ 7 - 0
src/ui/pages/help.html

@@ -440,6 +440,13 @@
 						<p>Check this option to save the downloaded page on the filesystem of your computer.</p>
 						<p class="notice">It is recommended to <u>check</u> this option</p>
 					</li>
+					<li data-options-label="sharePageLabel" id="sharePageInput"> <span class="option">Option: share
+							page</span>
+						<p>Check this option to share the page with other applications. This option is only available in
+							browsers supporting the <a href="https://web.dev/web-share/" target="_blank">Web Share
+								API</a>.
+						<p class="notice">It is recommended to <u>uncheck</u> this option</p>
+					</li>
 					<li data-options-label="saveToClipboardLabel" id="saveToClipboardOption"> <span
 							class="option">Option: copy to clipboard</span>
 						<p>Check this option to copy the page to the clipboard. This option does not work with

+ 4 - 0
src/ui/pages/options.html

@@ -239,6 +239,10 @@
 				<label for="saveToFilesystemInput" id="saveToFilesystemLabel"></label>
 				<input type="radio" id="saveToFilesystemInput" name="destinationInput">
 			</div>
+			<div class="option" id="sharePageOption">
+				<label for="sharePageInput" id="sharePageLabel"></label>
+				<input type="radio" id="sharePageInput" name="destinationInput">
+			</div>
 			<div class="option" id="saveToClipboardOption">
 				<label for="saveToClipboardInput" id="saveToClipboardLabel"></label>
 				<input type="radio" id="saveToClipboardInput" name="destinationInput">