Răsfoiți Sursa

add option "Destination > upload to Dropbox"
(see #1308)

Gildas 2 ani în urmă
părinte
comite
5e69bc2e34

+ 4 - 0
_locales/de/messages.json

@@ -627,6 +627,10 @@
 		"message": "Hochladen auf Google Drive",
 		"message": "Hochladen auf Google Drive",
 		"description": "Options page label: 'upload to Google Drive'"
 		"description": "Options page label: 'upload to Google Drive'"
 	},
 	},
+	"optionSaveToDropbox": {
+		"message": "Hochladen auf Dropbox",
+		"description": "Options page label: 'upload to Dropbox'"
+	},
 	"optionSaveToGitHub": {
 	"optionSaveToGitHub": {
 		"message": "Hochladen auf GitHub",
 		"message": "Hochladen auf GitHub",
 		"description": "Options page label: 'upload to GitHub'"
 		"description": "Options page label: 'upload to GitHub'"

+ 4 - 0
_locales/en/messages.json

@@ -627,6 +627,10 @@
 		"message": "upload to Google Drive",
 		"message": "upload to Google Drive",
 		"description": "Options page label: 'upload to Google Drive'"
 		"description": "Options page label: 'upload to Google Drive'"
 	},
 	},
+	"optionSaveToDropbox": {
+		"message": "upload to Dropbox",
+		"description": "Options page label: 'upload to Dropbox'"
+	},
 	"optionSaveToGitHub": {
 	"optionSaveToGitHub": {
 		"message": "upload to GitHub",
 		"message": "upload to GitHub",
 		"description": "Options page label: 'upload to GitHub'"
 		"description": "Options page label: 'upload to GitHub'"

+ 4 - 0
_locales/es/messages.json

@@ -627,6 +627,10 @@
 		"message": "subir a Google Drive",
 		"message": "subir a Google Drive",
 		"description": "Options page label: 'upload to Google Drive'"
 		"description": "Options page label: 'upload to Google Drive'"
 	},
 	},
+	"optionSaveToDropbox": {
+		"message": "subir a Dropbox",
+		"description": "Options page label: 'upload to Dropbox'"
+	},
 	"optionSaveToGitHub": {
 	"optionSaveToGitHub": {
 		"message": "subir a GitHub",
 		"message": "subir a GitHub",
 		"description": "Options page label: 'upload to GitHub'"
 		"description": "Options page label: 'upload to GitHub'"

+ 4 - 0
_locales/fr/messages.json

@@ -627,6 +627,10 @@
 		"message": "téléverser sur Google Drive",
 		"message": "téléverser sur Google Drive",
 		"description": "Options page label: 'upload to Google Drive'"
 		"description": "Options page label: 'upload to Google Drive'"
 	},
 	},
+	"optionSaveToDropbox": {
+		"message": "téléverser sur Dropbox",
+		"description": "Options page label: 'upload to Dropbox'"
+	},
 	"optionSaveToGitHub": {
 	"optionSaveToGitHub": {
 		"message": "téléverser sur GitHub",
 		"message": "téléverser sur GitHub",
 		"description": "Options page label: 'upload to GitHub'"
 		"description": "Options page label: 'upload to GitHub'"

+ 4 - 0
_locales/it/messages.json

@@ -627,6 +627,10 @@
 		"message": "carica su Google Drive",
 		"message": "carica su Google Drive",
 		"description": "Options page label: 'upload to Google Drive'"
 		"description": "Options page label: 'upload to Google Drive'"
 	},
 	},
+	"optionSaveToDropbox": {
+		"message": "carica su Dropbox",
+		"description": "Options page label: 'upload to Dropbox'"
+	},
 	"optionSaveToGitHub": {
 	"optionSaveToGitHub": {
 		"message": "carica su GitHub",
 		"message": "carica su GitHub",
 		"description": "Options page label: 'upload to GitHub'"
 		"description": "Options page label: 'upload to GitHub'"

+ 4 - 0
_locales/ja/messages.json

@@ -627,6 +627,10 @@
 		"message": "Google Drive に保存",
 		"message": "Google Drive に保存",
 		"description": "Options page label: 'upload to Google Drive'"
 		"description": "Options page label: 'upload to Google Drive'"
 	},
 	},
+	"optionSaveToDropbox": {
+		"message": "Dropbox に保存",
+		"description": "Options page label: 'upload to Dropbox'"
+	},
 	"optionSaveToGitHub": {
 	"optionSaveToGitHub": {
 		"message": "GitHub にアップロードする",
 		"message": "GitHub にアップロードする",
 		"description": "Options page label: 'upload to GitHub'"
 		"description": "Options page label: 'upload to GitHub'"

+ 4 - 0
_locales/pl/messages.json

@@ -627,6 +627,10 @@
 		"message": "przesyłaj na Dysk Google",
 		"message": "przesyłaj na Dysk Google",
 		"description": "Options page label: 'upload to Google Drive'"
 		"description": "Options page label: 'upload to Google Drive'"
 	},
 	},
+	"optionSaveToDropbox": {
+		"message": "przesyłaj na Dropbox",
+		"description": "Options page label: 'upload to Dropbox'"
+	},
 	"optionSaveToGitHub": {
 	"optionSaveToGitHub": {
 		"message": "przesyłaj do GitHuba",
 		"message": "przesyłaj do GitHuba",
 		"description": "Options page label: 'upload to GitHub'"
 		"description": "Options page label: 'upload to GitHub'"

+ 4 - 0
_locales/pt_PT/messages.json

@@ -627,6 +627,10 @@
 		"message": "carregar para o Google Drive",
 		"message": "carregar para o Google Drive",
 		"description": "Options page label: 'upload to Google Drive'"
 		"description": "Options page label: 'upload to Google Drive'"
 	},
 	},
+	"optionSaveToDropbox": {
+		"message": "carregar para o Dropbox",
+		"description": "Options page label: 'upload to Dropbox'"
+	},
 	"optionSaveToGitHub": {
 	"optionSaveToGitHub": {
 		"message": "carregar para o GitHub",
 		"message": "carregar para o GitHub",
 		"description": "Options page label: 'upload to GitHub'"
 		"description": "Options page label: 'upload to GitHub'"

+ 4 - 0
_locales/pt_br/messages.json

@@ -627,6 +627,10 @@
 		"message": "enviar para o Google Drive",
 		"message": "enviar para o Google Drive",
 		"description": "Options page label: 'upload to Google Drive'"
 		"description": "Options page label: 'upload to Google Drive'"
 	},
 	},
+	"optionSaveToDropbox": {
+		"message": "enviar para o Dropbox",
+		"description": "Options page label: 'upload to Dropbox'"
+	},
 	"optionSaveToGitHub": {
 	"optionSaveToGitHub": {
 		"message": "enviar para o GitHub",
 		"message": "enviar para o GitHub",
 		"description": "Options page label: 'upload to GitHub'"
 		"description": "Options page label: 'upload to GitHub'"

+ 4 - 0
_locales/ru/messages.json

@@ -627,6 +627,10 @@
 		"message": "загрузить на Google Drive",
 		"message": "загрузить на Google Drive",
 		"description": "Options page label: 'upload to Google Drive'"
 		"description": "Options page label: 'upload to Google Drive'"
 	},
 	},
+	"optionSaveToDropbox": {
+		"message": "загрузить на Dropbox",
+		"description": "Options page label: 'upload to Dropbox'"
+	},
 	"optionSaveToGitHub": {
 	"optionSaveToGitHub": {
 		"message": "загрузить на GitHub",
 		"message": "загрузить на GitHub",
 		"description": "Options page label: 'upload to GitHub'"
 		"description": "Options page label: 'upload to GitHub'"

+ 4 - 0
_locales/tr/messages.json

@@ -627,6 +627,10 @@
 		"message": "Google Drive'a yükle",
 		"message": "Google Drive'a yükle",
 		"description": "Options page label: 'upload to Google Drive'"
 		"description": "Options page label: 'upload to Google Drive'"
 	},
 	},
+	"optionSaveToDropbox": {
+		"message": "Dropbox'a yükle",
+		"description": "Options page label: 'upload to Dropbox'"
+	},
 	"optionSaveToGitHub": {
 	"optionSaveToGitHub": {
 		"message": "GitHub'a yükle",
 		"message": "GitHub'a yükle",
 		"description": "Options page label: 'upload to GitHub'"
 		"description": "Options page label: 'upload to GitHub'"

+ 4 - 0
_locales/uk/messages.json

@@ -627,6 +627,10 @@
 		"message": "завантажити на Google Drive",
 		"message": "завантажити на Google Drive",
 		"description": "Options page label: 'upload to Google Drive'"
 		"description": "Options page label: 'upload to Google Drive'"
 	},
 	},
+	"optionSaveToDropbox": {
+		"message": "завантажити на Dropbox",
+		"description": "Options page label: 'upload to Dropbox'"
+	},
 	"optionSaveToGitHub": {
 	"optionSaveToGitHub": {
 		"message": "завантажити на GitHub",
 		"message": "завантажити на GitHub",
 		"description": "Options page label: 'upload to GitHub'"
 		"description": "Options page label: 'upload to GitHub'"

+ 4 - 0
_locales/zh_CN/messages.json

@@ -627,6 +627,10 @@
 		"message": "保存到 Google Drive",
 		"message": "保存到 Google Drive",
 		"description": "Options page label: 'upload to Google Drive'"
 		"description": "Options page label: 'upload to Google Drive'"
 	},
 	},
+	"optionSaveToDropbox": {
+		"message": "保存到 Dropbox",
+		"description": "Options page label: 'upload to Dropbox'"
+	},
 	"optionSaveToGitHub": {
 	"optionSaveToGitHub": {
 		"message": "保存到 GitHub",
 		"message": "保存到 GitHub",
 		"description": "Options page label: 'upload to GitHub'"
 		"description": "Options page label: 'upload to GitHub'"

+ 4 - 0
_locales/zh_TW/messages.json

@@ -627,6 +627,10 @@
 		"message": "保存到 Google Drive",
 		"message": "保存到 Google Drive",
 		"description": "Options page label: 'upload to Google Drive'"
 		"description": "Options page label: 'upload to Google Drive'"
 	},
 	},
+	"optionSaveToDropbox": {
+		"message": "保存到 Dropbox",
+		"description": "Options page label: 'upload to Dropbox'"
+	},
 	"optionSaveToGitHub": {
 	"optionSaveToGitHub": {
 		"message": "保存到 GitHub",
 		"message": "保存到 GitHub",
 		"description": "Options page label: 'upload to GitHub'"
 		"description": "Options page label: 'upload to GitHub'"

+ 9 - 2
src/core/bg/autosave.js

@@ -151,7 +151,7 @@ async function saveContent(message, tab) {
 		options.incognito = tab.incognito;
 		options.incognito = tab.incognito;
 		options.tabId = tabId;
 		options.tabId = tabId;
 		options.tabIndex = tab.index;
 		options.tabIndex = tab.index;
-		options.keepFilename = options.saveToGDrive || options.saveToGitHub || options.saveWithWebDAV;
+		options.keepFilename = options.saveToGDrive || options.saveToGitHub || options.saveWithWebDAV || options.saveToDropbox;
 		let pageData;
 		let pageData;
 		try {
 		try {
 			if (options.autoSaveExternalSave) {
 			if (options.autoSaveExternalSave) {
@@ -163,7 +163,7 @@ async function saveContent(message, tab) {
 				options.tabId = tabId;
 				options.tabId = tabId;
 				pageData = await getPageData(options, null, null, { fetch });
 				pageData = await getPageData(options, null, null, { fetch });
 				let skipped;
 				let skipped;
-				if (!options.saveToGDrive && !options.saveWithWebDAV && !options.saveToGitHub && !options.saveWithCompanion) {
+				if (!options.saveToGDrive && !options.saveWithWebDAV && !options.saveToGitHub && !options.saveToDropbox && !options.saveWithCompanion) {
 					const testSkip = await downloads.testSkipSave(pageData.filename, options);
 					const testSkip = await downloads.testSkipSave(pageData.filename, options);
 					skipped = testSkip.skipped;
 					skipped = testSkip.skipped;
 					options.filenameConflictAction = testSkip.filenameConflictAction;
 					options.filenameConflictAction = testSkip.filenameConflictAction;
@@ -182,6 +182,13 @@ async function saveContent(message, tab) {
 						}, {
 						}, {
 							filenameConflictAction: options.filenameConflictAction
 							filenameConflictAction: options.filenameConflictAction
 						});
 						});
+					} if (options.saveToDropbox) {
+						if (!(content instanceof Blob)) {
+							content = new Blob([content], { type: "text/html" });
+						}
+						await downloads.saveToDropbox(message.taskId, downloads.encodeSharpCharacter(pageData.filename), content, {
+							filenameConflictAction: options.filenameConflictAction
+						});
 					} else if (options.saveWithWebDAV) {
 					} else if (options.saveWithWebDAV) {
 						await downloads.saveWithWebDAV(message.taskId, downloads.encodeSharpCharacter(pageData.filename), content, options.webDAVURL, options.webDAVUser, options.webDAVPassword, {
 						await downloads.saveWithWebDAV(message.taskId, downloads.encodeSharpCharacter(pageData.filename), content, options.webDAVURL, options.webDAVUser, options.webDAVPassword, {
 							filenameConflictAction: options.filenameConflictAction
 							filenameConflictAction: options.filenameConflictAction

+ 22 - 1
src/core/bg/config.js

@@ -102,6 +102,7 @@ const DEFAULT_CONFIG = {
 	saveToClipboard: false,
 	saveToClipboard: false,
 	addProof: false,
 	addProof: false,
 	saveToGDrive: false,
 	saveToGDrive: false,
+	saveToDropbox: false,
 	saveWithWebDAV: false,
 	saveWithWebDAV: false,
 	webDAVURL: "",
 	webDAVURL: "",
 	webDAVUser: "",
 	webDAVUser: "",
@@ -221,8 +222,11 @@ export {
 	updateRule,
 	updateRule,
 	addRule,
 	addRule,
 	getAuthInfo,
 	getAuthInfo,
+	getDropboxAuthInfo,
 	setAuthInfo,
 	setAuthInfo,
-	removeAuthInfo
+	setDropboxAuthInfo,
+	removeAuthInfo,
+	removeDropboxAuthInfo
 };
 };
 
 
 async function upgrade() {
 async function upgrade() {
@@ -578,10 +582,18 @@ async function getAuthInfo() {
 	return (await configStorage.get()).authInfo;
 	return (await configStorage.get()).authInfo;
 }
 }
 
 
+async function getDropboxAuthInfo() {
+	return (await configStorage.get()).dropboxAuthInfo;
+}
+
 async function setAuthInfo(authInfo) {
 async function setAuthInfo(authInfo) {
 	await configStorage.set({ authInfo });
 	await configStorage.set({ authInfo });
 }
 }
 
 
+async function setDropboxAuthInfo(authInfo) {
+	await configStorage.set({ dropboxAuthInfo: authInfo });
+}
+
 async function removeAuthInfo() {
 async function removeAuthInfo() {
 	let authInfo = getAuthInfo();
 	let authInfo = getAuthInfo();
 	if (authInfo.revokableAccessToken) {
 	if (authInfo.revokableAccessToken) {
@@ -591,6 +603,15 @@ async function removeAuthInfo() {
 	}
 	}
 }
 }
 
 
+async function removeDropboxAuthInfo() {
+	let authInfo = getDropboxAuthInfo();
+	if (authInfo.revokableAccessToken) {
+		setDropboxAuthInfo({ revokableAccessToken: authInfo.revokableAccessToken });
+	} else {
+		await configStorage.remove(["dropboxAuthInfo"]);
+	}
+}
+
 async function resetProfiles() {
 async function resetProfiles() {
 	await pendingUpgradePromise;
 	await pendingUpgradePromise;
 	const allTabsData = await tabsData.get();
 	const allTabsData = await tabsData.get();

+ 80 - 8
src/core/bg/downloads.js

@@ -32,6 +32,7 @@ import { launchWebAuthFlow, extractAuthCode } from "./tabs-util.js";
 import * as ui from "./../../ui/bg/index.js";
 import * as ui from "./../../ui/bg/index.js";
 import * as woleet from "./../../lib/woleet/woleet.js";
 import * as woleet from "./../../lib/woleet/woleet.js";
 import { GDrive } from "./../../lib/gdrive/gdrive.js";
 import { GDrive } from "./../../lib/gdrive/gdrive.js";
+import { Dropbox } from "./../../lib/dropbox/dropbox.js";
 import { WebDAV } from "./../../lib/webdav/webdav.js";
 import { WebDAV } from "./../../lib/webdav/webdav.js";
 import { GitHub } from "./../../lib/github/github.js";
 import { GitHub } from "./../../lib/github/github.js";
 import { download } from "./download-util.js";
 import { download } from "./download-util.js";
@@ -46,14 +47,16 @@ const CONFLICT_ACTION_UNIQUIFY = "uniquify";
 const REGEXP_ESCAPE = /([{}()^$&.*?/+|[\\\\]|\]|-)/g;
 const REGEXP_ESCAPE = /([{}()^$&.*?/+|[\\\\]|\]|-)/g;
 let GDRIVE_CLIENT_ID = "207618107333-h1220p1oasj3050kr5r416661adm091a.apps.googleusercontent.com";
 let GDRIVE_CLIENT_ID = "207618107333-h1220p1oasj3050kr5r416661adm091a.apps.googleusercontent.com";
 let GDRIVE_CLIENT_KEY = "VQJ8Gq8Vxx72QyxPyeLtWvUt";
 let GDRIVE_CLIENT_KEY = "VQJ8Gq8Vxx72QyxPyeLtWvUt";
+const DROPBOX_CLIENT_ID = "s50p6litdvuzrtb";
+const DROPBOX_CLIENT_KEY = "i1vzwllesr14fzd";
 
 
-let gDrive;
-const oauth2 = browser.runtime.getManifest().oauth2;
-if (oauth2) {
-	GDRIVE_CLIENT_ID = oauth2.client_id;
-	GDRIVE_CLIENT_KEY = oauth2.client_secret;
+const gDriveOauth2 = browser.runtime.getManifest().oauth2;
+if (gDriveOauth2) {
+	GDRIVE_CLIENT_ID = gDriveOauth2.client_id;
+	GDRIVE_CLIENT_KEY = gDriveOauth2.client_secret;
 }
 }
-gDrive = new GDrive(GDRIVE_CLIENT_ID, GDRIVE_CLIENT_KEY, SCOPES);
+const gDrive = new GDrive(GDRIVE_CLIENT_ID, GDRIVE_CLIENT_KEY, SCOPES);
+const dropbox = new Dropbox(DROPBOX_CLIENT_ID, DROPBOX_CLIENT_KEY);
 
 
 export {
 export {
 	onMessage,
 	onMessage,
@@ -61,6 +64,7 @@ export {
 	testSkipSave,
 	testSkipSave,
 	saveToGDrive,
 	saveToGDrive,
 	saveToGitHub,
 	saveToGitHub,
+	saveToDropbox,
 	saveWithWebDAV,
 	saveWithWebDAV,
 	encodeSharpCharacter
 	encodeSharpCharacter
 };
 };
@@ -75,6 +79,12 @@ async function onMessage(message, sender) {
 		await gDrive.revokeAuthToken(authInfo && (authInfo.accessToken || authInfo.revokableAccessToken));
 		await gDrive.revokeAuthToken(authInfo && (authInfo.accessToken || authInfo.revokableAccessToken));
 		return {};
 		return {};
 	}
 	}
+	if (message.method.endsWith(".disableDropbox")) {
+		const authInfo = await config.getDropboxAuthInfo();
+		config.removeDropboxAuthInfo();
+		await dropbox.revokeAuthToken(authInfo && (authInfo.accessToken || authInfo.revokableAccessToken));
+		return {};
+	}
 	if (message.method.endsWith(".end")) {
 	if (message.method.endsWith(".end")) {
 		if (message.hash) {
 		if (message.hash) {
 			try {
 			try {
@@ -156,7 +166,7 @@ async function downloadContent(contents, tab, incognito, message) {
 	const tabId = tab.id;
 	const tabId = tab.id;
 	try {
 	try {
 		let skipped;
 		let skipped;
-		if (message.backgroundSave && !message.saveToGDrive && !message.saveWithWebDAV && !message.saveToGitHub) {
+		if (message.backgroundSave && !message.saveToGDrive && !message.saveToDropbox && !message.saveWithWebDAV && !message.saveToGitHub) {
 			const testSkip = await testSkipSave(message.filename, message);
 			const testSkip = await testSkipSave(message.filename, message);
 			message.filenameConflictAction = testSkip.filenameConflictAction;
 			message.filenameConflictAction = testSkip.filenameConflictAction;
 			skipped = testSkip.skipped;
 			skipped = testSkip.skipped;
@@ -182,6 +192,12 @@ async function downloadContent(contents, tab, incognito, message) {
 					filenameConflictAction: message.filenameConflictAction,
 					filenameConflictAction: message.filenameConflictAction,
 					prompt
 					prompt
 				});
 				});
+			} else if (message.saveToDropbox) {
+				await saveToDropbox(message.taskId, encodeSharpCharacter(message.filename), new Blob(contents, { type: MIMETYPE_HTML }), {
+					onProgress: (offset, size) => ui.onUploadProgress(tabId, offset, size),
+					filenameConflictAction: message.filenameConflictAction,
+					prompt
+				});
 			} else if (message.saveToGitHub) {
 			} else if (message.saveToGitHub) {
 				response = await saveToGitHub(message.taskId, encodeSharpCharacter(message.filename), contents.join(""), message.githubToken, message.githubUser, message.githubRepository, message.githubBranch, {
 				response = await saveToGitHub(message.taskId, encodeSharpCharacter(message.filename), contents.join(""), message.githubToken, message.githubUser, message.githubRepository, message.githubBranch, {
 					filenameConflictAction: message.filenameConflictAction,
 					filenameConflictAction: message.filenameConflictAction,
@@ -234,7 +250,7 @@ async function downloadCompressedContent(message, tab) {
 	const tabId = tab.id;
 	const tabId = tab.id;
 	try {
 	try {
 		let skipped;
 		let skipped;
-		if (message.backgroundSave && !message.saveToGDrive && !message.saveWithWebDAV && !message.saveToGitHub) {
+		if (message.backgroundSave && !message.saveToGDrive && !message.saveToDropbox && !message.saveWithWebDAV && !message.saveToGitHub) {
 			const testSkip = await testSkipSave(message.filename, message);
 			const testSkip = await testSkipSave(message.filename, message);
 			message.filenameConflictAction = testSkip.filenameConflictAction;
 			message.filenameConflictAction = testSkip.filenameConflictAction;
 			skipped = testSkip.skipped;
 			skipped = testSkip.skipped;
@@ -280,6 +296,12 @@ async function downloadCompressedContent(message, tab) {
 					filenameConflictAction: message.filenameConflictAction,
 					filenameConflictAction: message.filenameConflictAction,
 					prompt
 					prompt
 				});
 				});
+			} else if (message.saveToDropbox) {
+				await saveToDropbox(message.taskId, encodeSharpCharacter(message.filename), blob, {
+					onProgress: (offset, size) => ui.onUploadProgress(tabId, offset, size),
+					filenameConflictAction: message.filenameConflictAction,
+					prompt
+				});
 			} else if (message.saveToGitHub) {
 			} else if (message.saveToGitHub) {
 				response = await saveToGitHub(message.taskId, encodeSharpCharacter(message.filename), blob, message.githubToken, message.githubUser, message.githubRepository, message.githubBranch, {
 				response = await saveToGitHub(message.taskId, encodeSharpCharacter(message.filename), blob, message.githubToken, message.githubUser, message.githubRepository, message.githubBranch, {
 					filenameConflictAction: message.filenameConflictAction,
 					filenameConflictAction: message.filenameConflictAction,
@@ -354,6 +376,24 @@ async function getAuthInfo(authOptions, force) {
 	return authInfo;
 	return authInfo;
 }
 }
 
 
+async function getDropboxAuthInfo(force) {
+	let authInfo = await config.getDropboxAuthInfo();
+	const options = {
+		launchWebAuthFlow: options => launchWebAuthFlow(options),
+		extractAuthCode: authURL => extractAuthCode(authURL)
+	};
+	dropbox.setAuthInfo(authInfo);
+	if (!authInfo || !authInfo.accessToken || force) {
+		authInfo = await dropbox.auth(options);
+		if (authInfo) {
+			await config.setDropboxAuthInfo(authInfo);
+		} else {
+			await config.removeDropboxAuthInfo();
+		}
+	}
+	return authInfo;
+}
+
 async function saveToGitHub(taskId, filename, content, githubToken, githubUser, githubRepository, githubBranch, { filenameConflictAction, prompt }) {
 async function saveToGitHub(taskId, filename, content, githubToken, githubUser, githubRepository, githubBranch, { filenameConflictAction, prompt }) {
 	try {
 	try {
 		const taskInfo = business.getTaskInfo(taskId);
 		const taskInfo = business.getTaskInfo(taskId);
@@ -412,6 +452,38 @@ async function saveToGDrive(taskId, filename, blob, authOptions, uploadOptions)
 	}
 	}
 }
 }
 
 
+async function saveToDropbox(taskId, filename, blob, uploadOptions) {
+	try {
+		await getDropboxAuthInfo();
+		const taskInfo = business.getTaskInfo(taskId);
+		if (!taskInfo || !taskInfo.cancelled) {
+			return await dropbox.upload(filename, blob, uploadOptions, callback => business.setCancelCallback(taskId, callback));
+		}
+	}
+	catch (error) {
+		if (error.message == "invalid_token") {
+			let authInfo;
+			try {
+				authInfo = await dropbox.refreshAuthToken();
+			} catch (error) {
+				if (error.message == "unknown_token") {
+					authInfo = await getDropboxAuthInfo(true);
+				} else {
+					throw new Error(error.message + " (Dropbox)");
+				}
+			}
+			if (authInfo) {
+				await config.setDropboxAuthInfo(authInfo);
+			} else {
+				await config.removeDropboxAuthInfo();
+			}
+			return await saveToDropbox(taskId, filename, blob, uploadOptions);
+		} else {
+			throw new Error(error.message + " (Dropbox)");
+		}
+	}
+}
+
 async function testSkipSave(filename, options) {
 async function testSkipSave(filename, options) {
 	let skipped, filenameConflictAction = options.filenameConflictAction;
 	let skipped, filenameConflictAction = options.filenameConflictAction;
 	if (filenameConflictAction == CONFLICT_ACTION_SKIP) {
 	if (filenameConflictAction == CONFLICT_ACTION_SKIP) {

+ 2 - 1
src/core/common/download.js

@@ -44,6 +44,7 @@ async function downloadPage(pageData, options) {
 		filename: pageData.filename,
 		filename: pageData.filename,
 		saveToClipboard: options.saveToClipboard,
 		saveToClipboard: options.saveToClipboard,
 		saveToGDrive: options.saveToGDrive,
 		saveToGDrive: options.saveToGDrive,
+		saveToDropbox: options.saveToDropbox,
 		saveWithWebDAV: options.saveWithWebDAV,
 		saveWithWebDAV: options.saveWithWebDAV,
 		webDAVURL: options.webDAVURL,
 		webDAVURL: options.webDAVURL,
 		webDAVUser: options.webDAVUser,
 		webDAVUser: options.webDAVUser,
@@ -105,7 +106,7 @@ async function downloadPage(pageData, options) {
 			await browser.runtime.sendMessage({ method: "downloads.end", taskId: options.taskId });
 			await browser.runtime.sendMessage({ method: "downloads.end", taskId: options.taskId });
 		}
 		}
 	} else {
 	} else {
-		if (options.backgroundSave || options.openEditor || options.saveToGDrive || options.saveToGitHub || options.saveWithCompanion || options.saveWithWebDAV) {
+		if (options.backgroundSave || options.openEditor || options.saveToGDrive || options.saveToGitHub || options.saveWithCompanion || options.saveWithWebDAV || options.saveToDropbox) {
 			const blobURL = URL.createObjectURL(new Blob([pageData.content], { type: "text/html" }));
 			const blobURL = URL.createObjectURL(new Blob([pageData.content], { type: "text/html" }));
 			message.blobURL = blobURL;
 			message.blobURL = blobURL;
 			const result = await browser.runtime.sendMessage(message);
 			const result = await browser.runtime.sendMessage(message);

+ 2 - 2
src/core/content/content.js

@@ -119,7 +119,7 @@ async function savePage(message) {
 			try {
 			try {
 				const pageData = await processPage(options);
 				const pageData = await processPage(options);
 				if (pageData) {
 				if (pageData) {
-					if (((!options.backgroundSave && !options.saveToClipboard) || options.saveToGDrive || options.saveToGitHub || options.saveWithCompanion || options.saveWithWebDAV) && options.confirmFilename) {
+					if (((!options.backgroundSave && !options.saveToClipboard) || options.saveToGDrive || options.saveToGitHub || options.saveWithCompanion || options.saveWithWebDAV || options.saveToDropbox) && options.confirmFilename) {
 						pageData.filename = ui.prompt("Save as", pageData.filename) || pageData.filename;
 						pageData.filename = ui.prompt("Save as", pageData.filename) || pageData.filename;
 					}
 					}
 					await download.downloadPage(pageData, options);
 					await download.downloadPage(pageData, options);
@@ -145,7 +145,7 @@ async function savePage(message) {
 async function processPage(options) {
 async function processPage(options) {
 	const frames = singlefile.processors.frameTree;
 	const frames = singlefile.processors.frameTree;
 	let framesSessionId;
 	let framesSessionId;
-	options.keepFilename = options.saveToGDrive || options.saveToGitHub || options.saveWithWebDAV;
+	options.keepFilename = options.saveToGDrive || options.saveToGitHub || options.saveWithWebDAV || options.saveToDropbox;
 	singlefile.helper.initDoc(document);
 	singlefile.helper.initDoc(document);
 	ui.onStartPage(options);
 	ui.onStartPage(options);
 	processor = new singlefile.SingleFile(options);
 	processor = new singlefile.SingleFile(options);

+ 326 - 0
src/lib/dropbox/dropbox.js

@@ -0,0 +1,326 @@
+/*
+ * Copyright 2010-2020 Gildas Lormeau
+ * contact : gildas.lormeau <at> gmail.com
+ * 
+ * This file is part of SingleFile.
+ *
+ *   The code in this file is free software: you can redistribute it and/or 
+ *   modify it under the terms of the GNU Affero General Public License 
+ *   (GNU AGPL) as published by the Free Software Foundation, either version 3
+ *   of the License, or (at your option) any later version.
+ * 
+ *   The code in this file is distributed in the hope that it will be useful, 
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of 
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 
+ *   General Public License for more details.
+ *
+ *   As additional permission under GNU AGPL version 3 section 7, you may 
+ *   distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 
+ *   AGPL normally required by section 4, provided you include this license 
+ *   notice and a URL through which recipients can access the Corresponding 
+ *   Source.
+ */
+
+/* global browser, fetch */
+
+const TOKEN_URL = "https://api.dropboxapi.com/oauth2/token";
+const AUTH_URL = "https://www.dropbox.com/oauth2/authorize";
+const REVOKE_ACCESS_URL = "https://api.dropboxapi.com/2/auth/token/revoke";
+const DROPBOX_SEARCH_URL = "https://api.dropboxapi.com/2/files/search_v2";
+const DROPBOX_UPLOAD_URL = "https://content.dropboxapi.com/2/files/upload_session/start";
+const DROPBOX_APPEND_URL = "https://content.dropboxapi.com/2/files/upload_session/append_v2";
+const DROPBOX_FINISH_URL = "https://content.dropboxapi.com/2/files/upload_session/finish";
+const CONFLICT_ACTION_UNIQUIFY = "uniquify";
+const CONFLICT_ACTION_OVERWRITE = "overwrite";
+const CONFLICT_ACTION_SKIP = "skip";
+const CONFLICT_ACTION_PROMPT = "prompt";
+const ENCODED_CHARS = /[\u007f-\uffff]/g;
+
+class Dropbox {
+	constructor(clientId, clientKey) {
+		this.clientId = clientId;
+		this.clientKey = clientKey;
+	}
+	async auth(options = { interactive: true }) {
+		this.authURL = AUTH_URL +
+			"?client_id=" + this.clientId +
+			"&response_type=code" +
+			"&token_access_type=offline" +
+			"&redirect_uri=" + browser.identity.getRedirectURL();
+		return options.code ? authFromCode(this, options) : initAuth(this, options);
+	}
+	setAuthInfo(authInfo) {
+		if (authInfo) {
+			this.accessToken = authInfo.accessToken;
+			this.refreshToken = authInfo.refreshToken;
+			this.expirationDate = authInfo.expirationDate;
+		} else {
+			delete this.accessToken;
+			delete this.refreshToken;
+			delete this.expirationDate;
+		}
+	}
+	async refreshAuthToken() {
+		if (this.refreshToken) {
+			const httpResponse = await fetch(TOKEN_URL, {
+				method: "POST",
+				headers: { "Content-Type": "application/x-www-form-urlencoded" },
+				body: "client_id=" + this.clientId +
+					"&refresh_token=" + this.refreshToken +
+					"&grant_type=refresh_token" +
+					"&client_secret=" + this.clientKey
+			});
+			if (httpResponse.status == 400) {
+				throw new Error("unknown_token");
+			}
+			const response = await getJSON(httpResponse);
+			this.accessToken = response.access_token;
+			if (response.refresh_token) {
+				this.refreshToken = response.refresh_token;
+			}
+			if (response.expires_in) {
+				this.expirationDate = Date.now() + (response.expires_in * 1000);
+			}
+			return { accessToken: this.accessToken, refreshToken: this.refreshToken, expirationDate: this.expirationDate };
+		} else {
+			delete this.accessToken;
+		}
+	}
+	async revokeAuthToken(accessToken) {
+		if (accessToken) {
+			const httpResponse = await fetch(REVOKE_ACCESS_URL, {
+				method: "POST",
+				headers: {
+					"Authorization": "Bearer " + accessToken
+				}
+			});
+			try {
+				await httpResponse.text();
+			}
+			catch (error) {
+				if (error.message != "invalid_token") {
+					throw error;
+				}
+			}
+			finally {
+				delete this.accessToken;
+				delete this.refreshToken;
+				delete this.expirationDate;
+			}
+		}
+	}
+	async upload(filename, blob, options, setCancelCallback) {
+		const uploader = new MediaUploader({
+			token: this.accessToken,
+			file: blob,
+			filename,
+			onProgress: options.onProgress,
+			filenameConflictAction: options.filenameConflictAction,
+			prompt: options.prompt
+		});
+		if (setCancelCallback) {
+			setCancelCallback(() => uploader.cancelled = true);
+		}
+		await uploader.upload();
+	}
+}
+
+class MediaUploader {
+	constructor(options) {
+		this.file = options.file;
+		this.onProgress = options.onProgress;
+		this.contentType = this.file.type || "application/octet-stream";
+		this.metadata = {
+			name: options.filename,
+			mimeType: this.contentType
+		};
+		this.token = options.token;
+		this.offset = 0;
+		this.chunkSize = options.chunkSize || 8 * 1024 * 1024;
+		this.filenameConflictAction = options.filenameConflictAction;
+		this.prompt = options.prompt;
+	}
+	async upload() {
+		const httpListResponse = getResponse(await fetch(DROPBOX_SEARCH_URL, {
+			method: "POST",
+			headers: {
+				"Authorization": "Bearer " + this.token,
+				"Content-Type": "application/json"
+			},
+			body: stringify({
+				query: this.metadata.name,
+				options: {
+					filename: true
+				}
+			})
+		}));
+		const response = await getJSON(httpListResponse);
+		if (response.matches.length) {
+			if (this.filenameConflictAction == CONFLICT_ACTION_PROMPT) {
+				if (this.prompt) {
+					const name = await this.prompt(this.metadata.name);
+					if (name) {
+						this.metadata.name = name;
+					} else {
+						return response;
+					}
+				} else {
+					this.filenameConflictAction = CONFLICT_ACTION_UNIQUIFY;
+				}
+			} else if (this.filenameConflictAction == CONFLICT_ACTION_SKIP) {
+				return response;
+			}
+		}
+		const httpResponse = getResponse(await fetch(DROPBOX_UPLOAD_URL, {
+			method: "POST",
+			headers: {
+				"Authorization": "Bearer " + this.token,
+				"Dropbox-API-Arg": stringify({
+					close: false
+				}),
+				"Content-Type": "application/octet-stream"
+			}
+		}));
+		const sessionId = (await getJSON(httpResponse)).session_id;
+		this.sessionId = sessionId;
+		if (!this.cancelled) {
+			if (this.onProgress) {
+				this.onProgress(0, this.file.size);
+			}
+			return sendFile(this);
+		}
+	}
+}
+
+export { Dropbox };
+
+async function authFromCode(dropbox, options) {
+	const httpResponse = await fetch(TOKEN_URL, {
+		method: "POST",
+		headers: { "Content-Type": "application/x-www-form-urlencoded" },
+		body: "client_id=" + dropbox.clientId +
+			"&client_secret=" + dropbox.clientKey +
+			"&grant_type=authorization_code" +
+			"&code=" + options.code +
+			"&redirect_uri=" + browser.identity.getRedirectURL()
+	});
+	const response = await getJSON(httpResponse);
+	dropbox.accessToken = response.access_token;
+	dropbox.refreshToken = response.refresh_token;
+	dropbox.expirationDate = Date.now() + (response.expires_in * 1000);
+	return { accessToken: dropbox.accessToken, refreshToken: dropbox.refreshToken, expirationDate: dropbox.expirationDate };
+}
+
+async function initAuth(dropbox, options) {
+	let code;
+	try {
+		options.extractAuthCode(browser.identity.getRedirectURL())
+			.then(authCode => code = authCode)
+			.catch(() => { /* ignored */ });
+		return await options.launchWebAuthFlow({ url: dropbox.authURL });
+	}
+	catch (error) {
+		if (error.message && (error.message == "code_required" || error.message.includes("access"))) {
+			if (code) {
+				options.code = code;
+				return await authFromCode(dropbox, options);
+			} else {
+				throw new Error("code_required");
+			}
+		} else {
+			throw error;
+		}
+	}
+}
+
+async function sendFile(mediaUploader) {
+	let content = mediaUploader.file, end = mediaUploader.file.size;
+	if (mediaUploader.offset || mediaUploader.chunkSize) {
+		if (mediaUploader.chunkSize) {
+			end = Math.min(mediaUploader.offset + mediaUploader.chunkSize, mediaUploader.file.size);
+		}
+		content = content.slice(mediaUploader.offset, end);
+	}
+	const httpAppendResponse = getResponse(await fetch(DROPBOX_APPEND_URL, {
+		method: "POST",
+		headers: {
+			"Authorization": "Bearer " + mediaUploader.token,
+			"Content-Type": "application/octet-stream",
+			"Dropbox-API-Arg": stringify({
+				cursor: {
+					session_id: mediaUploader.sessionId,
+					offset: mediaUploader.offset
+				},
+				close: end == mediaUploader.file.size
+			})
+		},
+		body: content
+	}));
+	if (mediaUploader.onProgress && !mediaUploader.cancelled) {
+		mediaUploader.onProgress(mediaUploader.offset + mediaUploader.chunkSize, mediaUploader.file.size);
+	}
+	if (httpAppendResponse.status == 200) {
+		mediaUploader.offset = end;
+		if (mediaUploader.offset < mediaUploader.file.size) {
+			return sendFile(mediaUploader);
+		}
+	}
+	let path = mediaUploader.metadata.name;
+	if (!path.startsWith("/")) {
+		path = "/" + path;
+	}
+	const httpFinishResponse = await fetch(DROPBOX_FINISH_URL, {
+		method: "POST",
+		headers: {
+			"Authorization": "Bearer " + mediaUploader.token,
+			"Content-Type": "application/octet-stream",
+			"Dropbox-API-Arg": stringify({
+				cursor: {
+					session_id: mediaUploader.sessionId,
+					offset: mediaUploader.offset
+				},
+				commit: {
+					path,
+					mode: mediaUploader.filenameConflictAction == CONFLICT_ACTION_OVERWRITE ? "overwrite" : "add",
+					autorename: mediaUploader.filenameConflictAction == CONFLICT_ACTION_UNIQUIFY
+				}
+			})
+		}
+	});
+	if (httpFinishResponse.status == 200) {
+		return getJSON(httpFinishResponse);
+	} else if (httpFinishResponse.status == 409 && mediaUploader.filenameConflictAction == CONFLICT_ACTION_PROMPT) {
+		mediaUploader.offset = 0;
+		return mediaUploader.upload();
+	} else {
+		throw new Error("unknown_error (" + httpFinishResponse.status + ")");
+	}
+}
+
+async function getJSON(httpResponse) {
+	httpResponse = getResponse(httpResponse);
+	const response = await httpResponse.json();
+	if (response.error) {
+		throw new Error(response.error);
+	} else {
+		return response;
+	}
+}
+
+function getResponse(httpResponse) {
+	if (httpResponse.status == 200) {
+		return httpResponse;
+	} else if (httpResponse.status == 401) {
+		throw new Error("invalid_token");
+	} else {
+		throw new Error("unknown_error (" + httpResponse.status + ")");
+	}
+}
+
+function stringify(value) {
+	return JSON.stringify(value).replace(ENCODED_CHARS,
+		function (c) {
+			return "\\u" + ("000" + c.charCodeAt(0).toString(16)).slice(-4);
+		}
+	);
+}

+ 11 - 2
src/ui/bg/ui-options.js

@@ -78,6 +78,7 @@ const saveToFilesystemLabel = document.getElementById("saveToFilesystemLabel");
 const addProofLabel = document.getElementById("addProofLabel");
 const addProofLabel = document.getElementById("addProofLabel");
 const woleetKeyLabel = document.getElementById("woleetKeyLabel");
 const woleetKeyLabel = document.getElementById("woleetKeyLabel");
 const saveToGDriveLabel = document.getElementById("saveToGDriveLabel");
 const saveToGDriveLabel = document.getElementById("saveToGDriveLabel");
+const saveToDropboxLabel = document.getElementById("saveToDropboxLabel");
 const saveWithWebDAVLabel = document.getElementById("saveWithWebDAVLabel");
 const saveWithWebDAVLabel = document.getElementById("saveWithWebDAVLabel");
 const webDAVURLLabel = document.getElementById("webDAVURLLabel");
 const webDAVURLLabel = document.getElementById("webDAVURLLabel");
 const webDAVUserLabel = document.getElementById("webDAVUserLabel");
 const webDAVUserLabel = document.getElementById("webDAVUserLabel");
@@ -215,6 +216,7 @@ const saveToClipboardInput = document.getElementById("saveToClipboardInput");
 const addProofInput = document.getElementById("addProofInput");
 const addProofInput = document.getElementById("addProofInput");
 const woleetKeyInput = document.getElementById("woleetKeyInput");
 const woleetKeyInput = document.getElementById("woleetKeyInput");
 const saveToGDriveInput = document.getElementById("saveToGDriveInput");
 const saveToGDriveInput = document.getElementById("saveToGDriveInput");
+const saveToDropboxInput = document.getElementById("saveToDropboxInput");
 const saveWithWebDAVInput = document.getElementById("saveWithWebDAVInput");
 const saveWithWebDAVInput = document.getElementById("saveWithWebDAVInput");
 const webDAVURLInput = document.getElementById("webDAVURLInput");
 const webDAVURLInput = document.getElementById("webDAVURLInput");
 const webDAVUserInput = document.getElementById("webDAVUserInput");
 const webDAVUserInput = document.getElementById("webDAVUserInput");
@@ -512,6 +514,7 @@ saveToFilesystemInput.addEventListener("click", () => disableDestinationPermissi
 saveToClipboardInput.addEventListener("click", () => disableDestinationPermissions(["nativeMessaging"]), false);
 saveToClipboardInput.addEventListener("click", () => disableDestinationPermissions(["nativeMessaging"]), false);
 saveWithCompanionInput.addEventListener("click", () => disableDestinationPermissions(["clipboardWrite"]), false);
 saveWithCompanionInput.addEventListener("click", () => disableDestinationPermissions(["clipboardWrite"]), false);
 saveToGDriveInput.addEventListener("click", () => disableDestinationPermissions(["clipboardWrite", "nativeMessaging"], false), false);
 saveToGDriveInput.addEventListener("click", () => disableDestinationPermissions(["clipboardWrite", "nativeMessaging"], false), false);
+saveToDropboxInput.addEventListener("click", () => disableDestinationPermissions(["clipboardWrite", "nativeMessaging"], true, false), false);
 saveWithWebDAVInput.addEventListener("click", () => disableDestinationPermissions(["clipboardWrite", "nativeMessaging"]), false);
 saveWithWebDAVInput.addEventListener("click", () => disableDestinationPermissions(["clipboardWrite", "nativeMessaging"]), false);
 saveCreatedBookmarksInput.addEventListener("click", saveCreatedBookmarks, false);
 saveCreatedBookmarksInput.addEventListener("click", saveCreatedBookmarks, false);
 passReferrerOnErrorInput.addEventListener("click", passReferrerOnError, false);
 passReferrerOnErrorInput.addEventListener("click", passReferrerOnError, false);
@@ -600,6 +603,7 @@ saveToFilesystemLabel.textContent = browser.i18n.getMessage("optionSaveToFilesys
 addProofLabel.textContent = browser.i18n.getMessage("optionAddProof");
 addProofLabel.textContent = browser.i18n.getMessage("optionAddProof");
 woleetKeyLabel.textContent = browser.i18n.getMessage("optionWoleetKey");
 woleetKeyLabel.textContent = browser.i18n.getMessage("optionWoleetKey");
 saveToGDriveLabel.textContent = browser.i18n.getMessage("optionSaveToGDrive");
 saveToGDriveLabel.textContent = browser.i18n.getMessage("optionSaveToGDrive");
+saveToDropboxLabel.textContent = browser.i18n.getMessage("optionSaveToDropbox");
 saveWithWebDAVLabel.textContent = browser.i18n.getMessage("optionSaveWithWebDAV");
 saveWithWebDAVLabel.textContent = browser.i18n.getMessage("optionSaveWithWebDAV");
 webDAVURLLabel.textContent = browser.i18n.getMessage("optionWebDAVURL");
 webDAVURLLabel.textContent = browser.i18n.getMessage("optionWebDAVURL");
 webDAVUserLabel.textContent = browser.i18n.getMessage("optionWebDAVUser");
 webDAVUserLabel.textContent = browser.i18n.getMessage("optionWebDAVUser");
@@ -885,6 +889,7 @@ async function refresh(profileName) {
 	woleetKeyInput.value = profileOptions.woleetKey;
 	woleetKeyInput.value = profileOptions.woleetKey;
 	woleetKeyInput.disabled = !profileOptions.addProof;
 	woleetKeyInput.disabled = !profileOptions.addProof;
 	saveToGDriveInput.checked = profileOptions.saveToGDrive;
 	saveToGDriveInput.checked = profileOptions.saveToGDrive;
+	saveToDropboxInput.checked = profileOptions.saveToDropbox;
 	saveWithWebDAVInput.checked = profileOptions.saveWithWebDAV;
 	saveWithWebDAVInput.checked = profileOptions.saveWithWebDAV;
 	webDAVURLInput.value = profileOptions.webDAVURL;
 	webDAVURLInput.value = profileOptions.webDAVURL;
 	webDAVURLInput.disabled = !profileOptions.saveWithWebDAV;
 	webDAVURLInput.disabled = !profileOptions.saveWithWebDAV;
@@ -902,7 +907,7 @@ async function refresh(profileName) {
 	githubBranchInput.value = profileOptions.githubBranch;
 	githubBranchInput.value = profileOptions.githubBranch;
 	githubBranchInput.disabled = !profileOptions.saveToGitHub;
 	githubBranchInput.disabled = !profileOptions.saveToGitHub;
 	saveWithCompanionInput.checked = profileOptions.saveWithCompanion;
 	saveWithCompanionInput.checked = profileOptions.saveWithCompanion;
-	saveToFilesystemInput.checked = !profileOptions.saveToGDrive && !profileOptions.saveToGitHub && !profileOptions.saveWithCompanion && !profileOptions.saveToClipboard && !profileOptions.saveWithWebDAV;
+	saveToFilesystemInput.checked = !profileOptions.saveToGDrive && !profileOptions.saveToGitHub && !profileOptions.saveWithCompanion && !profileOptions.saveToClipboard && !profileOptions.saveWithWebDAV && !profileOptions.saveToDropbox;
 	compressHTMLInput.checked = profileOptions.compressHTML;
 	compressHTMLInput.checked = profileOptions.compressHTML;
 	compressCSSInput.checked = profileOptions.compressCSS;
 	compressCSSInput.checked = profileOptions.compressCSS;
 	moveStylesInHeadInput.checked = profileOptions.moveStylesInHead;
 	moveStylesInHeadInput.checked = profileOptions.moveStylesInHead;
@@ -1024,6 +1029,7 @@ async function update() {
 			addProof: addProofInput.checked,
 			addProof: addProofInput.checked,
 			woleetKey: woleetKeyInput.value,
 			woleetKey: woleetKeyInput.value,
 			saveToGDrive: saveToGDriveInput.checked,
 			saveToGDrive: saveToGDriveInput.checked,
+			saveToDropbox: saveToDropboxInput.checked,
 			saveWithWebDAV: saveWithWebDAVInput.checked,
 			saveWithWebDAV: saveWithWebDAVInput.checked,
 			webDAVURL: webDAVURLInput.value,
 			webDAVURL: webDAVURLInput.value,
 			webDAVUser: webDAVUserInput.value,
 			webDAVUser: webDAVUserInput.value,
@@ -1190,10 +1196,13 @@ async function onClickSaveToGDrive() {
 	await refresh();
 	await refresh();
 }
 }
 
 
-async function disableDestinationPermissions(permissions, disableGDrive = true) {
+async function disableDestinationPermissions(permissions, disableGDrive = true, disableDropbox = true) {
 	if (disableGDrive) {
 	if (disableGDrive) {
 		await browser.runtime.sendMessage({ method: "downloads.disableGDrive" });
 		await browser.runtime.sendMessage({ method: "downloads.disableGDrive" });
 	}
 	}
+	if (disableDropbox) {
+		await browser.runtime.sendMessage({ method: "downloads.disableDropbox" });
+	}
 	try {
 	try {
 		await browser.permissions.remove({ permissions });
 		await browser.permissions.remove({ permissions });
 	} catch (error) {
 	} catch (error) {

+ 9 - 3
src/ui/pages/help.html

@@ -488,12 +488,18 @@
 						<p>Enter your password.</p>
 						<p>Enter your password.</p>
 					</li>
 					</li>
 					<li data-options-label="saveToGDriveLabel" id="saveToGDriveOption"> <span class="option">Option:
 					<li data-options-label="saveToGDriveLabel" id="saveToGDriveOption"> <span class="option">Option:
-							upload to Google
-							Drive</span>
+							upload to Google Drive</span>
 						<p>Check this option to save the page on Google Drive.</p>
 						<p>Check this option to save the page on Google Drive.</p>
 						<p>The permissions requested by SingleFile allow it to access only to the files and folders it
 						<p>The permissions requested by SingleFile allow it to access only to the files and folders it
 							has created. When you uncheck this option, SingleFile revokes automatically its access to
 							has created. When you uncheck this option, SingleFile revokes automatically its access to
-							your Google Drive account. </p>
+							your Google Drive account.</p>
+						<p class="notice">It is recommended to <u>uncheck</u> this option</p>
+					</li>
+					<li data-options-label="saveToDropboxLabel" id="saveToDropboxOption"> <span class="option">Option:
+							upload to Dropbox</span>
+						<p>Check this option to save the page on Dropbox.</p>
+						<p>The permissions requested by SingleFile allow it to access only to the files and folders it
+							has created.</p>
 						<p class="notice">It is recommended to <u>uncheck</u> this option</p>
 						<p class="notice">It is recommended to <u>uncheck</u> this option</p>
 					</li>
 					</li>
 					<li data-options-label="saveWithCompanionLabel" id="saveWithCompanionOption"> <span
 					<li data-options-label="saveWithCompanionLabel" id="saveWithCompanionOption"> <span

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

@@ -279,6 +279,10 @@
 				<label for="saveToGDriveInput" id="saveToGDriveLabel"></label>
 				<label for="saveToGDriveInput" id="saveToGDriveLabel"></label>
 				<input type="radio" id="saveToGDriveInput" name="destinationInput">
 				<input type="radio" id="saveToGDriveInput" name="destinationInput">
 			</div>
 			</div>
+			<div class="option" id="saveToDropboxOption">
+				<label for="saveToDropboxInput" id="saveToDropboxLabel"></label>
+				<input type="radio" id="saveToDropboxInput" name="destinationInput">
+			</div>
 			<div class="option" id="saveWithCompanionOption">
 			<div class="option" id="saveWithCompanionOption">
 				<label for="saveWithCompanionInput" id="saveWithCompanionLabel"></label>
 				<label for="saveWithCompanionInput" id="saveWithCompanionLabel"></label>
 				<input type="radio" id="saveWithCompanionInput" name="destinationInput">
 				<input type="radio" id="saveWithCompanionInput" name="destinationInput">