[Scummvm-git-logs] scummvm master -> cffd6d79abe613201db64995bf4779edc77c9858

Mataniko mataniko at gmail.com
Tue Jul 30 20:52:06 CEST 2019


This automated email contains information about 30 new commits which have been
pushed to the 'scummvm' repo located at https://github.com/scummvm/scummvm .

Summary:
1bebaf96f7 CLOUD: Regenerate wwwroot archive to include graphic resources
ddcfcc18b2 CLOUD: Update DropboxStorage to work via scummvm.org & StorageWizardDialog correspondingly
16b6588146 CLOUD: Update Dropbox and Google Drive icons in themes
2a56328d35 CLOUD: Update DropboxStorage to work with cloud.scummvm.org
faa19c7bf0 CLOUD: Cleanup a little bit - remove unused config keys usage
d04c1dfad4 COMMON: Add getHumanReadableBytes() in util.h
64fd116092 CLOUD: Add BaseStorage, which does auth via cloud.scummvm.org
8333cce498 CLOUD: Update OneDriveStorage to work via cloud.scummvm.org
0ee0e2d537 CLOUD: Update GoogleDriveStorage and BoxStorage to auth via cloud.scummvm.org
4a427faf9c CLOUD: Get rid of 'dists/clouds/cloud_keys.h'
60504dce75 CLOUD: Update storages to refresh token via cloud.scummvm.org
e8669f693c CLOUD: Update BaseStorage to expect no refresh_token
edbea10c2e CLOUD: Fix OneDriveStorage API interaction
c1124c9cda CLOUD: Handle Cloud requests more frequently
1e92e7b9f3 CLOUD: Fix saves syncing
72c7f8226c CLOUD: Minor cleanup in OneDriveTokenRefresher
31628d6428 CLOUD: Refactor BaseStorage largest methods
99c2418d1a GUI: Rewrite Cloud tab
3df126853a GUI: Separate Cloud tab in two
28c93ed027 GUI: Fix Cloud tab wizard elements displaying after connecting storage
a1b847a1ef GUI: Make Cloud tab wizard input box cleanup value
a9d47a163e GUI: Fix Cloud tab scrolling problem
e4da63823a GUI: Add feature description into Wi-Fi Sharing tab
f7902583bc GUI: Change 'Wi-Fi Sharing' tab name to 'LAN'
16d97b6948 CLOUD: Minor fixes for the PR#1754
7fc6477ce2 COMMON: Update getHumanReadableBytes() in util.h
e7ca2b8db0 CLOUD: Ignore hidden files in sync/download
85431c39bf CLOUD: Change interaction with /refresh endpoint
f6a17e679f CLOUD: Ask user to manually enable Storage
cffd6d79ab GUI: Fix a label in Cloud tab


Commit: 1bebaf96f7ba1b11417fb16b69b7baebb711ae42
    https://github.com/scummvm/scummvm/commit/1bebaf96f7ba1b11417fb16b69b7baebb711ae42
Author: Alexander Tkachev (alexander at tkachov.ru)
Date: 2019-07-30T14:51:41-04:00

Commit Message:
CLOUD: Regenerate wwwroot archive to include graphic resources

Changed paths:
    dists/networking/wwwroot.zip


diff --git a/dists/networking/wwwroot.zip b/dists/networking/wwwroot.zip
index 29fa663..74de46d 100644
Binary files a/dists/networking/wwwroot.zip and b/dists/networking/wwwroot.zip differ


Commit: ddcfcc18b2b4fc17e2217014ad069dc95fbf4fa1
    https://github.com/scummvm/scummvm/commit/ddcfcc18b2b4fc17e2217014ad069dc95fbf4fa1
Author: Alexander Tkachev (alexander at tkachov.ru)
Date: 2019-07-30T14:51:41-04:00

Commit Message:
CLOUD: Update DropboxStorage to work via scummvm.org & StorageWizardDialog correspondingly

Changed paths:
    backends/cloud/cloudmanager.cpp
    backends/cloud/cloudmanager.h
    backends/cloud/dropbox/dropboxstorage.cpp
    backends/cloud/dropbox/dropboxstorage.h
    backends/cloud/googledrive/googledrivestorage.cpp
    backends/cloud/onedrive/onedrivestorage.cpp
    gui/storagewizarddialog.cpp


diff --git a/backends/cloud/cloudmanager.cpp b/backends/cloud/cloudmanager.cpp
index 5519e4b..20c2793 100644
--- a/backends/cloud/cloudmanager.cpp
+++ b/backends/cloud/cloudmanager.cpp
@@ -336,14 +336,6 @@ bool CloudManager::isWorking() const {
 	return false;
 }
 
-bool CloudManager::couldUseLocalServer() {
-#ifdef USE_SDL_NET
-	return Networking::LocalWebserver::getPort() == Networking::LocalWebserver::DEFAULT_SERVER_PORT;
-#else
-	return false;
-#endif
-}
-
 ///// SavesSyncRequest-related /////
 
 bool CloudManager::isSyncing() const {
diff --git a/backends/cloud/cloudmanager.h b/backends/cloud/cloudmanager.h
index f58ea83..eb882a6 100644
--- a/backends/cloud/cloudmanager.h
+++ b/backends/cloud/cloudmanager.h
@@ -227,9 +227,6 @@ public:
 	/** Returns whether there are any requests running. */
 	bool isWorking() const;
 
-	/** Returns whether LocalWebserver is available to use for auth. */
-	static bool couldUseLocalServer();
-
 	///// SavesSyncRequest-related /////
 
 	/** Returns whether there is a SavesSyncRequest running. */
diff --git a/backends/cloud/dropbox/dropboxstorage.cpp b/backends/cloud/dropbox/dropboxstorage.cpp
index b856c6c..bf964ae 100644
--- a/backends/cloud/dropbox/dropboxstorage.cpp
+++ b/backends/cloud/dropbox/dropboxstorage.cpp
@@ -42,29 +42,9 @@
 namespace Cloud {
 namespace Dropbox {
 
-#define DROPBOX_OAUTH2_TOKEN "https://api.dropboxapi.com/oauth2/token"
+#define DROPBOX_OAUTH2_TOKEN "https://scummvm.org/admin/cloud/cloud/dropbox/token/"
 #define DROPBOX_API_FILES_DOWNLOAD "https://content.dropboxapi.com/2/files/download"
 
-char *DropboxStorage::KEY = nullptr; //can't use CloudConfig there yet, loading it on instance creation/auth
-char *DropboxStorage::SECRET = nullptr;
-
-void DropboxStorage::loadKeyAndSecret() {
-#ifdef ENABLE_RELEASE
-	KEY = RELEASE_DROPBOX_KEY;
-	SECRET = RELEASE_DROPBOX_SECRET;
-#else
-	Common::String k = ConfMan.get("DROPBOX_KEY", ConfMan.kCloudDomain);
-	KEY = new char[k.size() + 1];
-	memcpy(KEY, k.c_str(), k.size());
-	KEY[k.size()] = 0;
-
-	k = ConfMan.get("DROPBOX_SECRET", ConfMan.kCloudDomain);
-	SECRET = new char[k.size() + 1];
-	memcpy(SECRET, k.c_str(), k.size());
-	SECRET[k.size()] = 0;
-#endif
-}
-
 DropboxStorage::DropboxStorage(Common::String accessToken, Common::String userId): _token(accessToken), _uid(userId) {}
 
 DropboxStorage::DropboxStorage(Common::String code) {
@@ -74,20 +54,12 @@ DropboxStorage::DropboxStorage(Common::String code) {
 DropboxStorage::~DropboxStorage() {}
 
 void DropboxStorage::getAccessToken(Common::String code) {
-	if (!KEY || !SECRET)
-		loadKeyAndSecret();
 	Networking::JsonCallback callback = new Common::Callback<DropboxStorage, Networking::JsonResponse>(this, &DropboxStorage::codeFlowComplete);
 	Networking::ErrorCallback errorCallback = new Common::Callback<DropboxStorage, Networking::ErrorResponse>(this, &DropboxStorage::codeFlowFailed);
-	Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(callback, errorCallback, DROPBOX_OAUTH2_TOKEN);
-	request->addPostField("code=" + code);
-	request->addPostField("grant_type=authorization_code");
-	request->addPostField("client_id=" + Common::String(KEY));
-	request->addPostField("client_secret=" + Common::String(SECRET));
-	if (Cloud::CloudManager::couldUseLocalServer()) {
-		request->addPostField("&redirect_uri=http%3A%2F%2Flocalhost%3A12345%2F");
-	} else {
-		request->addPostField("&redirect_uri=https%3A%2F%2Fwww.scummvm.org/c/code");
-	}
+
+	Common::String url = Common::String(DROPBOX_OAUTH2_TOKEN) + code;
+	Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(callback, errorCallback, url);
+
 	addRequest(request);
 }
 
@@ -177,8 +149,6 @@ Networking::Request *DropboxStorage::info(StorageInfoCallback callback, Networki
 Common::String DropboxStorage::savesDirectoryPath() { return "/saves/"; }
 
 DropboxStorage *DropboxStorage::loadFromConfig(Common::String keyPrefix) {
-	loadKeyAndSecret();
-
 	if (!ConfMan.hasKey(keyPrefix + "access_token", ConfMan.kCloudDomain)) {
 		warning("DropboxStorage: no access_token found");
 		return nullptr;
diff --git a/backends/cloud/dropbox/dropboxstorage.h b/backends/cloud/dropbox/dropboxstorage.h
index 44fb8a3..eec34d8 100644
--- a/backends/cloud/dropbox/dropboxstorage.h
+++ b/backends/cloud/dropbox/dropboxstorage.h
@@ -31,10 +31,6 @@ namespace Cloud {
 namespace Dropbox {
 
 class DropboxStorage: public Cloud::Storage {
-	static char *KEY, *SECRET;
-
-	static void loadKeyAndSecret();
-
 	Common::String _token, _uid;
 
 	/** This private constructor is called from loadFromConfig(). */
diff --git a/backends/cloud/googledrive/googledrivestorage.cpp b/backends/cloud/googledrive/googledrivestorage.cpp
index 4a870c0..8bd9228 100644
--- a/backends/cloud/googledrive/googledrivestorage.cpp
+++ b/backends/cloud/googledrive/googledrivestorage.cpp
@@ -105,11 +105,7 @@ void GoogleDriveStorage::getAccessToken(BoolCallback callback, Networking::Error
 	}
 	request->addPostField("client_id=" + Common::String(KEY));
 	request->addPostField("client_secret=" + Common::String(SECRET));
-	if (Cloud::CloudManager::couldUseLocalServer()) {
-		request->addPostField("&redirect_uri=http%3A%2F%2Flocalhost%3A12345");
-	} else {
-		request->addPostField("&redirect_uri=https%3A%2F%2Fwww.scummvm.org/c/code");
-	}
+	request->addPostField("&redirect_uri=https%3A%2F%2Fwww.scummvm.org/c/code");
 	addRequest(request);
 }
 
diff --git a/backends/cloud/onedrive/onedrivestorage.cpp b/backends/cloud/onedrive/onedrivestorage.cpp
index 9f7cad2..396370e 100644
--- a/backends/cloud/onedrive/onedrivestorage.cpp
+++ b/backends/cloud/onedrive/onedrivestorage.cpp
@@ -105,11 +105,7 @@ void OneDriveStorage::getAccessToken(BoolCallback callback, Networking::ErrorCal
 	}
 	request->addPostField("client_id=" + Common::String(KEY));
 	request->addPostField("client_secret=" + Common::String(SECRET));
-	if (Cloud::CloudManager::couldUseLocalServer()) {
-		request->addPostField("&redirect_uri=http%3A%2F%2Flocalhost%3A12345%2F");
-	} else {
-		request->addPostField("&redirect_uri=https%3A%2F%2Fwww.scummvm.org/c/code");
-	}
+	request->addPostField("&redirect_uri=https%3A%2F%2Fwww.scummvm.org/c/code");
 	addRequest(request);
 }
 
diff --git a/gui/storagewizarddialog.cpp b/gui/storagewizarddialog.cpp
index fe5a109..0a50f8e 100644
--- a/gui/storagewizarddialog.cpp
+++ b/gui/storagewizarddialog.cpp
@@ -73,12 +73,6 @@ StorageWizardDialog::StorageWizardDialog(uint32 storageId):
 	// Initialy the code is empty, so disable the connect button
 	_connectWidget->setEnabled(false);
 
-	if (Cloud::CloudManager::couldUseLocalServer()) {
-		// hide fields and even the button if local webserver is on
-		_returnLine1->setLabel(_("You will be directed to ScummVM's page where"));
-		_returnLine2->setLabel(_("you should allow it to access your storage."));
-	}
-
 	_picture = new GraphicsWidget(container, "GlobalOptions_Cloud_ConnectionWizard_Container.Picture");
 #ifndef DISABLE_FANCY_THEMES
 	if (g_gui.theme()->supportsImages()) {
@@ -130,24 +124,9 @@ void StorageWizardDialog::open() {
 			return;
 		}
 	}
-
-#ifdef USE_SDL_NET
-	if (Cloud::CloudManager::couldUseLocalServer()) {
-		_stopServerOnClose = !LocalServer.isRunning();
-		LocalServer.start(true); // using "minimal mode" (no "/files", "/download", etc available)
-		LocalServer.indexPageHandler().setTarget(this);
-	}
-#endif
 }
 
 void StorageWizardDialog::close() {
-#ifdef USE_SDL_NET
-	if (Cloud::CloudManager::couldUseLocalServer()) {
-		if (_stopServerOnClose)
-			LocalServer.stopOnIdle();
-		LocalServer.indexPageHandler().setTarget(nullptr);
-	}
-#endif
 	Dialog::close();
 }
 
@@ -155,6 +134,19 @@ void StorageWizardDialog::handleCommand(CommandSender *sender, uint32 cmd, uint3
 	switch (cmd) {
 	case kCodeBoxCmd: {
 		Common::String code, message;
+
+		if (_storageId == Cloud::kStorageDropboxId) {
+			// new handling
+			code = _codeWidget[0]->getEditString();
+
+			bool ok = (code.size() > 0);
+			message = ""; // (ok ? _("All OK!") : "");
+
+			_connectWidget->setEnabled(ok);
+			_messageWidget->setLabel(message);
+			return;
+		}
+		
 		uint32 correctFields = 0;
 		for (uint32 i = 0; i < CODE_FIELDS; ++i) {
 			Common::String subcode = _codeWidget[i]->getEditString();
@@ -236,6 +228,18 @@ void StorageWizardDialog::handleCommand(CommandSender *sender, uint32 cmd, uint3
 		break;
 	}
 	case kConnectCmd: {
+		if (_storageId == Cloud::kStorageDropboxId) {
+			// new handling
+			Common::String code = _codeWidget[0]->getEditString();
+			if (code.size() == 0)
+				return;
+
+			CloudMan.connectStorage(_storageId, code);
+			setResult(1);
+			close();
+			return;
+		}
+
 		Common::String code;
 		for (uint32 i = 0; i < CODE_FIELDS; ++i) {
 			Common::String subcode = _codeWidget[i]->getEditString();
@@ -283,9 +287,9 @@ void StorageWizardDialog::containerWidgetsReflow() {
 	if (_returnLine1) _returnLine1->setVisible(true);
 	if (_returnLine2) _returnLine2->setVisible(true);
 
-	bool showFields = (!Cloud::CloudManager::couldUseLocalServer());
+	bool showFields = true; // TODO: remove this const
 	for (uint32 i = 0; i < CODE_FIELDS; ++i)
-		_codeWidget[i]->setVisible(showFields);
+		_codeWidget[i]->setVisible(showFields && (_storageId != Cloud::kStorageDropboxId || i < 1)); // show only one field for Dropbox
 	_messageWidget->setVisible(showFields);
 
 	// left column / first bottom row
@@ -312,7 +316,7 @@ Common::String StorageWizardDialog::getUrl() const {
 	Common::String url = "https://www.scummvm.org/c/";
 	switch (_storageId) {
 	case Cloud::kStorageDropboxId:
-		url += "db";
+		url = "https://cloud.scummvm.org/";
 		break;
 	case Cloud::kStorageOneDriveId:
 		url += "od";
@@ -325,9 +329,6 @@ Common::String StorageWizardDialog::getUrl() const {
 		break;
 	}
 
-	if (Cloud::CloudManager::couldUseLocalServer())
-		url += "s";
-
 	return url;
 }
 


Commit: 16b658814622b69163def3254e51556412b7e9fb
    https://github.com/scummvm/scummvm/commit/16b658814622b69163def3254e51556412b7e9fb
Author: Alexander Tkachev (alexander at tkachov.ru)
Date: 2019-07-30T14:51:41-04:00

Commit Message:
CLOUD: Update Dropbox and Google Drive icons in themes

Changed paths:
    gui/themes/scummmodern.zip
    gui/themes/scummmodern/dropbox.bmp
    gui/themes/scummmodern/googledrive.bmp
    gui/themes/scummremastered.zip
    gui/themes/scummremastered/dropbox.bmp
    gui/themes/scummremastered/googledrive.bmp


diff --git a/gui/themes/scummmodern.zip b/gui/themes/scummmodern.zip
index 686716c..1a5fd5c 100644
Binary files a/gui/themes/scummmodern.zip and b/gui/themes/scummmodern.zip differ
diff --git a/gui/themes/scummmodern/dropbox.bmp b/gui/themes/scummmodern/dropbox.bmp
index 4ed95f0..bfe6207 100644
Binary files a/gui/themes/scummmodern/dropbox.bmp and b/gui/themes/scummmodern/dropbox.bmp differ
diff --git a/gui/themes/scummmodern/googledrive.bmp b/gui/themes/scummmodern/googledrive.bmp
index 30377a5..f79a0e7 100644
Binary files a/gui/themes/scummmodern/googledrive.bmp and b/gui/themes/scummmodern/googledrive.bmp differ
diff --git a/gui/themes/scummremastered.zip b/gui/themes/scummremastered.zip
index e60ea04..6d235b4 100644
Binary files a/gui/themes/scummremastered.zip and b/gui/themes/scummremastered.zip differ
diff --git a/gui/themes/scummremastered/dropbox.bmp b/gui/themes/scummremastered/dropbox.bmp
index 543c4d6..bfe6207 100644
Binary files a/gui/themes/scummremastered/dropbox.bmp and b/gui/themes/scummremastered/dropbox.bmp differ
diff --git a/gui/themes/scummremastered/googledrive.bmp b/gui/themes/scummremastered/googledrive.bmp
index d331cc3..f79a0e7 100644
Binary files a/gui/themes/scummremastered/googledrive.bmp and b/gui/themes/scummremastered/googledrive.bmp differ


Commit: 2a56328d358307ae629c44d10e1f4a2ea8198672
    https://github.com/scummvm/scummvm/commit/2a56328d358307ae629c44d10e1f4a2ea8198672
Author: Alexander Tkachev (alexander at tkachov.ru)
Date: 2019-07-30T14:51:41-04:00

Commit Message:
CLOUD: Update DropboxStorage to work with cloud.scummvm.org

Changed paths:
    backends/cloud/dropbox/dropboxstorage.cpp
    backends/cloud/dropbox/dropboxstorage.h


diff --git a/backends/cloud/dropbox/dropboxstorage.cpp b/backends/cloud/dropbox/dropboxstorage.cpp
index bf964ae..44ad9a1 100644
--- a/backends/cloud/dropbox/dropboxstorage.cpp
+++ b/backends/cloud/dropbox/dropboxstorage.cpp
@@ -42,10 +42,10 @@
 namespace Cloud {
 namespace Dropbox {
 
-#define DROPBOX_OAUTH2_TOKEN "https://scummvm.org/admin/cloud/cloud/dropbox/token/"
+#define DROPBOX_OAUTH2_TOKEN "https://cloud.scummvm.org/dropbox/token/"
 #define DROPBOX_API_FILES_DOWNLOAD "https://content.dropboxapi.com/2/files/download"
 
-DropboxStorage::DropboxStorage(Common::String accessToken, Common::String userId): _token(accessToken), _uid(userId) {}
+DropboxStorage::DropboxStorage(Common::String accessToken, bool unused): _token(accessToken) {}
 
 DropboxStorage::DropboxStorage(Common::String code) {
 	getAccessToken(code);
@@ -79,14 +79,12 @@ void DropboxStorage::codeFlowComplete(Networking::JsonResponse response) {
 	}
 
 	Common::JSONObject result = json->asObject();
-	if (!Networking::CurlJsonRequest::jsonContainsString(result, "access_token", "DropboxStorage::codeFlowComplete") ||
-		!Networking::CurlJsonRequest::jsonContainsString(result, "uid", "DropboxStorage::codeFlowComplete")) {
-		warning("DropboxStorage: bad response, no token/uid passed");
+	if (!Networking::CurlJsonRequest::jsonContainsString(result, "access_token", "DropboxStorage::codeFlowComplete")) {
+		warning("DropboxStorage: bad response, no token passed");
 		debug(9, "%s", json->stringify(true).c_str());
 		CloudMan.removeStorage(this);
 	} else {
 		_token = result.getVal("access_token")->asString();
-		_uid = result.getVal("uid")->asString();
 		ConfMan.removeKey("dropbox_code", ConfMan.kCloudDomain);
 		CloudMan.replaceStorage(this, kStorageDropboxId);
 		ConfMan.flushToDisk();
@@ -103,7 +101,6 @@ void DropboxStorage::codeFlowFailed(Networking::ErrorResponse error) {
 
 void DropboxStorage::saveConfig(Common::String keyPrefix) {
 	ConfMan.set(keyPrefix + "access_token", _token, ConfMan.kCloudDomain);
-	ConfMan.set(keyPrefix + "user_id", _uid, ConfMan.kCloudDomain);
 }
 
 Common::String DropboxStorage::name() const {
@@ -154,15 +151,8 @@ DropboxStorage *DropboxStorage::loadFromConfig(Common::String keyPrefix) {
 		return nullptr;
 	}
 
-	if (!ConfMan.hasKey(keyPrefix + "user_id", ConfMan.kCloudDomain)) {
-		warning("DropboxStorage: no user_id found");
-		return nullptr;
-	}
-
 	Common::String accessToken = ConfMan.get(keyPrefix + "access_token", ConfMan.kCloudDomain);
-	Common::String userId = ConfMan.get(keyPrefix + "user_id", ConfMan.kCloudDomain);
-
-	return new DropboxStorage(accessToken, userId);
+	return new DropboxStorage(accessToken, true);
 }
 
 } // End of namespace Dropbox
diff --git a/backends/cloud/dropbox/dropboxstorage.h b/backends/cloud/dropbox/dropboxstorage.h
index eec34d8..7505edd 100644
--- a/backends/cloud/dropbox/dropboxstorage.h
+++ b/backends/cloud/dropbox/dropboxstorage.h
@@ -31,10 +31,10 @@ namespace Cloud {
 namespace Dropbox {
 
 class DropboxStorage: public Cloud::Storage {
-	Common::String _token, _uid;
+	Common::String _token;
 
 	/** This private constructor is called from loadFromConfig(). */
-	DropboxStorage(Common::String token, Common::String uid);
+	DropboxStorage(Common::String token, bool unused);
 
 	void getAccessToken(Common::String code);
 	void codeFlowComplete(Networking::JsonResponse response);


Commit: faa19c7bf097e6a7f1f76c29b0faba12d5781d19
    https://github.com/scummvm/scummvm/commit/faa19c7bf097e6a7f1f76c29b0faba12d5781d19
Author: Alexander Tkachev (alexander at tkachov.ru)
Date: 2019-07-30T14:51:41-04:00

Commit Message:
CLOUD: Cleanup a little bit - remove unused config keys usage

Changed paths:
    backends/cloud/dropbox/dropboxstorage.cpp
    backends/cloud/googledrive/googledrivestorage.cpp
    backends/cloud/onedrive/onedrivestorage.cpp


diff --git a/backends/cloud/dropbox/dropboxstorage.cpp b/backends/cloud/dropbox/dropboxstorage.cpp
index 44ad9a1..63c21fb 100644
--- a/backends/cloud/dropbox/dropboxstorage.cpp
+++ b/backends/cloud/dropbox/dropboxstorage.cpp
@@ -85,7 +85,6 @@ void DropboxStorage::codeFlowComplete(Networking::JsonResponse response) {
 		CloudMan.removeStorage(this);
 	} else {
 		_token = result.getVal("access_token")->asString();
-		ConfMan.removeKey("dropbox_code", ConfMan.kCloudDomain);
 		CloudMan.replaceStorage(this, kStorageDropboxId);
 		ConfMan.flushToDisk();
 	}
diff --git a/backends/cloud/googledrive/googledrivestorage.cpp b/backends/cloud/googledrive/googledrivestorage.cpp
index 8bd9228..51799eb 100644
--- a/backends/cloud/googledrive/googledrivestorage.cpp
+++ b/backends/cloud/googledrive/googledrivestorage.cpp
@@ -154,7 +154,6 @@ void GoogleDriveStorage::codeFlowComplete(BoolResponse response) {
 		return;
 	}
 
-	ConfMan.removeKey("googledrive_code", ConfMan.kCloudDomain);
 	CloudMan.replaceStorage(this, kStorageGoogleDriveId);
 	ConfMan.flushToDisk();
 }
diff --git a/backends/cloud/onedrive/onedrivestorage.cpp b/backends/cloud/onedrive/onedrivestorage.cpp
index 396370e..af8c70f8 100644
--- a/backends/cloud/onedrive/onedrivestorage.cpp
+++ b/backends/cloud/onedrive/onedrivestorage.cpp
@@ -154,7 +154,6 @@ void OneDriveStorage::codeFlowComplete(BoolResponse response) {
 		return;
 	}
 
-	ConfMan.removeKey("onedrive_code", ConfMan.kCloudDomain);
 	CloudMan.replaceStorage(this, kStorageOneDriveId);
 	ConfMan.flushToDisk();
 }


Commit: d04c1dfad422d9ea8c2b460ff01b88911d9fd3ef
    https://github.com/scummvm/scummvm/commit/d04c1dfad422d9ea8c2b460ff01b88911d9fd3ef
Author: Alexander Tkachev (alexander at tkachov.ru)
Date: 2019-07-30T14:51:41-04:00

Commit Message:
COMMON: Add getHumanReadableBytes() in util.h

This function was used in cloud-related DownloadDialog before,
and now it is also used in Options > Cloud tab.

Changed paths:
    common/util.cpp
    common/util.h
    gui/downloaddialog.cpp
    gui/options.cpp


diff --git a/common/util.cpp b/common/util.cpp
index 9a4214e..7c309ce 100644
--- a/common/util.cpp
+++ b/common/util.cpp
@@ -163,4 +163,43 @@ bool isGraph(int c) {
 	return isgraph((byte)c);
 }
 
+
+#pragma mark -
+
+
+Common::String getHumanReadableBytes(uint64 bytes, Common::String &unitsOut) {
+	Common::String result = Common::String::format("%lu", bytes);
+	unitsOut = "B";
+
+	if (bytes >= 1024) {
+		bytes /= 1024;
+		result = Common::String::format("%lu", bytes);
+		unitsOut = "KB";
+	}
+
+	double floating = bytes;
+
+	if (bytes >= 1024) {
+		bytes /= 1024;
+		floating /= 1024.0;
+		unitsOut = "MB";
+	}
+
+	if (bytes >= 1024) {
+		bytes /= 1024;
+		floating /= 1024.0;
+		unitsOut = "GB";
+	}
+
+	if (bytes >= 1024) { // woah
+		bytes /= 1024;
+		floating /= 1024.0;
+		unitsOut = "TB";
+	}
+
+	// print one digit after floating point
+	result = Common::String::format("%.1f", floating);
+	return result;
+}
+
 } // End of namespace Common
diff --git a/common/util.h b/common/util.h
index a90ea721..8254e97 100644
--- a/common/util.h
+++ b/common/util.h
@@ -220,6 +220,18 @@ bool isCntrl(int c);
  */
 bool isGraph(int c);
 
+
+/**
+ * Represent bytes size of a file as a number with floating point and
+ * largest suitable units. For example, 1474560 bytes as 1.4 MB.
+ * 
+ * @param bytes		size in bytes to be represented
+ * @param unitsOut	(out-parameter) string with units
+ * @note			use _() to translate units correctly
+ * @return			string with a floating point number representing given size
+ */
+Common::String getHumanReadableBytes(uint64 bytes, Common::String &unitsOut);
+
 } // End of namespace Common
 
 #endif
diff --git a/gui/downloaddialog.cpp b/gui/downloaddialog.cpp
index 526a895..8b7408c 100644
--- a/gui/downloaddialog.cpp
+++ b/gui/downloaddialog.cpp
@@ -24,6 +24,7 @@
 #include "backends/cloud/cloudmanager.h"
 #include "common/config-manager.h"
 #include "common/translation.h"
+#include "common/util.h"
 #include "engines/metaengine.h"
 #include "gui/browser.h"
 #include "gui/chooser.h"
@@ -207,43 +208,6 @@ void DownloadDialog::reflowLayout() {
 	refreshWidgets();
 }
 
-namespace {
-Common::String getHumanReadableBytes(uint64 bytes, Common::String &unitsOut) {
-	Common::String result = Common::String::format("%lu", bytes);
-	unitsOut = "B";
-
-	if (bytes >= 1024) {
-		bytes /= 1024;
-		result = Common::String::format("%lu", bytes);
-		unitsOut = "KB";
-	}
-
-	double floating = bytes;
-
-	if (bytes >= 1024) {
-		bytes /= 1024;
-		floating /= 1024.0;
-		unitsOut = "MB";
-	}
-
-	if (bytes >= 1024) {
-		bytes /= 1024;
-		floating /= 1024.0;
-		unitsOut = "GB";
-	}
-
-	if (bytes >= 1024) { // woah
-		bytes /= 1024;
-		floating /= 1024.0;
-		unitsOut = "TB";
-	}
-
-	// print one digit after floating point
-	result = Common::String::format("%.1f", floating);
-	return result;
-}
-}
-
 Common::String DownloadDialog::getSizeLabelText() {
 	Common::String downloaded, downloadedUnits, total, totalUnits;
 	downloaded = getHumanReadableBytes(CloudMan.getDownloadBytesNumber(), downloadedUnits);
diff --git a/gui/options.cpp b/gui/options.cpp
index 8d2bda5..5d90b70 100644
--- a/gui/options.cpp
+++ b/gui/options.cpp
@@ -38,6 +38,7 @@
 #include "common/textconsole.h"
 #include "common/translation.h"
 #include "common/updates.h"
+#include "common/util.h"
 
 #include "audio/mididrv.h"
 #include "audio/musicplugin.h"
@@ -2340,7 +2341,9 @@ void GlobalOptionsDialog::setupCloudTab() {
 	if (_storageUsedSpaceDesc) _storageUsedSpaceDesc->setVisible(shown);
 	if (_storageUsedSpace) {
 		uint64 usedSpace = CloudMan.getStorageUsedSpace(_selectedStorageIndex);
-		_storageUsedSpace->setLabel(Common::String::format(_("%llu bytes"), usedSpace));
+		Common::String usedSpaceNumber, usedSpaceUnits;
+		usedSpaceNumber = Common::getHumanReadableBytes(usedSpace, usedSpaceUnits);
+		_storageUsedSpace->setLabel(Common::String::format("%s %s", usedSpaceNumber.c_str(), _(usedSpaceUnits.c_str())));
 		_storageUsedSpace->setVisible(shown);
 	}
 	if (_storageLastSyncDesc) _storageLastSyncDesc->setVisible(shown);


Commit: 64fd11609205bb7f1082975d2e2452a6f6bc7194
    https://github.com/scummvm/scummvm/commit/64fd11609205bb7f1082975d2e2452a6f6bc7194
Author: Alexander Tkachev (alexander at tkachov.ru)
Date: 2019-07-30T14:51:41-04:00

Commit Message:
CLOUD: Add BaseStorage, which does auth via cloud.scummvm.org

Changed paths:
  A backends/cloud/basestorage.cpp
  A backends/cloud/basestorage.h
    backends/cloud/dropbox/dropboxstorage.cpp
    backends/cloud/dropbox/dropboxstorage.h
    backends/module.mk


diff --git a/backends/cloud/basestorage.cpp b/backends/cloud/basestorage.cpp
new file mode 100644
index 0000000..9b31b4c
--- /dev/null
+++ b/backends/cloud/basestorage.cpp
@@ -0,0 +1,104 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ */
+
+#include "backends/cloud/basestorage.h"
+#include "backends/cloud/cloudmanager.h"
+#include "backends/networking/curl/connectionmanager.h"
+#include "backends/networking/curl/curljsonrequest.h"
+#include "common/config-manager.h"
+#include "common/debug.h"
+#include "common/json.h"
+
+namespace Cloud {
+
+BaseStorage::BaseStorage() {}
+
+BaseStorage::BaseStorage(Common::String token, Common::String refreshToken):
+	_token(token), _refreshToken(refreshToken) {}
+
+BaseStorage::~BaseStorage() {}
+
+void BaseStorage::getAccessToken(Common::String code) {
+	Networking::JsonCallback callback = new Common::Callback<BaseStorage, Networking::JsonResponse>(this, &BaseStorage::codeFlowComplete);
+	Networking::ErrorCallback errorCallback = new Common::Callback<BaseStorage, Networking::ErrorResponse>(this, &BaseStorage::codeFlowFailed);
+
+	Common::String url = Common::String::format("https://cloud.scummvm.org/%s/%s", cloudProvider().c_str(), code.c_str());
+	Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(callback, errorCallback, url);
+
+	addRequest(request);
+}
+
+void BaseStorage::codeFlowComplete(Networking::JsonResponse response) {
+	Common::JSONValue *json = (Common::JSONValue *)response.value;
+	if (json == nullptr) {
+		debug(9, "BaseStorage::codeFlowComplete: got NULL instead of JSON!");
+		CloudMan.removeStorage(this);
+		return;
+	}
+
+	if (!json->isObject()) {
+		debug(9, "BaseStorage::codeFlowComplete: passed JSON is not an object!");
+		CloudMan.removeStorage(this);
+		delete json;
+		return;
+	}
+
+	Common::JSONObject result = json->asObject();
+	if (!Networking::CurlJsonRequest::jsonContainsAttribute(result, "error", "BaseStorage::codeFlowComplete")) {
+		warning("BaseStorage: bad response, no 'error' attribute passed");
+		debug(9, "%s", json->stringify(true).c_str());
+		CloudMan.removeStorage(this);
+		delete json;
+		return;
+	}
+
+	if (result.getVal("error")->asBool()) {
+		Common::String errorMessage = "{error: true}, message is missing";
+		if (Networking::CurlJsonRequest::jsonContainsString(result, "message", "BaseStorage::codeFlowComplete")) {
+			errorMessage = result.getVal("message")->asString();
+		}
+		warning("BaseStorage: response says error occurred: %s", errorMessage.c_str());
+		CloudMan.removeStorage(this);
+		delete json;
+		return;
+	}
+
+	if (!Networking::CurlJsonRequest::jsonContainsString(result, "access_token", "BaseStorage::codeFlowComplete")) {
+		warning("BaseStorage: bad response, no 'access_token' attribute passed");
+		debug(9, "%s", json->stringify(true).c_str());
+		CloudMan.removeStorage(this);
+	} else {
+		_token = result.getVal("access_token")->asString();
+		CloudMan.replaceStorage(this, storageIndex());
+		ConfMan.flushToDisk();
+	}
+
+	delete json;
+}
+
+void BaseStorage::codeFlowFailed(Networking::ErrorResponse error) {
+	debug(9, "BaseStorage: code flow failed (%s, %ld):", (error.failed ? "failed" : "interrupted"), error.httpResponseCode);
+	debug(9, "%s", error.response.c_str());
+	CloudMan.removeStorage(this);
+}
+
+} // End of namespace Cloud
diff --git a/backends/cloud/basestorage.h b/backends/cloud/basestorage.h
new file mode 100644
index 0000000..cca6e1b
--- /dev/null
+++ b/backends/cloud/basestorage.h
@@ -0,0 +1,75 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ */
+
+#ifndef BACKENDS_CLOUD_BASE_STORAGE_H
+#define BACKENDS_CLOUD_BASE_STORAGE_H
+
+#include "backends/cloud/storage.h"
+#include "backends/networking/curl/curljsonrequest.h"
+
+namespace Cloud {
+
+class BaseStorage: public Cloud::Storage {
+protected:
+	/** Storage's access and refresh tokens. */
+	Common::String _token, _refreshToken;
+
+	/**
+	 * Gets token from cloud.scummvm.org using given code.
+	 * Base implementation for storages with common auth procedure.
+	 */
+	virtual void getAccessToken(Common::String code);
+
+	/**
+	 * Handles JSON response which should contain access token requested 
+	 * with getAccessToken().
+	 */
+	virtual void codeFlowComplete(Networking::JsonResponse response);
+
+	/**
+	 * Handles network errors occurred while getting access token requested
+	 * with getAccessToken().
+	 */
+	virtual void codeFlowFailed(Networking::ErrorResponse error);
+
+	/**
+	 * Return cloud provider name, used in cloud.scummvm.org endpoints.
+	 * @return cloud provider (for example, "dropbox").
+	 */
+	virtual Common::String cloudProvider() = 0;
+
+	/**
+	 * Return CloudManager's StorageID for this storage.
+	 * @return StorageID corresponding to this storage (for example,
+	 *         kStorageDropboxId).
+	 */
+	virtual uint32 storageIndex() = 0;
+
+public:
+	BaseStorage();
+	BaseStorage(Common::String token, Common::String refreshToken);
+	virtual ~BaseStorage();
+};
+
+} // End of namespace Cloud
+
+#endif
diff --git a/backends/cloud/dropbox/dropboxstorage.cpp b/backends/cloud/dropbox/dropboxstorage.cpp
index 63c21fb..53ed704 100644
--- a/backends/cloud/dropbox/dropboxstorage.cpp
+++ b/backends/cloud/dropbox/dropboxstorage.cpp
@@ -42,61 +42,19 @@
 namespace Cloud {
 namespace Dropbox {
 
-#define DROPBOX_OAUTH2_TOKEN "https://cloud.scummvm.org/dropbox/token/"
 #define DROPBOX_API_FILES_DOWNLOAD "https://content.dropboxapi.com/2/files/download"
 
-DropboxStorage::DropboxStorage(Common::String accessToken, bool unused): _token(accessToken) {}
+DropboxStorage::DropboxStorage(Common::String accessToken, bool unused): BaseStorage(accessToken, "") {}
 
-DropboxStorage::DropboxStorage(Common::String code) {
+DropboxStorage::DropboxStorage(Common::String code): BaseStorage() {
 	getAccessToken(code);
 }
 
 DropboxStorage::~DropboxStorage() {}
 
-void DropboxStorage::getAccessToken(Common::String code) {
-	Networking::JsonCallback callback = new Common::Callback<DropboxStorage, Networking::JsonResponse>(this, &DropboxStorage::codeFlowComplete);
-	Networking::ErrorCallback errorCallback = new Common::Callback<DropboxStorage, Networking::ErrorResponse>(this, &DropboxStorage::codeFlowFailed);
+Common::String DropboxStorage::cloudProvider() { return "dropbox"; }
 
-	Common::String url = Common::String(DROPBOX_OAUTH2_TOKEN) + code;
-	Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(callback, errorCallback, url);
-
-	addRequest(request);
-}
-
-void DropboxStorage::codeFlowComplete(Networking::JsonResponse response) {
-	Common::JSONValue *json = (Common::JSONValue *)response.value;
-	if (json == nullptr) {
-		debug(9, "DropboxStorage::codeFlowComplete: got NULL instead of JSON!");
-		CloudMan.removeStorage(this);
-		return;
-	}
-
-	if (!json->isObject()) {
-		debug(9, "DropboxStorage::codeFlowComplete: Passed JSON is not an object!");
-		CloudMan.removeStorage(this);
-		delete json;
-		return;
-	}
-
-	Common::JSONObject result = json->asObject();
-	if (!Networking::CurlJsonRequest::jsonContainsString(result, "access_token", "DropboxStorage::codeFlowComplete")) {
-		warning("DropboxStorage: bad response, no token passed");
-		debug(9, "%s", json->stringify(true).c_str());
-		CloudMan.removeStorage(this);
-	} else {
-		_token = result.getVal("access_token")->asString();
-		CloudMan.replaceStorage(this, kStorageDropboxId);
-		ConfMan.flushToDisk();
-	}
-
-	delete json;
-}
-
-void DropboxStorage::codeFlowFailed(Networking::ErrorResponse error) {
-	debug(9, "DropboxStorage: code flow failed (%s, %ld):", (error.failed ? "failed" : "interrupted"), error.httpResponseCode);
-	debug(9, "%s", error.response.c_str());
-	CloudMan.removeStorage(this);
-}
+uint32 DropboxStorage::storageIndex() { return kStorageDropboxId; }
 
 void DropboxStorage::saveConfig(Common::String keyPrefix) {
 	ConfMan.set(keyPrefix + "access_token", _token, ConfMan.kCloudDomain);
diff --git a/backends/cloud/dropbox/dropboxstorage.h b/backends/cloud/dropbox/dropboxstorage.h
index 7505edd..b82e38e 100644
--- a/backends/cloud/dropbox/dropboxstorage.h
+++ b/backends/cloud/dropbox/dropboxstorage.h
@@ -23,22 +23,27 @@
 #ifndef BACKENDS_CLOUD_DROPBOX_STORAGE_H
 #define BACKENDS_CLOUD_DROPBOX_STORAGE_H
 
-#include "backends/cloud/storage.h"
+#include "backends/cloud/basestorage.h"
 #include "common/callback.h"
 #include "backends/networking/curl/curljsonrequest.h"
 
 namespace Cloud {
 namespace Dropbox {
 
-class DropboxStorage: public Cloud::Storage {
-	Common::String _token;
-
+class DropboxStorage: public Cloud::BaseStorage {
 	/** This private constructor is called from loadFromConfig(). */
 	DropboxStorage(Common::String token, bool unused);
 
-	void getAccessToken(Common::String code);
-	void codeFlowComplete(Networking::JsonResponse response);
-	void codeFlowFailed(Networking::ErrorResponse error);
+protected:
+	/**
+	 * @return "dropbox"
+	 */
+	virtual Common::String cloudProvider();
+
+	/**
+	 * @return kStorageDropboxId
+	 */
+	virtual uint32 storageIndex();
 
 public:
 	/** This constructor uses OAuth code flow to get tokens. */
diff --git a/backends/module.mk b/backends/module.mk
index 22b4a63..ee12343 100644
--- a/backends/module.mk
+++ b/backends/module.mk
@@ -23,6 +23,7 @@ ifdef USE_CLOUD
 
 ifdef USE_LIBCURL
 MODULE_OBJS += \
+	cloud/basestorage.o \
 	cloud/cloudicon.o \
 	cloud/cloudmanager.o \
 	cloud/iso8601.o \


Commit: 8333cce498a0c600ff42e58d644f9dad7a10068b
    https://github.com/scummvm/scummvm/commit/8333cce498a0c600ff42e58d644f9dad7a10068b
Author: Alexander Tkachev (alexander at tkachov.ru)
Date: 2019-07-30T14:51:41-04:00

Commit Message:
CLOUD: Update OneDriveStorage to work via cloud.scummvm.org

Changed paths:
    backends/cloud/basestorage.cpp
    backends/cloud/onedrive/onedrivestorage.cpp
    backends/cloud/onedrive/onedrivestorage.h
    backends/cloud/onedrive/onedrivetokenrefresher.cpp
    gui/storagewizarddialog.cpp


diff --git a/backends/cloud/basestorage.cpp b/backends/cloud/basestorage.cpp
index 9b31b4c..b71f7f6 100644
--- a/backends/cloud/basestorage.cpp
+++ b/backends/cloud/basestorage.cpp
@@ -87,6 +87,7 @@ void BaseStorage::codeFlowComplete(Networking::JsonResponse response) {
 		debug(9, "%s", json->stringify(true).c_str());
 		CloudMan.removeStorage(this);
 	} else {
+		debug(9, "%s", json->stringify(true).c_str()); // TODO: remove before commit
 		_token = result.getVal("access_token")->asString();
 		CloudMan.replaceStorage(this, storageIndex());
 		ConfMan.flushToDisk();
diff --git a/backends/cloud/onedrive/onedrivestorage.cpp b/backends/cloud/onedrive/onedrivestorage.cpp
index af8c70f8..09d7a81 100644
--- a/backends/cloud/onedrive/onedrivestorage.cpp
+++ b/backends/cloud/onedrive/onedrivestorage.cpp
@@ -43,49 +43,24 @@
 namespace Cloud {
 namespace OneDrive {
 
-#define ONEDRIVE_OAUTH2_TOKEN "https://login.live.com/oauth20_token.srf"
 #define ONEDRIVE_API_SPECIAL_APPROOT_ID "https://api.onedrive.com/v1.0/drive/special/approot:/"
 #define ONEDRIVE_API_SPECIAL_APPROOT "https://api.onedrive.com/v1.0/drive/special/approot"
 
-char *OneDriveStorage::KEY = nullptr; //can't use CloudConfig there yet, loading it on instance creation/auth
-char *OneDriveStorage::SECRET = nullptr;
-
-void OneDriveStorage::loadKeyAndSecret() {
-#ifdef ENABLE_RELEASE
-	KEY = RELEASE_ONEDRIVE_KEY;
-	SECRET = RELEASE_ONEDRIVE_SECRET;
-#else
-	Common::String k = ConfMan.get("ONEDRIVE_KEY", ConfMan.kCloudDomain);
-	KEY = new char[k.size() + 1];
-	memcpy(KEY, k.c_str(), k.size());
-	KEY[k.size()] = 0;
-
-	k = ConfMan.get("ONEDRIVE_SECRET", ConfMan.kCloudDomain);
-	SECRET = new char[k.size() + 1];
-	memcpy(SECRET, k.c_str(), k.size());
-	SECRET[k.size()] = 0;
-#endif
-}
-
-OneDriveStorage::OneDriveStorage(Common::String token, Common::String uid, Common::String refreshToken):
-	_token(token), _uid(uid), _refreshToken(refreshToken) {}
+OneDriveStorage::OneDriveStorage(Common::String token,  Common::String refreshToken):
+	BaseStorage(token, refreshToken) {}
 
 OneDriveStorage::OneDriveStorage(Common::String code) {
-	getAccessToken(
-		new Common::Callback<OneDriveStorage, BoolResponse>(this, &OneDriveStorage::codeFlowComplete),
-		new Common::Callback<OneDriveStorage, Networking::ErrorResponse>(this, &OneDriveStorage::codeFlowFailed),
-		code
-	);
+	getAccessToken(code);
 }
 
 OneDriveStorage::~OneDriveStorage() {}
 
-void OneDriveStorage::getAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback, Common::String code) {
-	if (!KEY || !SECRET)
-		loadKeyAndSecret();
-	bool codeFlow = (code != "");
+Common::String OneDriveStorage::cloudProvider() { return "onedrive"; }
 
-	if (!codeFlow && _refreshToken == "") {
+uint32 OneDriveStorage::storageIndex() { return kStorageOneDriveId; }
+
+void OneDriveStorage::refreshAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback) {
+	if (_refreshToken == "") {
 		warning("OneDriveStorage: no refresh token available to get new access token.");
 		if (callback)
 			(*callback)(BoolResponse(nullptr, false));
@@ -95,17 +70,9 @@ void OneDriveStorage::getAccessToken(BoolCallback callback, Networking::ErrorCal
 	Networking::JsonCallback innerCallback = new Common::CallbackBridge<OneDriveStorage, BoolResponse, Networking::JsonResponse>(this, &OneDriveStorage::tokenRefreshed, callback);
 	if (errorCallback == nullptr)
 		errorCallback = getErrorPrintingCallback();
-	Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(innerCallback, errorCallback, ONEDRIVE_OAUTH2_TOKEN);
-	if (codeFlow) {
-		request->addPostField("code=" + code);
-		request->addPostField("grant_type=authorization_code");
-	} else {
-		request->addPostField("refresh_token=" + _refreshToken);
-		request->addPostField("grant_type=refresh_token");
-	}
-	request->addPostField("client_id=" + Common::String(KEY));
-	request->addPostField("client_secret=" + Common::String(SECRET));
-	request->addPostField("&redirect_uri=https%3A%2F%2Fwww.scummvm.org/c/code");
+
+	Common::String url = "https://cloud.scummvm.org/onedrive/refresh/" + _refreshToken; // TODO: subject to change
+	Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(innerCallback, errorCallback, url);
 	addRequest(request);
 }
 
@@ -137,7 +104,6 @@ void OneDriveStorage::tokenRefreshed(BoolCallback callback, Networking::JsonResp
 			(*callback)(BoolResponse(nullptr, false));
 	} else {
 		_token = result.getVal("access_token")->asString();
-		_uid = result.getVal("user_id")->asString();
 		_refreshToken = result.getVal("refresh_token")->asString();
 		CloudMan.save(); //ask CloudManager to save our new refreshToken
 		if (callback)
@@ -147,26 +113,8 @@ void OneDriveStorage::tokenRefreshed(BoolCallback callback, Networking::JsonResp
 	delete callback;
 }
 
-void OneDriveStorage::codeFlowComplete(BoolResponse response) {
-	if (!response.value) {
-		warning("OneDriveStorage: failed to get access token through code flow");
-		CloudMan.removeStorage(this);
-		return;
-	}
-
-	CloudMan.replaceStorage(this, kStorageOneDriveId);
-	ConfMan.flushToDisk();
-}
-
-void OneDriveStorage::codeFlowFailed(Networking::ErrorResponse error) {
-	debug(9, "OneDriveStorage: code flow failed (%s, %ld):", (error.failed ? "failed" : "interrupted"), error.httpResponseCode);
-	debug(9, "%s", error.response.c_str());
-	CloudMan.removeStorage(this);
-}
-
 void OneDriveStorage::saveConfig(Common::String keyPrefix) {
 	ConfMan.set(keyPrefix + "access_token", _token, ConfMan.kCloudDomain);
-	ConfMan.set(keyPrefix + "user_id", _uid, ConfMan.kCloudDomain);
 	ConfMan.set(keyPrefix + "refresh_token", _refreshToken, ConfMan.kCloudDomain);
 }
 
@@ -295,27 +243,19 @@ Networking::Request *OneDriveStorage::info(StorageInfoCallback callback, Network
 Common::String OneDriveStorage::savesDirectoryPath() { return "saves/"; }
 
 OneDriveStorage *OneDriveStorage::loadFromConfig(Common::String keyPrefix) {
-	loadKeyAndSecret();
-
 	if (!ConfMan.hasKey(keyPrefix + "access_token", ConfMan.kCloudDomain)) {
 		warning("OneDriveStorage: no access_token found");
 		return nullptr;
 	}
 
-	if (!ConfMan.hasKey(keyPrefix + "user_id", ConfMan.kCloudDomain)) {
-		warning("OneDriveStorage: no user_id found");
-		return nullptr;
-	}
-
 	if (!ConfMan.hasKey(keyPrefix + "refresh_token", ConfMan.kCloudDomain)) {
 		warning("OneDriveStorage: no refresh_token found");
 		return nullptr;
 	}
 
 	Common::String accessToken = ConfMan.get(keyPrefix + "access_token", ConfMan.kCloudDomain);
-	Common::String userId = ConfMan.get(keyPrefix + "user_id", ConfMan.kCloudDomain);
 	Common::String refreshToken = ConfMan.get(keyPrefix + "refresh_token", ConfMan.kCloudDomain);
-	return new OneDriveStorage(accessToken, userId, refreshToken);
+	return new OneDriveStorage(accessToken, refreshToken);
 }
 
 } // End of namespace OneDrive
diff --git a/backends/cloud/onedrive/onedrivestorage.h b/backends/cloud/onedrive/onedrivestorage.h
index 5d24eb2..2dab86e 100644
--- a/backends/cloud/onedrive/onedrivestorage.h
+++ b/backends/cloud/onedrive/onedrivestorage.h
@@ -23,30 +23,34 @@
 #ifndef BACKENDS_CLOUD_ONEDRIVE_ONEDRIVESTORAGE_H
 #define BACKENDS_CLOUD_ONEDRIVE_ONEDRIVESTORAGE_H
 
-#include "backends/cloud/storage.h"
+#include "backends/cloud/basestorage.h"
 #include "backends/networking/curl/curljsonrequest.h"
 
 namespace Cloud {
 namespace OneDrive {
 
-class OneDriveStorage: public Cloud::Storage {
-	static char *KEY, *SECRET;
-
-	static void loadKeyAndSecret();
-
-	Common::String _token, _uid, _refreshToken;
-
+class OneDriveStorage: public Cloud::BaseStorage {
 	/** This private constructor is called from loadFromConfig(). */
-	OneDriveStorage(Common::String token, Common::String uid, Common::String refreshToken);
+	OneDriveStorage(Common::String token, Common::String refreshToken);
 
 	void tokenRefreshed(BoolCallback callback, Networking::JsonResponse response);
-	void codeFlowComplete(BoolResponse response);
-	void codeFlowFailed(Networking::ErrorResponse error);
 
 	/** Constructs StorageInfo based on JSON response from cloud. */
 	void infoInnerCallback(StorageInfoCallback outerCallback, Networking::JsonResponse json);
 
 	void fileInfoCallback(Networking::NetworkReadStreamCallback outerCallback, Networking::JsonResponse response);
+
+protected:
+	/**
+	 * @return "onedrive"
+	 */
+	virtual Common::String cloudProvider();
+
+	/**
+	 * @return kStorageOneDriveId
+	 */
+	virtual uint32 storageIndex();
+
 public:
 	/** This constructor uses OAuth code flow to get tokens. */
 	OneDriveStorage(Common::String code);
@@ -98,11 +102,10 @@ public:
 	static OneDriveStorage *loadFromConfig(Common::String keyPrefix);
 
 	/**
-	 * Gets new access_token. If <code> passed is "", refresh_token is used.
-	 * Use "" in order to refresh token and pass a callback, so you could
+	 * Gets new access_token. Pass a callback, so you could
 	 * continue your work when new token is available.
 	 */
-	void getAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback = nullptr, Common::String code = "");
+	void refreshAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback = nullptr);
 
 	Common::String accessToken() const { return _token; }
 };
diff --git a/backends/cloud/onedrive/onedrivetokenrefresher.cpp b/backends/cloud/onedrive/onedrivetokenrefresher.cpp
index be6de40..1404f76 100644
--- a/backends/cloud/onedrive/onedrivetokenrefresher.cpp
+++ b/backends/cloud/onedrive/onedrivetokenrefresher.cpp
@@ -105,7 +105,7 @@ void OneDriveTokenRefresher::finishJson(Common::JSONValue *json) {
 
 			pause();
 			delete json;
-			_parentStorage->getAccessToken(new Common::Callback<OneDriveTokenRefresher, Storage::BoolResponse>(this, &OneDriveTokenRefresher::tokenRefreshed));
+			_parentStorage->refreshAccessToken(new Common::Callback<OneDriveTokenRefresher, Storage::BoolResponse>(this, &OneDriveTokenRefresher::tokenRefreshed));
 			return;
 		}
 	}
diff --git a/gui/storagewizarddialog.cpp b/gui/storagewizarddialog.cpp
index 0a50f8e..b5b4b6f 100644
--- a/gui/storagewizarddialog.cpp
+++ b/gui/storagewizarddialog.cpp
@@ -135,7 +135,7 @@ void StorageWizardDialog::handleCommand(CommandSender *sender, uint32 cmd, uint3
 	case kCodeBoxCmd: {
 		Common::String code, message;
 
-		if (_storageId == Cloud::kStorageDropboxId) {
+		if (_storageId == Cloud::kStorageDropboxId || _storageId == Cloud::kStorageOneDriveId) {
 			// new handling
 			code = _codeWidget[0]->getEditString();
 
@@ -228,7 +228,7 @@ void StorageWizardDialog::handleCommand(CommandSender *sender, uint32 cmd, uint3
 		break;
 	}
 	case kConnectCmd: {
-		if (_storageId == Cloud::kStorageDropboxId) {
+		if (_storageId == Cloud::kStorageDropboxId || _storageId == Cloud::kStorageOneDriveId) {
 			// new handling
 			Common::String code = _codeWidget[0]->getEditString();
 			if (code.size() == 0)
@@ -289,7 +289,7 @@ void StorageWizardDialog::containerWidgetsReflow() {
 
 	bool showFields = true; // TODO: remove this const
 	for (uint32 i = 0; i < CODE_FIELDS; ++i)
-		_codeWidget[i]->setVisible(showFields && (_storageId != Cloud::kStorageDropboxId || i < 1)); // show only one field for Dropbox
+		_codeWidget[i]->setVisible(showFields && ((_storageId != Cloud::kStorageDropboxId && _storageId != Cloud::kStorageOneDriveId) || i < 1)); // show only one field for Dropbox
 	_messageWidget->setVisible(showFields);
 
 	// left column / first bottom row
@@ -316,10 +316,8 @@ Common::String StorageWizardDialog::getUrl() const {
 	Common::String url = "https://www.scummvm.org/c/";
 	switch (_storageId) {
 	case Cloud::kStorageDropboxId:
-		url = "https://cloud.scummvm.org/";
-		break;
 	case Cloud::kStorageOneDriveId:
-		url += "od";
+		url = "https://cloud.scummvm.org/";
 		break;
 	case Cloud::kStorageGoogleDriveId:
 		url += "gd";


Commit: 0ee0e2d537e6217278e75ec59c216936896bf6cb
    https://github.com/scummvm/scummvm/commit/0ee0e2d537e6217278e75ec59c216936896bf6cb
Author: Alexander Tkachev (alexander at tkachov.ru)
Date: 2019-07-30T14:51:41-04:00

Commit Message:
CLOUD: Update GoogleDriveStorage and BoxStorage to auth via cloud.scummvm.org

Changed paths:
    backends/cloud/box/boxstorage.cpp
    backends/cloud/box/boxstorage.h
    backends/cloud/box/boxtokenrefresher.cpp
    backends/cloud/googledrive/googledrivestorage.cpp
    backends/cloud/googledrive/googledrivestorage.h
    backends/cloud/googledrive/googledrivetokenrefresher.cpp
    backends/cloud/id/idstorage.cpp
    backends/cloud/id/idstorage.h
    gui/storagewizarddialog.cpp
    gui/storagewizarddialog.h


diff --git a/backends/cloud/box/boxstorage.cpp b/backends/cloud/box/boxstorage.cpp
index 2671a77..df81773 100644
--- a/backends/cloud/box/boxstorage.cpp
+++ b/backends/cloud/box/boxstorage.cpp
@@ -42,50 +42,25 @@
 namespace Cloud {
 namespace Box {
 
-#define BOX_OAUTH2_TOKEN "https://api.box.com/oauth2/token"
 #define BOX_API_FOLDERS "https://api.box.com/2.0/folders"
 #define BOX_API_FILES_CONTENT "https://api.box.com/2.0/files/%s/content"
 #define BOX_API_USERS_ME "https://api.box.com/2.0/users/me"
 
-char *BoxStorage::KEY = nullptr; //can't use CloudConfig there yet, loading it on instance creation/auth
-char *BoxStorage::SECRET = nullptr;
-
-void BoxStorage::loadKeyAndSecret() {
-#ifdef ENABLE_RELEASE
-	KEY = RELEASE_BOX_KEY;
-	SECRET = RELEASE_BOX_SECRET;
-#else
-	Common::String k = ConfMan.get("BOX_KEY", ConfMan.kCloudDomain);
-	KEY = new char[k.size() + 1];
-	memcpy(KEY, k.c_str(), k.size());
-	KEY[k.size()] = 0;
-
-	k = ConfMan.get("BOX_SECRET", ConfMan.kCloudDomain);
-	SECRET = new char[k.size() + 1];
-	memcpy(SECRET, k.c_str(), k.size());
-	SECRET[k.size()] = 0;
-#endif
-}
-
 BoxStorage::BoxStorage(Common::String token, Common::String refreshToken):
-	_token(token), _refreshToken(refreshToken) {}
+	IdStorage(token, refreshToken) {}
 
 BoxStorage::BoxStorage(Common::String code) {
-	getAccessToken(
-		new Common::Callback<BoxStorage, BoolResponse>(this, &BoxStorage::codeFlowComplete),
-		new Common::Callback<BoxStorage, Networking::ErrorResponse>(this, &BoxStorage::codeFlowFailed),
-		code
-	);
+	getAccessToken(code);
 }
 
 BoxStorage::~BoxStorage() {}
 
-void BoxStorage::getAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback, Common::String code) {
-	if (!KEY || !SECRET)
-		loadKeyAndSecret();
-	bool codeFlow = (code != "");
+Common::String BoxStorage::cloudProvider() { return "box"; }
 
-	if (!codeFlow && _refreshToken == "") {
+uint32 BoxStorage::storageIndex() { return kStorageBoxId; }
+
+void BoxStorage::refreshAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback) {
+	if (_refreshToken == "") {
 		warning("BoxStorage: no refresh token available to get new access token.");
 		if (callback) (*callback)(BoolResponse(nullptr, false));
 		return;
@@ -95,23 +70,8 @@ void BoxStorage::getAccessToken(BoolCallback callback, Networking::ErrorCallback
 	if (errorCallback == nullptr)
 		errorCallback = getErrorPrintingCallback();
 
-	Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(innerCallback, errorCallback, BOX_OAUTH2_TOKEN);
-	if (codeFlow) {
-		request->addPostField("grant_type=authorization_code");
-		request->addPostField("code=" + code);
-	} else {
-		request->addPostField("grant_type=refresh_token");
-		request->addPostField("refresh_token=" + _refreshToken);
-	}
-	request->addPostField("client_id=" + Common::String(KEY));
-	request->addPostField("client_secret=" + Common::String(SECRET));
-	/*
-	if (Cloud::CloudManager::couldUseLocalServer()) {
-	    request->addPostField("&redirect_uri=http%3A%2F%2Flocalhost%3A12345");
-	} else {
-	    request->addPostField("&redirect_uri=https%3A%2F%2Fwww.scummvm.org/c/code");
-	}
-	*/
+	Common::String url = "https://cloud.scummvm.org/box/refresh/" + _refreshToken; // TODO: subject to change
+	Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(innerCallback, errorCallback, url);
 	addRequest(request);
 }
 
@@ -151,23 +111,6 @@ void BoxStorage::tokenRefreshed(BoolCallback callback, Networking::JsonResponse
 	delete callback;
 }
 
-void BoxStorage::codeFlowComplete(BoolResponse response) {
-	if (!response.value) {
-		warning("BoxStorage: failed to get access token through code flow");
-		CloudMan.removeStorage(this);
-		return;
-	}
-
-	CloudMan.replaceStorage(this, kStorageBoxId);
-	ConfMan.flushToDisk();
-}
-
-void BoxStorage::codeFlowFailed(Networking::ErrorResponse error) {
-	debug(9, "BoxStorage: code flow failed (%s, %ld):", (error.failed ? "failed" : "interrupted"), error.httpResponseCode);
-	debug(9, "%s", error.response.c_str());
-	CloudMan.removeStorage(this);
-}
-
 void BoxStorage::saveConfig(Common::String keyPrefix) {
 	ConfMan.set(keyPrefix + "access_token", _token, ConfMan.kCloudDomain);
 	ConfMan.set(keyPrefix + "refresh_token", _refreshToken, ConfMan.kCloudDomain);
@@ -321,8 +264,6 @@ Networking::Request *BoxStorage::info(StorageInfoCallback callback, Networking::
 Common::String BoxStorage::savesDirectoryPath() { return "scummvm/saves/"; }
 
 BoxStorage *BoxStorage::loadFromConfig(Common::String keyPrefix) {
-	loadKeyAndSecret();
-
 	if (!ConfMan.hasKey(keyPrefix + "access_token", ConfMan.kCloudDomain)) {
 		warning("BoxStorage: no access_token found");
 		return nullptr;
diff --git a/backends/cloud/box/boxstorage.h b/backends/cloud/box/boxstorage.h
index a641669..e22624a 100644
--- a/backends/cloud/box/boxstorage.h
+++ b/backends/cloud/box/boxstorage.h
@@ -30,23 +30,27 @@ namespace Cloud {
 namespace Box {
 
 class BoxStorage: public Id::IdStorage {
-	static char *KEY, *SECRET;
-
-	static void loadKeyAndSecret();
-
-	Common::String _token, _refreshToken;
-
 	/** This private constructor is called from loadFromConfig(). */
 	BoxStorage(Common::String token, Common::String refreshToken);
 
 	void tokenRefreshed(BoolCallback callback, Networking::JsonResponse response);
-	void codeFlowComplete(BoolResponse response);
-	void codeFlowFailed(Networking::ErrorResponse error);
 
 	/** Constructs StorageInfo based on JSON response from cloud. */
 	void infoInnerCallback(StorageInfoCallback outerCallback, Networking::JsonResponse json);
 
 	void createDirectoryInnerCallback(BoolCallback outerCallback, Networking::JsonResponse response);
+
+protected:
+	/**
+	 * @return "box"
+	 */
+	virtual Common::String cloudProvider();
+
+	/**
+	 * @return kStorageBoxId
+	 */
+	virtual uint32 storageIndex();
+
 public:
 	/** This constructor uses OAuth code flow to get tokens. */
 	BoxStorage(Common::String code);
@@ -101,11 +105,10 @@ public:
 	virtual Common::String getRootDirectoryId();
 
 	/**
-	 * Gets new access_token. If <code> passed is "", refresh_token is used.
-	 * Use "" in order to refresh token and pass a callback, so you could
+	 * Gets new access_token. Pass a callback, so you could
 	 * continue your work when new token is available.
 	 */
-	void getAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback = nullptr, Common::String code = "");
+	void refreshAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback = nullptr);
 
 	Common::String accessToken() const { return _token; }
 };
diff --git a/backends/cloud/box/boxtokenrefresher.cpp b/backends/cloud/box/boxtokenrefresher.cpp
index 5f7ad1d..19cdd92 100644
--- a/backends/cloud/box/boxtokenrefresher.cpp
+++ b/backends/cloud/box/boxtokenrefresher.cpp
@@ -99,7 +99,7 @@ void BoxTokenRefresher::finishJson(Common::JSONValue *json) {
 
 			pause();
 			delete json;
-			_parentStorage->getAccessToken(new Common::Callback<BoxTokenRefresher, Storage::BoolResponse>(this, &BoxTokenRefresher::tokenRefreshed));
+			_parentStorage->refreshAccessToken(new Common::Callback<BoxTokenRefresher, Storage::BoolResponse>(this, &BoxTokenRefresher::tokenRefreshed));
 			return;
 		}
 	}
@@ -111,7 +111,7 @@ void BoxTokenRefresher::finishJson(Common::JSONValue *json) {
 void BoxTokenRefresher::finishError(Networking::ErrorResponse error) {
 	if (error.httpResponseCode == 401) { // invalid_token
 		pause();
-		_parentStorage->getAccessToken(new Common::Callback<BoxTokenRefresher, Storage::BoolResponse>(this, &BoxTokenRefresher::tokenRefreshed));
+		_parentStorage->refreshAccessToken(new Common::Callback<BoxTokenRefresher, Storage::BoolResponse>(this, &BoxTokenRefresher::tokenRefreshed));
 		return;
 	}
 
diff --git a/backends/cloud/googledrive/googledrivestorage.cpp b/backends/cloud/googledrive/googledrivestorage.cpp
index 51799eb..bd4f2cb 100644
--- a/backends/cloud/googledrive/googledrivestorage.cpp
+++ b/backends/cloud/googledrive/googledrivestorage.cpp
@@ -43,49 +43,25 @@
 namespace Cloud {
 namespace GoogleDrive {
 
-#define GOOGLEDRIVE_OAUTH2_TOKEN "https://accounts.google.com/o/oauth2/token"
 #define GOOGLEDRIVE_API_FILES_ALT_MEDIA "https://www.googleapis.com/drive/v3/files/%s?alt=media"
 #define GOOGLEDRIVE_API_FILES "https://www.googleapis.com/drive/v3/files"
 #define GOOGLEDRIVE_API_ABOUT "https://www.googleapis.com/drive/v3/about?fields=storageQuota,user"
 
-char *GoogleDriveStorage::KEY = nullptr; //can't use CloudConfig there yet, loading it on instance creation/auth
-char *GoogleDriveStorage::SECRET = nullptr;
-
-void GoogleDriveStorage::loadKeyAndSecret() {
-#ifdef ENABLE_RELEASE
-	KEY = RELEASE_GOOGLE_DRIVE_KEY;
-	SECRET = RELEASE_GOOGLE_DRIVE_SECRET;
-#else
-	Common::String k = ConfMan.get("GOOGLE_DRIVE_KEY", ConfMan.kCloudDomain);
-	KEY = new char[k.size() + 1];
-	memcpy(KEY, k.c_str(), k.size());
-	KEY[k.size()] = 0;
-
-	k = ConfMan.get("GOOGLE_DRIVE_SECRET", ConfMan.kCloudDomain);
-	SECRET = new char[k.size() + 1];
-	memcpy(SECRET, k.c_str(), k.size());
-	SECRET[k.size()] = 0;
-#endif
-}
-
 GoogleDriveStorage::GoogleDriveStorage(Common::String token, Common::String refreshToken):
-	_token(token), _refreshToken(refreshToken) {}
+	IdStorage(token, refreshToken) {}
 
 GoogleDriveStorage::GoogleDriveStorage(Common::String code) {
-	getAccessToken(
-		new Common::Callback<GoogleDriveStorage, BoolResponse>(this, &GoogleDriveStorage::codeFlowComplete),
-		new Common::Callback<GoogleDriveStorage, Networking::ErrorResponse>(this, &GoogleDriveStorage::codeFlowFailed),
-		code
-	);
+	getAccessToken(code);
 }
 
 GoogleDriveStorage::~GoogleDriveStorage() {}
 
-void GoogleDriveStorage::getAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback, Common::String code) {
-	if (!KEY || !SECRET) loadKeyAndSecret();
-	bool codeFlow = (code != "");
+Common::String GoogleDriveStorage::cloudProvider() { return "gdrive"; }
 
-	if (!codeFlow && _refreshToken == "") {
+uint32 GoogleDriveStorage::storageIndex() { return kStorageGoogleDriveId; }
+
+void GoogleDriveStorage::refreshAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback) {
+	if (_refreshToken == "") {
 		warning("GoogleDriveStorage: no refresh token available to get new access token.");
 		if (callback)
 			(*callback)(BoolResponse(nullptr, false));
@@ -95,17 +71,9 @@ void GoogleDriveStorage::getAccessToken(BoolCallback callback, Networking::Error
 	Networking::JsonCallback innerCallback = new Common::CallbackBridge<GoogleDriveStorage, BoolResponse, Networking::JsonResponse>(this, &GoogleDriveStorage::tokenRefreshed, callback);
 	if (errorCallback == nullptr)
 		errorCallback = getErrorPrintingCallback();
-	Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(innerCallback, errorCallback, GOOGLEDRIVE_OAUTH2_TOKEN);
-	if (codeFlow) {
-		request->addPostField("code=" + code);
-		request->addPostField("grant_type=authorization_code");
-	} else {
-		request->addPostField("refresh_token=" + _refreshToken);
-		request->addPostField("grant_type=refresh_token");
-	}
-	request->addPostField("client_id=" + Common::String(KEY));
-	request->addPostField("client_secret=" + Common::String(SECRET));
-	request->addPostField("&redirect_uri=https%3A%2F%2Fwww.scummvm.org/c/code");
+
+	Common::String url = "https://cloud.scummvm.org/gdrive/refresh/" + _refreshToken; // TODO: subject to change
+	Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(innerCallback, errorCallback, url);
 	addRequest(request);
 }
 
@@ -147,23 +115,6 @@ void GoogleDriveStorage::tokenRefreshed(BoolCallback callback, Networking::JsonR
 	delete callback;
 }
 
-void GoogleDriveStorage::codeFlowComplete(BoolResponse response) {
-	if (!response.value) {
-		warning("GoogleDriveStorage: failed to get access token through code flow");
-		CloudMan.removeStorage(this);
-		return;
-	}
-
-	CloudMan.replaceStorage(this, kStorageGoogleDriveId);
-	ConfMan.flushToDisk();
-}
-
-void GoogleDriveStorage::codeFlowFailed(Networking::ErrorResponse error) {
-	debug(9, "GoogleDriveStorage: code flow failed (%s, %ld):", (error.failed ? "failed" : "interrupted"), error.httpResponseCode);
-	debug(9, "%s", error.response.c_str());
-	CloudMan.removeStorage(this);
-}
-
 void GoogleDriveStorage::saveConfig(Common::String keyPrefix) {
 	ConfMan.set(keyPrefix + "access_token", _token, ConfMan.kCloudDomain);
 	ConfMan.set(keyPrefix + "refresh_token", _refreshToken, ConfMan.kCloudDomain);
@@ -320,8 +271,6 @@ Networking::Request *GoogleDriveStorage::info(StorageInfoCallback callback, Netw
 Common::String GoogleDriveStorage::savesDirectoryPath() { return "scummvm/saves/"; }
 
 GoogleDriveStorage *GoogleDriveStorage::loadFromConfig(Common::String keyPrefix) {
-	loadKeyAndSecret();
-
 	if (!ConfMan.hasKey(keyPrefix + "access_token", ConfMan.kCloudDomain)) {
 		warning("GoogleDriveStorage: no access_token found");
 		return nullptr;
diff --git a/backends/cloud/googledrive/googledrivestorage.h b/backends/cloud/googledrive/googledrivestorage.h
index d0585bc..30bc9ab 100644
--- a/backends/cloud/googledrive/googledrivestorage.h
+++ b/backends/cloud/googledrive/googledrivestorage.h
@@ -30,18 +30,10 @@ namespace Cloud {
 namespace GoogleDrive {
 
 class GoogleDriveStorage: public Id::IdStorage {
-	static char *KEY, *SECRET;
-
-	static void loadKeyAndSecret();
-
-	Common::String _token, _refreshToken;
-
 	/** This private constructor is called from loadFromConfig(). */
 	GoogleDriveStorage(Common::String token, Common::String refreshToken);
 
 	void tokenRefreshed(BoolCallback callback, Networking::JsonResponse response);
-	void codeFlowComplete(BoolResponse response);
-	void codeFlowFailed(Networking::ErrorResponse error);
 
 	/** Constructs StorageInfo based on JSON response from cloud. */
 	void infoInnerCallback(StorageInfoCallback outerCallback, Networking::JsonResponse json);
@@ -50,6 +42,18 @@ class GoogleDriveStorage: public Id::IdStorage {
 	void createDirectoryInnerCallback(BoolCallback outerCallback, Networking::JsonResponse json);
 
 	void printInfo(StorageInfoResponse response);
+
+protected:
+	/**
+	 * @return "gdrive"
+	 */
+	virtual Common::String cloudProvider();
+
+	/**
+	 * @return kStorageGoogleDriveId
+	 */
+	virtual uint32 storageIndex();
+
 public:
 	/** This constructor uses OAuth code flow to get tokens. */
 	GoogleDriveStorage(Common::String code);
@@ -103,11 +107,10 @@ public:
 	virtual Common::String getRootDirectoryId();
 
 	/**
-	 * Gets new access_token. If <code> passed is "", refresh_token is used.
-	 * Use "" in order to refresh token and pass a callback, so you could
+	 * Gets new access_token. Pass a callback, so you could
 	 * continue your work when new token is available.
 	 */
-	void getAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback = nullptr, Common::String code = "");
+	void refreshAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback = nullptr);
 
 	Common::String accessToken() const { return _token; }
 };
diff --git a/backends/cloud/googledrive/googledrivetokenrefresher.cpp b/backends/cloud/googledrive/googledrivetokenrefresher.cpp
index a32a7fc..f28f73a 100644
--- a/backends/cloud/googledrive/googledrivetokenrefresher.cpp
+++ b/backends/cloud/googledrive/googledrivetokenrefresher.cpp
@@ -100,7 +100,7 @@ void GoogleDriveTokenRefresher::finishJson(Common::JSONValue *json) {
 
 			pause();
 			delete json;
-			_parentStorage->getAccessToken(new Common::Callback<GoogleDriveTokenRefresher, Storage::BoolResponse>(this, &GoogleDriveTokenRefresher::tokenRefreshed));
+			_parentStorage->refreshAccessToken(new Common::Callback<GoogleDriveTokenRefresher, Storage::BoolResponse>(this, &GoogleDriveTokenRefresher::tokenRefreshed));
 			return;
 		}
 	}
diff --git a/backends/cloud/id/idstorage.cpp b/backends/cloud/id/idstorage.cpp
index 44427ac..dd8805e 100644
--- a/backends/cloud/id/idstorage.cpp
+++ b/backends/cloud/id/idstorage.cpp
@@ -33,6 +33,11 @@
 namespace Cloud {
 namespace Id {
 
+IdStorage::IdStorage() {}
+
+IdStorage::IdStorage(Common::String token, Common::String refreshToken):
+	BaseStorage(token, refreshToken) {}
+
 IdStorage::~IdStorage() {}
 
 void IdStorage::printFiles(FileArrayResponse response) {
diff --git a/backends/cloud/id/idstorage.h b/backends/cloud/id/idstorage.h
index 946a792..35a3202 100644
--- a/backends/cloud/id/idstorage.h
+++ b/backends/cloud/id/idstorage.h
@@ -23,7 +23,7 @@
 #ifndef BACKENDS_CLOUD_ID_IDSTORAGE_H
 #define BACKENDS_CLOUD_ID_IDSTORAGE_H
 
-#include "backends/cloud/storage.h"
+#include "backends/cloud/basestorage.h"
 #include "backends/networking/curl/curljsonrequest.h"
 
 /*
@@ -43,7 +43,7 @@
 namespace Cloud {
 namespace Id {
 
-class IdStorage: public Cloud::Storage {
+class IdStorage: public Cloud::BaseStorage {
 protected:
 	void printFiles(FileArrayResponse response);
 	void printBool(BoolResponse response);
@@ -52,6 +52,8 @@ protected:
 	ListDirectoryCallback getPrintFilesCallback();
 
 public:
+	IdStorage();
+	IdStorage(Common::String token, Common::String refreshToken);
 	virtual ~IdStorage();
 
 	/** Public Cloud API comes down there. */
diff --git a/gui/storagewizarddialog.cpp b/gui/storagewizarddialog.cpp
index b5b4b6f..b01d442 100644
--- a/gui/storagewizarddialog.cpp
+++ b/gui/storagewizarddialog.cpp
@@ -60,8 +60,7 @@ StorageWizardDialog::StorageWizardDialog(uint32 storageId):
 
 	_returnLine1 = new StaticTextWidget(container, "GlobalOptions_Cloud_ConnectionWizard_Container.ReturnLine1", _("Obtain the code from the storage, enter it"));
 	_returnLine2 = new StaticTextWidget(container, "GlobalOptions_Cloud_ConnectionWizard_Container.ReturnLine2", _("in the following field and press 'Connect':"));
-	for (uint32 i = 0; i < CODE_FIELDS; ++i)
-		_codeWidget[i] = new EditTextWidget(container, "GlobalOptions_Cloud_ConnectionWizard_Container.CodeBox" + Common::String::format("%d", i+1), "", 0, kCodeBoxCmd);
+	_codeWidget = new EditTextWidget(container, "GlobalOptions_Cloud_ConnectionWizard_Container.CodeBox1", "", 0, kCodeBoxCmd);
 	_messageWidget = new StaticTextWidget(container, "GlobalOptions_Cloud_ConnectionWizard_Container.MessageLine", "");
 
 	// Buttons
@@ -121,7 +120,6 @@ void StorageWizardDialog::open() {
 
 		if (doClose) {
 			close();
-			return;
 		}
 	}
 }
@@ -133,67 +131,10 @@ void StorageWizardDialog::close() {
 void StorageWizardDialog::handleCommand(CommandSender *sender, uint32 cmd, uint32 data) {
 	switch (cmd) {
 	case kCodeBoxCmd: {
-		Common::String code, message;
-
-		if (_storageId == Cloud::kStorageDropboxId || _storageId == Cloud::kStorageOneDriveId) {
-			// new handling
-			code = _codeWidget[0]->getEditString();
-
-			bool ok = (code.size() > 0);
-			message = ""; // (ok ? _("All OK!") : "");
-
-			_connectWidget->setEnabled(ok);
-			_messageWidget->setLabel(message);
-			return;
-		}
-		
-		uint32 correctFields = 0;
-		for (uint32 i = 0; i < CODE_FIELDS; ++i) {
-			Common::String subcode = _codeWidget[i]->getEditString();
-			if (subcode.size() == 0) {
-				++correctFields;
-				continue;
-			}
-			bool correct = correctChecksum(subcode);
-			if (correct) {
-				code += subcode;
-				code.deleteLastChar();
-				++correctFields;
-			} else {
-				if (i == correctFields) { //first incorrect field
-					message += Common::String::format("#%d", i + 1);
-				} else {
-					message += Common::String::format(", #%d", i + 1);
-				}
-			}
-		}
-
-		if (message.size() > 0) {
-			Common::String messageTemplate;
-			if (CODE_FIELDS - correctFields == 1)
-				messageTemplate = _("Field %s has a mistake in it.");
-			else
-				messageTemplate = _("Fields %s have mistakes in them.");
-			message = Common::String::format(messageTemplate.c_str(), message.c_str());
-		}
-
-		bool ok = false;
-		if (correctFields == CODE_FIELDS && code.size() > 0) {
-			//the last 3 chars must be an encoded crc16
-			if (code.size() > 3) {
-				uint32 size = code.size();
-				uint32 gotcrc = decodeHashchar(code[size - 3]) | (decodeHashchar(code[size - 2]) << 6) | (decodeHashchar(code[size - 1]) << 12);
-				code.erase(size - 3);
-				uint32 crc = crc16(code);
-				ok = (crc == gotcrc);
-			}
-			if (ok)
-				message = _("All OK!");
-			else
-				message = _("Invalid code");
-		}
+		Common::String code = _codeWidget->getEditString();
+		bool ok = (code.size() > 0);
 		_connectWidget->setEnabled(ok);
-		_messageWidget->setLabel(message);
+		_messageWidget->setLabel("");
 		break;
 	}
 	case kOpenUrlCmd: {
@@ -206,21 +147,8 @@ void StorageWizardDialog::handleCommand(CommandSender *sender, uint32 cmd, uint3
 	case kPasteCodeCmd: {
 		if (g_system->hasTextInClipboard()) {
 			Common::String message = g_system->getTextFromClipboard();
-			for (uint32 i = 0; i < CODE_FIELDS; ++i) {
-				if (message.empty()) break;
-				Common::String subcode = "";
-				for (uint32 j = 0; j < message.size(); ++j) {
-					if (message[j] == ' ') {
-						message.erase(0, j+1);
-						break;
-					}
-					subcode += message[j];
-					if (j+1 == message.size()) {
-						message = "";
-						break;
-					}
-				}
-				_codeWidget[i]->setEditString(subcode);
+			if (!message.empty()) {
+				_codeWidget->setEditString(message);
 			}
 			handleCommand(sender, kCodeBoxCmd, data);
 			g_gui.scheduleTopDialogRedraw();
@@ -228,32 +156,13 @@ void StorageWizardDialog::handleCommand(CommandSender *sender, uint32 cmd, uint3
 		break;
 	}
 	case kConnectCmd: {
-		if (_storageId == Cloud::kStorageDropboxId || _storageId == Cloud::kStorageOneDriveId) {
-			// new handling
-			Common::String code = _codeWidget[0]->getEditString();
-			if (code.size() == 0)
-				return;
-
-			CloudMan.connectStorage(_storageId, code);
-			setResult(1);
-			close();
+		Common::String code = _codeWidget->getEditString();
+		if (code.size() == 0)
 			return;
-		}
 
-		Common::String code;
-		for (uint32 i = 0; i < CODE_FIELDS; ++i) {
-			Common::String subcode = _codeWidget[i]->getEditString();
-			if (subcode.size() == 0)
-				continue;
-			code += subcode;
-			code.deleteLastChar();
-		}
-		if (code.size() > 3) {
-			code.erase(code.size() - 3);
-			CloudMan.connectStorage(_storageId, code);
-			setResult(1);
-			close();
-		}
+		CloudMan.connectStorage(_storageId, code);
+		setResult(1);
+		close();
 		break;
 	}
 #ifdef USE_SDL_NET
@@ -286,11 +195,9 @@ void StorageWizardDialog::containerWidgetsReflow() {
 	if (_urlLineWidget) _urlLineWidget->setVisible(true);
 	if (_returnLine1) _returnLine1->setVisible(true);
 	if (_returnLine2) _returnLine2->setVisible(true);
-
-	bool showFields = true; // TODO: remove this const
-	for (uint32 i = 0; i < CODE_FIELDS; ++i)
-		_codeWidget[i]->setVisible(showFields && ((_storageId != Cloud::kStorageDropboxId && _storageId != Cloud::kStorageOneDriveId) || i < 1)); // show only one field for Dropbox
-	_messageWidget->setVisible(showFields);
+	
+	_codeWidget->setVisible(true);
+	_messageWidget->setVisible(true);
 
 	// left column / first bottom row
 	if (_picture) {
@@ -301,62 +208,17 @@ void StorageWizardDialog::containerWidgetsReflow() {
 		_openUrlWidget->setVisible(visible);
 	}
 	if (_pasteCodeWidget) {
-		bool visible = showFields && g_system->hasFeature(OSystem::kFeatureClipboardSupport);
+		bool visible = g_system->hasFeature(OSystem::kFeatureClipboardSupport);
 		_pasteCodeWidget->setVisible(visible);
 	}
 
 	// bottom row
 	if (_cancelWidget) _cancelWidget->setVisible(true);
-	if (_connectWidget) {
-		_connectWidget->setVisible(showFields);
-	}
+	if (_connectWidget) _connectWidget->setVisible(true);
 }
 
 Common::String StorageWizardDialog::getUrl() const {
-	Common::String url = "https://www.scummvm.org/c/";
-	switch (_storageId) {
-	case Cloud::kStorageDropboxId:
-	case Cloud::kStorageOneDriveId:
-		url = "https://cloud.scummvm.org/";
-		break;
-	case Cloud::kStorageGoogleDriveId:
-		url += "gd";
-		break;
-	case Cloud::kStorageBoxId:
-		url += "bx";
-		break;
-	}
-
-	return url;
-}
-
-int StorageWizardDialog::decodeHashchar(char c) {
-	const char HASHCHARS[65] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ?!";
-	for (uint32 i = 0; i < 64; ++i)
-		if (c == HASHCHARS[i])
-			return i;
-	return -1;
-}
-
-bool StorageWizardDialog::correctChecksum(Common::String s) {
-	if (s.size() == 0)
-		return false; //no last char
-	int providedChecksum = decodeHashchar(s.lastChar());
-	int calculatedChecksum = 0x2A; //any initial value would do, but it must equal to the one used on the page where these checksums were generated
-	for (uint32 i = 0; i < s.size()-1; ++i) {
-		calculatedChecksum = calculatedChecksum ^ s[i];
-	}
-	return providedChecksum == (calculatedChecksum % 64);
-}
-
-uint32 StorageWizardDialog::crc16(Common::String s) { //"CRC16_CCITT_FALSE"
-	uint32 crc = 0xFFFF, x;
-	for (uint32 i = 0; i < s.size(); ++i) {
-		x = ((crc >> 8) ^ s[i]) & 0xFF;
-		x ^= x >> 4;
-		crc = ((crc << 8) ^ (x << 12) ^ (x << 5) ^ x) & 0xFFFF;
-	}
-	return crc;
+	return "https://cloud.scummvm.org/";
 }
 
 } // End of namespace GUI
diff --git a/gui/storagewizarddialog.h b/gui/storagewizarddialog.h
index 61bc8ac..ede3750 100644
--- a/gui/storagewizarddialog.h
+++ b/gui/storagewizarddialog.h
@@ -41,7 +41,6 @@ enum StorageWizardDialogCommands {
 #endif
 
 class StorageWizardDialog : public Dialog {
-	static const uint32 CODE_FIELDS = 8;
 	uint32 _storageId;
 
 	StaticTextWidget *_headlineWidget;
@@ -49,7 +48,7 @@ class StorageWizardDialog : public Dialog {
 	StaticTextWidget *_urlLineWidget;
 	StaticTextWidget *_returnLine1;
 	StaticTextWidget *_returnLine2;
-	EditTextWidget *_codeWidget[CODE_FIELDS];
+	EditTextWidget *_codeWidget;
 	StaticTextWidget *_messageWidget;
 
 	GraphicsWidget *_picture;
@@ -70,29 +69,6 @@ class StorageWizardDialog : public Dialog {
 	/** Return short scummvm.org URL for user to navigate to. */
 	Common::String getUrl() const;
 
-	/**
-	 * Return the value corresponding to the given character.
-	 *
-	 * There is a value corresponding to each of 64 selected
-	 * printable characters (0-9, A-Z, a-z, ? and !).
-	 *
-	 * When given another character, -1 is returned.
-	 */
-	int decodeHashchar(char c);
-
-	/**
-	 * Return whether checksum is correct.
-	 *
-	 * The last character of the string is treated as
-	 * the checksum of all the others (decoded with
-	 * decodeHashchar()).
-	 *
-	 * Checksum = (c[0] ^ c[1] ^ ...) % 64
-	 */
-	bool correctChecksum(Common::String s);
-
-	/** The "CRC16_CCITT_FALSE" CRC-16 algorithm. */
-	uint32 crc16(Common::String s);
 public:
 	StorageWizardDialog(uint32 storageId);
 


Commit: 4a427faf9cc817cfce8e293c22cc470339b6e3c7
    https://github.com/scummvm/scummvm/commit/4a427faf9cc817cfce8e293c22cc470339b6e3c7
Author: Alexander Tkachev (alexander at tkachov.ru)
Date: 2019-07-30T14:51:41-04:00

Commit Message:
CLOUD: Get rid of 'dists/clouds/cloud_keys.h'

Changed paths:
    backends/cloud/box/boxstorage.cpp
    backends/cloud/dropbox/dropboxstorage.cpp
    backends/cloud/googledrive/googledrivestorage.cpp
    backends/cloud/onedrive/onedrivestorage.cpp


diff --git a/backends/cloud/box/boxstorage.cpp b/backends/cloud/box/boxstorage.cpp
index df81773..d743e95 100644
--- a/backends/cloud/box/boxstorage.cpp
+++ b/backends/cloud/box/boxstorage.cpp
@@ -35,10 +35,6 @@
 #include "common/debug.h"
 #include "common/json.h"
 
-#ifdef ENABLE_RELEASE
-#include "dists/clouds/cloud_keys.h"
-#endif
-
 namespace Cloud {
 namespace Box {
 
diff --git a/backends/cloud/dropbox/dropboxstorage.cpp b/backends/cloud/dropbox/dropboxstorage.cpp
index 53ed704..459f4dc 100644
--- a/backends/cloud/dropbox/dropboxstorage.cpp
+++ b/backends/cloud/dropbox/dropboxstorage.cpp
@@ -35,10 +35,6 @@
 #include "common/debug.h"
 #include "common/json.h"
 
-#ifdef ENABLE_RELEASE
-#include "dists/clouds/cloud_keys.h"
-#endif
-
 namespace Cloud {
 namespace Dropbox {
 
diff --git a/backends/cloud/googledrive/googledrivestorage.cpp b/backends/cloud/googledrive/googledrivestorage.cpp
index bd4f2cb..a2f688a 100644
--- a/backends/cloud/googledrive/googledrivestorage.cpp
+++ b/backends/cloud/googledrive/googledrivestorage.cpp
@@ -36,10 +36,6 @@
 #include "common/json.h"
 #include "common/debug.h"
 
-#ifdef ENABLE_RELEASE
-#include "dists/clouds/cloud_keys.h"
-#endif
-
 namespace Cloud {
 namespace GoogleDrive {
 
diff --git a/backends/cloud/onedrive/onedrivestorage.cpp b/backends/cloud/onedrive/onedrivestorage.cpp
index 09d7a81..779797a 100644
--- a/backends/cloud/onedrive/onedrivestorage.cpp
+++ b/backends/cloud/onedrive/onedrivestorage.cpp
@@ -36,10 +36,6 @@
 #include "common/debug.h"
 #include "common/json.h"
 
-#ifdef ENABLE_RELEASE
-#include "dists/clouds/cloud_keys.h"
-#endif
-
 namespace Cloud {
 namespace OneDrive {
 


Commit: 60504dce7539863a207610fd636236c7035ade3e
    https://github.com/scummvm/scummvm/commit/60504dce7539863a207610fd636236c7035ade3e
Author: Alexander Tkachev (alexander at tkachov.ru)
Date: 2019-07-30T14:51:41-04:00

Commit Message:
CLOUD: Update storages to refresh token via cloud.scummvm.org

Changed paths:
    backends/cloud/basestorage.cpp
    backends/cloud/basestorage.h
    backends/cloud/box/boxstorage.cpp
    backends/cloud/box/boxstorage.h
    backends/cloud/dropbox/dropboxstorage.cpp
    backends/cloud/dropbox/dropboxstorage.h
    backends/cloud/googledrive/googledrivestorage.cpp
    backends/cloud/googledrive/googledrivestorage.h
    backends/cloud/onedrive/onedrivestorage.cpp
    backends/cloud/onedrive/onedrivestorage.h


diff --git a/backends/cloud/basestorage.cpp b/backends/cloud/basestorage.cpp
index b71f7f6..035098f 100644
--- a/backends/cloud/basestorage.cpp
+++ b/backends/cloud/basestorage.cpp
@@ -41,7 +41,7 @@ void BaseStorage::getAccessToken(Common::String code) {
 	Networking::JsonCallback callback = new Common::Callback<BaseStorage, Networking::JsonResponse>(this, &BaseStorage::codeFlowComplete);
 	Networking::ErrorCallback errorCallback = new Common::Callback<BaseStorage, Networking::ErrorResponse>(this, &BaseStorage::codeFlowFailed);
 
-	Common::String url = Common::String::format("https://cloud.scummvm.org/%s/%s", cloudProvider().c_str(), code.c_str());
+	Common::String url = Common::String::format("https://cloud.scummvm.org/%s/token/%s", cloudProvider().c_str(), code.c_str());
 	Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(callback, errorCallback, url);
 
 	addRequest(request);
@@ -82,16 +82,32 @@ void BaseStorage::codeFlowComplete(Networking::JsonResponse response) {
 		return;
 	}
 
-	if (!Networking::CurlJsonRequest::jsonContainsString(result, "access_token", "BaseStorage::codeFlowComplete")) {
-		warning("BaseStorage: bad response, no 'access_token' attribute passed");
+	if (!Networking::CurlJsonRequest::jsonContainsObject(result, "oauth", "BaseStorage::codeFlowComplete")) {
+		warning("BaseStorage: bad response, no 'oauth' attribute passed");
 		debug(9, "%s", json->stringify(true).c_str());
 		CloudMan.removeStorage(this);
-	} else {
-		debug(9, "%s", json->stringify(true).c_str()); // TODO: remove before commit
-		_token = result.getVal("access_token")->asString();
-		CloudMan.replaceStorage(this, storageIndex());
-		ConfMan.flushToDisk();
+		delete json;
+		return;
+	}
+
+	Common::JSONObject oauth = result.getVal("oauth")->asObject();
+	bool requiresRefreshToken = needsRefreshToken();
+	if (!Networking::CurlJsonRequest::jsonContainsString(oauth, "access_token", "BaseStorage::codeFlowComplete") ||
+		!Networking::CurlJsonRequest::jsonContainsString(oauth, "refresh_token", "BaseStorage::codeFlowComplete", !requiresRefreshToken)) {
+		warning("BaseStorage: bad response, no 'access_token' or 'refresh_token' attribute passed");
+		debug(9, "%s", json->stringify(true).c_str());
+		CloudMan.removeStorage(this);
+		delete json;
+		return;
+	}
+
+	debug(9, "%s", json->stringify(true).c_str()); // TODO: remove before commit
+	_token = oauth.getVal("access_token")->asString();
+	if (requiresRefreshToken) {
+		_refreshToken = oauth.getVal("refresh_token")->asString();
 	}
+	CloudMan.replaceStorage(this, storageIndex());
+	ConfMan.flushToDisk();
 
 	delete json;
 }
@@ -102,4 +118,97 @@ void BaseStorage::codeFlowFailed(Networking::ErrorResponse error) {
 	CloudMan.removeStorage(this);
 }
 
+void BaseStorage::refreshAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback) {
+	if (_refreshToken == "") {
+		warning("BaseStorage: no refresh token available to get new access token.");
+		if (callback) (*callback)(BoolResponse(nullptr, false));
+		return;
+	}
+
+	Networking::JsonCallback innerCallback = new Common::CallbackBridge<BaseStorage, BoolResponse, Networking::JsonResponse>(this, &BaseStorage::tokenRefreshed, callback);
+	if (errorCallback == nullptr)
+		errorCallback = getErrorPrintingCallback();
+
+	Common::String url = Common::String::format("https://cloud.scummvm.org/%s/refresh?code=%s", cloudProvider().c_str(), _refreshToken.c_str());
+	Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(innerCallback, errorCallback, url);
+	addRequest(request);
+}
+
+void BaseStorage::tokenRefreshed(BoolCallback callback, Networking::JsonResponse response) {
+	Common::JSONValue *json = response.value;
+	if (json == nullptr) {
+		debug(9, "BaseStorage::tokenRefreshed: got NULL instead of JSON!");
+		if (callback)
+			(*callback)(BoolResponse(nullptr, false));
+		delete callback;
+		return;
+	}
+
+	if (!json->isObject()) {
+		debug(9, "BaseStorage::tokenRefreshed: passed JSON is not an object!");
+		if (callback)
+			(*callback)(BoolResponse(nullptr, false));
+		delete json;
+		delete callback;
+		return;
+	}
+
+	Common::JSONObject result = json->asObject();
+	if (!Networking::CurlJsonRequest::jsonContainsAttribute(result, "error", "BaseStorage::tokenRefreshed")) {
+		warning("BaseStorage: bad response, no 'error' attribute passed");
+		debug(9, "%s", json->stringify(true).c_str());
+		if (callback)
+			(*callback)(BoolResponse(nullptr, false));
+		delete json;
+		delete callback;
+		return;
+	}
+
+	if (result.getVal("error")->asBool()) {
+		Common::String errorMessage = "{error: true}, message is missing";
+		if (Networking::CurlJsonRequest::jsonContainsString(result, "message", "BaseStorage::tokenRefreshed")) {
+			errorMessage = result.getVal("message")->asString();
+		}
+		warning("BaseStorage: response says error occurred: %s", errorMessage.c_str());
+		if (callback)
+			(*callback)(BoolResponse(nullptr, false));
+		delete json;
+		delete callback;
+		return;
+	}
+
+	if (!Networking::CurlJsonRequest::jsonContainsObject(result, "oauth", "BaseStorage::tokenRefreshed")) {
+		warning("BaseStorage: bad response, no 'oauth' attribute passed");
+		debug(9, "%s", json->stringify(true).c_str());
+		if (callback)
+			(*callback)(BoolResponse(nullptr, false));
+		delete json;
+		delete callback;
+		return;
+	}
+
+	Common::JSONObject oauth = result.getVal("oauth")->asObject();
+	bool requiresRefreshToken = needsRefreshToken(); // TODO: it seems Google Drive might not send new refresh token, and still accept old one
+	if (!Networking::CurlJsonRequest::jsonContainsString(oauth, "access_token", "BaseStorage::tokenRefreshed") ||
+		!Networking::CurlJsonRequest::jsonContainsString(oauth, "refresh_token", "BaseStorage::tokenRefreshed", !requiresRefreshToken)) {
+		warning("BaseStorage: bad response, no 'access_token' or 'refresh_token' attribute passed");
+		debug(9, "%s", json->stringify(true).c_str());
+		if (callback)
+			(*callback)(BoolResponse(nullptr, false));
+		delete json;
+		delete callback;
+		return;
+	}
+
+	_token = oauth.getVal("access_token")->asString();
+	if (requiresRefreshToken) {
+		_refreshToken = oauth.getVal("refresh_token")->asString();
+	}
+	CloudMan.save(); //ask CloudManager to save our new access_token and refresh_token
+	if (callback)
+		(*callback)(BoolResponse(nullptr, true));
+	delete json;
+	delete callback;
+}
+
 } // End of namespace Cloud
diff --git a/backends/cloud/basestorage.h b/backends/cloud/basestorage.h
index cca6e1b..4b3b497 100644
--- a/backends/cloud/basestorage.h
+++ b/backends/cloud/basestorage.h
@@ -64,10 +64,24 @@ protected:
 	 */
 	virtual uint32 storageIndex() = 0;
 
+	/**
+	 * Return whether storage needs refresh_token to work.
+	 */
+	virtual bool needsRefreshToken() = 0;
+
+private:
+	void tokenRefreshed(BoolCallback callback, Networking::JsonResponse response);
+
 public:
 	BaseStorage();
 	BaseStorage(Common::String token, Common::String refreshToken);
 	virtual ~BaseStorage();
+
+	/**
+	 * Gets new access_token. Pass a callback, so you could
+	 * continue your work when new token is available.
+	 */
+	virtual void refreshAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback = nullptr);
 };
 
 } // End of namespace Cloud
diff --git a/backends/cloud/box/boxstorage.cpp b/backends/cloud/box/boxstorage.cpp
index d743e95..42e9029 100644
--- a/backends/cloud/box/boxstorage.cpp
+++ b/backends/cloud/box/boxstorage.cpp
@@ -55,57 +55,7 @@ Common::String BoxStorage::cloudProvider() { return "box"; }
 
 uint32 BoxStorage::storageIndex() { return kStorageBoxId; }
 
-void BoxStorage::refreshAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback) {
-	if (_refreshToken == "") {
-		warning("BoxStorage: no refresh token available to get new access token.");
-		if (callback) (*callback)(BoolResponse(nullptr, false));
-		return;
-	}
-
-	Networking::JsonCallback innerCallback = new Common::CallbackBridge<BoxStorage, BoolResponse, Networking::JsonResponse>(this, &BoxStorage::tokenRefreshed, callback);
-	if (errorCallback == nullptr)
-		errorCallback = getErrorPrintingCallback();
-
-	Common::String url = "https://cloud.scummvm.org/box/refresh/" + _refreshToken; // TODO: subject to change
-	Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(innerCallback, errorCallback, url);
-	addRequest(request);
-}
-
-void BoxStorage::tokenRefreshed(BoolCallback callback, Networking::JsonResponse response) {
-	Common::JSONValue *json = response.value;
-	if (!json) {
-		warning("BoxStorage: got NULL instead of JSON");
-		if (callback)
-			(*callback)(BoolResponse(nullptr, false));
-		delete callback;
-		return;
-	}
-
-	if (!Networking::CurlJsonRequest::jsonIsObject(json, "BoxStorage")) {
-		if (callback)
-			(*callback)(BoolResponse(nullptr, false));
-		delete json;
-		delete callback;
-		return;
-	}
-
-	Common::JSONObject result = json->asObject();
-	if (!Networking::CurlJsonRequest::jsonContainsString(result, "access_token", "BoxStorage") ||
-		!Networking::CurlJsonRequest::jsonContainsString(result, "refresh_token", "BoxStorage")) {
-		warning("BoxStorage: bad response, no token passed");
-		debug(9, "%s", json->stringify().c_str());
-		if (callback)
-			(*callback)(BoolResponse(nullptr, false));
-	} else {
-		_token = result.getVal("access_token")->asString();
-		_refreshToken = result.getVal("refresh_token")->asString();
-		CloudMan.save(); //ask CloudManager to save our new refreshToken
-		if (callback)
-			(*callback)(BoolResponse(nullptr, true));
-	}
-	delete json;
-	delete callback;
-}
+bool BoxStorage::needsRefreshToken() { return true; }
 
 void BoxStorage::saveConfig(Common::String keyPrefix) {
 	ConfMan.set(keyPrefix + "access_token", _token, ConfMan.kCloudDomain);
diff --git a/backends/cloud/box/boxstorage.h b/backends/cloud/box/boxstorage.h
index e22624a..a6ceb52 100644
--- a/backends/cloud/box/boxstorage.h
+++ b/backends/cloud/box/boxstorage.h
@@ -33,8 +33,6 @@ class BoxStorage: public Id::IdStorage {
 	/** This private constructor is called from loadFromConfig(). */
 	BoxStorage(Common::String token, Common::String refreshToken);
 
-	void tokenRefreshed(BoolCallback callback, Networking::JsonResponse response);
-
 	/** Constructs StorageInfo based on JSON response from cloud. */
 	void infoInnerCallback(StorageInfoCallback outerCallback, Networking::JsonResponse json);
 
@@ -51,6 +49,8 @@ protected:
 	 */
 	virtual uint32 storageIndex();
 
+	virtual bool needsRefreshToken();
+
 public:
 	/** This constructor uses OAuth code flow to get tokens. */
 	BoxStorage(Common::String code);
@@ -104,12 +104,6 @@ public:
 
 	virtual Common::String getRootDirectoryId();
 
-	/**
-	 * Gets new access_token. Pass a callback, so you could
-	 * continue your work when new token is available.
-	 */
-	void refreshAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback = nullptr);
-
 	Common::String accessToken() const { return _token; }
 };
 
diff --git a/backends/cloud/dropbox/dropboxstorage.cpp b/backends/cloud/dropbox/dropboxstorage.cpp
index 459f4dc..8941dff 100644
--- a/backends/cloud/dropbox/dropboxstorage.cpp
+++ b/backends/cloud/dropbox/dropboxstorage.cpp
@@ -52,6 +52,8 @@ Common::String DropboxStorage::cloudProvider() { return "dropbox"; }
 
 uint32 DropboxStorage::storageIndex() { return kStorageDropboxId; }
 
+bool DropboxStorage::needsRefreshToken() { return false; }
+
 void DropboxStorage::saveConfig(Common::String keyPrefix) {
 	ConfMan.set(keyPrefix + "access_token", _token, ConfMan.kCloudDomain);
 }
diff --git a/backends/cloud/dropbox/dropboxstorage.h b/backends/cloud/dropbox/dropboxstorage.h
index b82e38e..bd9f284 100644
--- a/backends/cloud/dropbox/dropboxstorage.h
+++ b/backends/cloud/dropbox/dropboxstorage.h
@@ -45,6 +45,8 @@ protected:
 	 */
 	virtual uint32 storageIndex();
 
+	virtual bool needsRefreshToken();
+
 public:
 	/** This constructor uses OAuth code flow to get tokens. */
 	DropboxStorage(Common::String code);
diff --git a/backends/cloud/googledrive/googledrivestorage.cpp b/backends/cloud/googledrive/googledrivestorage.cpp
index a2f688a..be81629 100644
--- a/backends/cloud/googledrive/googledrivestorage.cpp
+++ b/backends/cloud/googledrive/googledrivestorage.cpp
@@ -56,60 +56,7 @@ Common::String GoogleDriveStorage::cloudProvider() { return "gdrive"; }
 
 uint32 GoogleDriveStorage::storageIndex() { return kStorageGoogleDriveId; }
 
-void GoogleDriveStorage::refreshAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback) {
-	if (_refreshToken == "") {
-		warning("GoogleDriveStorage: no refresh token available to get new access token.");
-		if (callback)
-			(*callback)(BoolResponse(nullptr, false));
-		return;
-	}
-
-	Networking::JsonCallback innerCallback = new Common::CallbackBridge<GoogleDriveStorage, BoolResponse, Networking::JsonResponse>(this, &GoogleDriveStorage::tokenRefreshed, callback);
-	if (errorCallback == nullptr)
-		errorCallback = getErrorPrintingCallback();
-
-	Common::String url = "https://cloud.scummvm.org/gdrive/refresh/" + _refreshToken; // TODO: subject to change
-	Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(innerCallback, errorCallback, url);
-	addRequest(request);
-}
-
-void GoogleDriveStorage::tokenRefreshed(BoolCallback callback, Networking::JsonResponse response) {
-	Common::JSONValue *json = response.value;
-	if (!json) {
-		warning("GoogleDriveStorage: got NULL instead of JSON");
-		if (callback)
-			(*callback)(BoolResponse(nullptr, false));
-		delete callback;
-		return;
-	}
-
-	if (!Networking::CurlJsonRequest::jsonIsObject(json, "GoogleDriveStorage")) {
-		if (callback)
-			(*callback)(BoolResponse(nullptr, false));
-		delete json;
-		delete callback;
-		return;
-	}
-
-	Common::JSONObject result = json->asObject();
-	if (!Networking::CurlJsonRequest::jsonContainsString(result, "access_token", "GoogleDriveStorage")) {
-		warning("GoogleDriveStorage: bad response, no token passed");
-		debug(9, "%s", json->stringify().c_str());
-		if (callback)
-			(*callback)(BoolResponse(nullptr, false));
-	} else {
-		_token = result.getVal("access_token")->asString();
-		if (!Networking::CurlJsonRequest::jsonContainsString(result, "refresh_token", "GoogleDriveStorage"))
-			warning("GoogleDriveStorage: no refresh_token passed");
-		else
-			_refreshToken = result.getVal("refresh_token")->asString();
-		CloudMan.save(); //ask CloudManager to save our new refreshToken
-		if (callback)
-			(*callback)(BoolResponse(nullptr, true));
-	}
-	delete json;
-	delete callback;
-}
+bool GoogleDriveStorage::needsRefreshToken() { return true; }
 
 void GoogleDriveStorage::saveConfig(Common::String keyPrefix) {
 	ConfMan.set(keyPrefix + "access_token", _token, ConfMan.kCloudDomain);
diff --git a/backends/cloud/googledrive/googledrivestorage.h b/backends/cloud/googledrive/googledrivestorage.h
index 30bc9ab..730e8e2 100644
--- a/backends/cloud/googledrive/googledrivestorage.h
+++ b/backends/cloud/googledrive/googledrivestorage.h
@@ -33,8 +33,6 @@ class GoogleDriveStorage: public Id::IdStorage {
 	/** This private constructor is called from loadFromConfig(). */
 	GoogleDriveStorage(Common::String token, Common::String refreshToken);
 
-	void tokenRefreshed(BoolCallback callback, Networking::JsonResponse response);
-
 	/** Constructs StorageInfo based on JSON response from cloud. */
 	void infoInnerCallback(StorageInfoCallback outerCallback, Networking::JsonResponse json);
 
@@ -54,6 +52,8 @@ protected:
 	 */
 	virtual uint32 storageIndex();
 
+	virtual bool needsRefreshToken();
+
 public:
 	/** This constructor uses OAuth code flow to get tokens. */
 	GoogleDriveStorage(Common::String code);
@@ -106,12 +106,6 @@ public:
 
 	virtual Common::String getRootDirectoryId();
 
-	/**
-	 * Gets new access_token. Pass a callback, so you could
-	 * continue your work when new token is available.
-	 */
-	void refreshAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback = nullptr);
-
 	Common::String accessToken() const { return _token; }
 };
 
diff --git a/backends/cloud/onedrive/onedrivestorage.cpp b/backends/cloud/onedrive/onedrivestorage.cpp
index 779797a..8a2a8a5 100644
--- a/backends/cloud/onedrive/onedrivestorage.cpp
+++ b/backends/cloud/onedrive/onedrivestorage.cpp
@@ -42,7 +42,7 @@ namespace OneDrive {
 #define ONEDRIVE_API_SPECIAL_APPROOT_ID "https://api.onedrive.com/v1.0/drive/special/approot:/"
 #define ONEDRIVE_API_SPECIAL_APPROOT "https://api.onedrive.com/v1.0/drive/special/approot"
 
-OneDriveStorage::OneDriveStorage(Common::String token,  Common::String refreshToken):
+OneDriveStorage::OneDriveStorage(Common::String token, Common::String refreshToken):
 	BaseStorage(token, refreshToken) {}
 
 OneDriveStorage::OneDriveStorage(Common::String code) {
@@ -55,59 +55,7 @@ Common::String OneDriveStorage::cloudProvider() { return "onedrive"; }
 
 uint32 OneDriveStorage::storageIndex() { return kStorageOneDriveId; }
 
-void OneDriveStorage::refreshAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback) {
-	if (_refreshToken == "") {
-		warning("OneDriveStorage: no refresh token available to get new access token.");
-		if (callback)
-			(*callback)(BoolResponse(nullptr, false));
-		return;
-	}
-
-	Networking::JsonCallback innerCallback = new Common::CallbackBridge<OneDriveStorage, BoolResponse, Networking::JsonResponse>(this, &OneDriveStorage::tokenRefreshed, callback);
-	if (errorCallback == nullptr)
-		errorCallback = getErrorPrintingCallback();
-
-	Common::String url = "https://cloud.scummvm.org/onedrive/refresh/" + _refreshToken; // TODO: subject to change
-	Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(innerCallback, errorCallback, url);
-	addRequest(request);
-}
-
-void OneDriveStorage::tokenRefreshed(BoolCallback callback, Networking::JsonResponse response) {
-	Common::JSONValue *json = response.value;
-	if (!json) {
-		warning("OneDriveStorage: got NULL instead of JSON");
-		if (callback)
-			(*callback)(BoolResponse(nullptr, false));
-		delete callback;
-		return;
-	}
-
-	if (!Networking::CurlJsonRequest::jsonIsObject(json, "OneDriveStorage")) {
-		if (callback)
-			(*callback)(BoolResponse(nullptr, false));
-		delete json;
-		delete callback;
-		return;
-	}
-
-	Common::JSONObject result = json->asObject();
-	if (!Networking::CurlJsonRequest::jsonContainsString(result, "access_token", "OneDriveStorage") ||
-		!Networking::CurlJsonRequest::jsonContainsString(result, "user_id", "OneDriveStorage") ||
-		!Networking::CurlJsonRequest::jsonContainsString(result, "refresh_token", "OneDriveStorage")) {
-		warning("OneDriveStorage: bad response, no token or user_id passed");
-		debug(9, "%s", json->stringify().c_str());
-		if (callback)
-			(*callback)(BoolResponse(nullptr, false));
-	} else {
-		_token = result.getVal("access_token")->asString();
-		_refreshToken = result.getVal("refresh_token")->asString();
-		CloudMan.save(); //ask CloudManager to save our new refreshToken
-		if (callback)
-			(*callback)(BoolResponse(nullptr, true));
-	}
-	delete json;
-	delete callback;
-}
+bool OneDriveStorage::needsRefreshToken() { return true; }
 
 void OneDriveStorage::saveConfig(Common::String keyPrefix) {
 	ConfMan.set(keyPrefix + "access_token", _token, ConfMan.kCloudDomain);
diff --git a/backends/cloud/onedrive/onedrivestorage.h b/backends/cloud/onedrive/onedrivestorage.h
index 2dab86e..5059e0d 100644
--- a/backends/cloud/onedrive/onedrivestorage.h
+++ b/backends/cloud/onedrive/onedrivestorage.h
@@ -33,8 +33,6 @@ class OneDriveStorage: public Cloud::BaseStorage {
 	/** This private constructor is called from loadFromConfig(). */
 	OneDriveStorage(Common::String token, Common::String refreshToken);
 
-	void tokenRefreshed(BoolCallback callback, Networking::JsonResponse response);
-
 	/** Constructs StorageInfo based on JSON response from cloud. */
 	void infoInnerCallback(StorageInfoCallback outerCallback, Networking::JsonResponse json);
 
@@ -51,6 +49,8 @@ protected:
 	 */
 	virtual uint32 storageIndex();
 
+	virtual bool needsRefreshToken();
+
 public:
 	/** This constructor uses OAuth code flow to get tokens. */
 	OneDriveStorage(Common::String code);
@@ -101,12 +101,6 @@ public:
 	 */
 	static OneDriveStorage *loadFromConfig(Common::String keyPrefix);
 
-	/**
-	 * Gets new access_token. Pass a callback, so you could
-	 * continue your work when new token is available.
-	 */
-	void refreshAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback = nullptr);
-
 	Common::String accessToken() const { return _token; }
 };
 


Commit: e8669f693c6adbb33f515d9a4ce9d1079756e2e8
    https://github.com/scummvm/scummvm/commit/e8669f693c6adbb33f515d9a4ce9d1079756e2e8
Author: Alexander Tkachev (alexander at tkachov.ru)
Date: 2019-07-30T14:51:41-04:00

Commit Message:
CLOUD: Update BaseStorage to expect no refresh_token

While refreshing access_token, some cloud providers also pass a new
refresh_token. Google Drive does not, and accepts the same refresh_token
next time. These changes allow this to happen.

Changed paths:
    backends/cloud/basestorage.cpp
    backends/cloud/basestorage.h
    backends/cloud/box/boxstorage.cpp
    backends/cloud/box/boxstorage.h
    backends/cloud/dropbox/dropboxstorage.cpp
    backends/cloud/dropbox/dropboxstorage.h
    backends/cloud/googledrive/googledrivestorage.cpp
    backends/cloud/googledrive/googledrivestorage.h
    backends/cloud/onedrive/onedrivestorage.cpp
    backends/cloud/onedrive/onedrivestorage.h


diff --git a/backends/cloud/basestorage.cpp b/backends/cloud/basestorage.cpp
index 035098f..e856bbe 100644
--- a/backends/cloud/basestorage.cpp
+++ b/backends/cloud/basestorage.cpp
@@ -101,7 +101,7 @@ void BaseStorage::codeFlowComplete(Networking::JsonResponse response) {
 		return;
 	}
 
-	debug(9, "%s", json->stringify(true).c_str()); // TODO: remove before commit
+	debug(9, "%s", json->stringify(true).c_str()); // TODO: remove when done testing against cloud.scummvm.org
 	_token = oauth.getVal("access_token")->asString();
 	if (requiresRefreshToken) {
 		_refreshToken = oauth.getVal("refresh_token")->asString();
@@ -188,7 +188,7 @@ void BaseStorage::tokenRefreshed(BoolCallback callback, Networking::JsonResponse
 	}
 
 	Common::JSONObject oauth = result.getVal("oauth")->asObject();
-	bool requiresRefreshToken = needsRefreshToken(); // TODO: it seems Google Drive might not send new refresh token, and still accept old one
+	bool requiresRefreshToken = !canReuseRefreshToken();
 	if (!Networking::CurlJsonRequest::jsonContainsString(oauth, "access_token", "BaseStorage::tokenRefreshed") ||
 		!Networking::CurlJsonRequest::jsonContainsString(oauth, "refresh_token", "BaseStorage::tokenRefreshed", !requiresRefreshToken)) {
 		warning("BaseStorage: bad response, no 'access_token' or 'refresh_token' attribute passed");
@@ -200,6 +200,8 @@ void BaseStorage::tokenRefreshed(BoolCallback callback, Networking::JsonResponse
 		return;
 	}
 
+	debug(9, "%s", json->stringify(true).c_str()); // TODO: remove when done testing against cloud.scummvm.org
+
 	_token = oauth.getVal("access_token")->asString();
 	if (requiresRefreshToken) {
 		_refreshToken = oauth.getVal("refresh_token")->asString();
diff --git a/backends/cloud/basestorage.h b/backends/cloud/basestorage.h
index 4b3b497..243e7f4 100644
--- a/backends/cloud/basestorage.h
+++ b/backends/cloud/basestorage.h
@@ -69,6 +69,11 @@ protected:
 	 */
 	virtual bool needsRefreshToken() = 0;
 
+	/**
+	 * Return whether to expect new refresh_token on refresh.
+	 */
+	virtual bool canReuseRefreshToken() = 0;
+
 private:
 	void tokenRefreshed(BoolCallback callback, Networking::JsonResponse response);
 
diff --git a/backends/cloud/box/boxstorage.cpp b/backends/cloud/box/boxstorage.cpp
index 42e9029..f76fa3a 100644
--- a/backends/cloud/box/boxstorage.cpp
+++ b/backends/cloud/box/boxstorage.cpp
@@ -57,6 +57,8 @@ uint32 BoxStorage::storageIndex() { return kStorageBoxId; }
 
 bool BoxStorage::needsRefreshToken() { return true; }
 
+bool BoxStorage::canReuseRefreshToken() { return false; }
+
 void BoxStorage::saveConfig(Common::String keyPrefix) {
 	ConfMan.set(keyPrefix + "access_token", _token, ConfMan.kCloudDomain);
 	ConfMan.set(keyPrefix + "refresh_token", _refreshToken, ConfMan.kCloudDomain);
diff --git a/backends/cloud/box/boxstorage.h b/backends/cloud/box/boxstorage.h
index a6ceb52..a8fd32c 100644
--- a/backends/cloud/box/boxstorage.h
+++ b/backends/cloud/box/boxstorage.h
@@ -51,6 +51,8 @@ protected:
 
 	virtual bool needsRefreshToken();
 
+	virtual bool canReuseRefreshToken();
+
 public:
 	/** This constructor uses OAuth code flow to get tokens. */
 	BoxStorage(Common::String code);
diff --git a/backends/cloud/dropbox/dropboxstorage.cpp b/backends/cloud/dropbox/dropboxstorage.cpp
index 8941dff..c12dec9 100644
--- a/backends/cloud/dropbox/dropboxstorage.cpp
+++ b/backends/cloud/dropbox/dropboxstorage.cpp
@@ -54,6 +54,8 @@ uint32 DropboxStorage::storageIndex() { return kStorageDropboxId; }
 
 bool DropboxStorage::needsRefreshToken() { return false; }
 
+bool DropboxStorage::canReuseRefreshToken() { return false; }
+
 void DropboxStorage::saveConfig(Common::String keyPrefix) {
 	ConfMan.set(keyPrefix + "access_token", _token, ConfMan.kCloudDomain);
 }
diff --git a/backends/cloud/dropbox/dropboxstorage.h b/backends/cloud/dropbox/dropboxstorage.h
index bd9f284..bca83d2 100644
--- a/backends/cloud/dropbox/dropboxstorage.h
+++ b/backends/cloud/dropbox/dropboxstorage.h
@@ -47,6 +47,8 @@ protected:
 
 	virtual bool needsRefreshToken();
 
+	virtual bool canReuseRefreshToken();
+
 public:
 	/** This constructor uses OAuth code flow to get tokens. */
 	DropboxStorage(Common::String code);
diff --git a/backends/cloud/googledrive/googledrivestorage.cpp b/backends/cloud/googledrive/googledrivestorage.cpp
index be81629..67d1577 100644
--- a/backends/cloud/googledrive/googledrivestorage.cpp
+++ b/backends/cloud/googledrive/googledrivestorage.cpp
@@ -58,6 +58,8 @@ uint32 GoogleDriveStorage::storageIndex() { return kStorageGoogleDriveId; }
 
 bool GoogleDriveStorage::needsRefreshToken() { return true; }
 
+bool GoogleDriveStorage::canReuseRefreshToken() { return true; }
+
 void GoogleDriveStorage::saveConfig(Common::String keyPrefix) {
 	ConfMan.set(keyPrefix + "access_token", _token, ConfMan.kCloudDomain);
 	ConfMan.set(keyPrefix + "refresh_token", _refreshToken, ConfMan.kCloudDomain);
diff --git a/backends/cloud/googledrive/googledrivestorage.h b/backends/cloud/googledrive/googledrivestorage.h
index 730e8e2..21e027c 100644
--- a/backends/cloud/googledrive/googledrivestorage.h
+++ b/backends/cloud/googledrive/googledrivestorage.h
@@ -54,6 +54,8 @@ protected:
 
 	virtual bool needsRefreshToken();
 
+	virtual bool canReuseRefreshToken();
+
 public:
 	/** This constructor uses OAuth code flow to get tokens. */
 	GoogleDriveStorage(Common::String code);
diff --git a/backends/cloud/onedrive/onedrivestorage.cpp b/backends/cloud/onedrive/onedrivestorage.cpp
index 8a2a8a5..fbaa675 100644
--- a/backends/cloud/onedrive/onedrivestorage.cpp
+++ b/backends/cloud/onedrive/onedrivestorage.cpp
@@ -57,6 +57,8 @@ uint32 OneDriveStorage::storageIndex() { return kStorageOneDriveId; }
 
 bool OneDriveStorage::needsRefreshToken() { return true; }
 
+bool OneDriveStorage::canReuseRefreshToken() { return false; }
+
 void OneDriveStorage::saveConfig(Common::String keyPrefix) {
 	ConfMan.set(keyPrefix + "access_token", _token, ConfMan.kCloudDomain);
 	ConfMan.set(keyPrefix + "refresh_token", _refreshToken, ConfMan.kCloudDomain);
diff --git a/backends/cloud/onedrive/onedrivestorage.h b/backends/cloud/onedrive/onedrivestorage.h
index 5059e0d..4b18929 100644
--- a/backends/cloud/onedrive/onedrivestorage.h
+++ b/backends/cloud/onedrive/onedrivestorage.h
@@ -51,6 +51,8 @@ protected:
 
 	virtual bool needsRefreshToken();
 
+	virtual bool canReuseRefreshToken();
+
 public:
 	/** This constructor uses OAuth code flow to get tokens. */
 	OneDriveStorage(Common::String code);


Commit: edbea10c2e5606daec18c148c8b103649d1011c5
    https://github.com/scummvm/scummvm/commit/edbea10c2e5606daec18c148c8b103649d1011c5
Author: Alexander Tkachev (alexander at tkachov.ru)
Date: 2019-07-30T14:51:41-04:00

Commit Message:
CLOUD: Fix OneDriveStorage API interaction

Something changed and old API endpoint "api.onedrive.com" now does not
work. The other one, "graph.microsoft.com", does, but there were some
other changes in JSON it returns. These changes are also in this commit.

Changed paths:
    backends/cloud/onedrive/onedrivecreatedirectoryrequest.cpp
    backends/cloud/onedrive/onedrivelistdirectoryrequest.cpp
    backends/cloud/onedrive/onedrivestorage.cpp
    backends/cloud/onedrive/onedrivetokenrefresher.cpp
    backends/cloud/onedrive/onedrivetokenrefresher.h
    backends/cloud/onedrive/onedriveuploadrequest.cpp
    backends/cloud/savessyncrequest.cpp


diff --git a/backends/cloud/onedrive/onedrivecreatedirectoryrequest.cpp b/backends/cloud/onedrive/onedrivecreatedirectoryrequest.cpp
index 74cf320..f7e995f 100644
--- a/backends/cloud/onedrive/onedrivecreatedirectoryrequest.cpp
+++ b/backends/cloud/onedrive/onedrivecreatedirectoryrequest.cpp
@@ -31,7 +31,7 @@
 namespace Cloud {
 namespace OneDrive {
 
-#define ONEDRIVE_API_SPECIAL_APPROOT "https://api.onedrive.com/v1.0/drive/special/approot"
+#define ONEDRIVE_API_SPECIAL_APPROOT "https://graph.microsoft.com/v1.0/drive/special/approot"
 
 OneDriveCreateDirectoryRequest::OneDriveCreateDirectoryRequest(OneDriveStorage *storage, Common::String path, Storage::BoolCallback cb, Networking::ErrorCallback ecb):
 	Networking::Request(nullptr, ecb), _storage(storage), _path(path), _boolCallback(cb),
diff --git a/backends/cloud/onedrive/onedrivelistdirectoryrequest.cpp b/backends/cloud/onedrive/onedrivelistdirectoryrequest.cpp
index 953845d..f160976 100644
--- a/backends/cloud/onedrive/onedrivelistdirectoryrequest.cpp
+++ b/backends/cloud/onedrive/onedrivelistdirectoryrequest.cpp
@@ -31,7 +31,8 @@
 namespace Cloud {
 namespace OneDrive {
 
-#define ONEDRIVE_API_SPECIAL_APPROOT_CHILDREN "https://api.onedrive.com/v1.0/drive/special/approot:/%s:/children"
+#define ONEDRIVE_API_SPECIAL_APPROOT_CHILDREN "https://graph.microsoft.com/v1.0/drive/special/approot:/%s:/children"
+#define ONEDRIVE_API_SPECIAL_APPROOT_CHILDREN_ROOT_ITSELF "https://graph.microsoft.com/v1.0/drive/special/approot/children"
 
 OneDriveListDirectoryRequest::OneDriveListDirectoryRequest(OneDriveStorage *storage, Common::String path, Storage::ListDirectoryCallback cb, Networking::ErrorCallback ecb, bool recursive):
 	Networking::Request(nullptr, ecb),
@@ -77,6 +78,7 @@ void OneDriveListDirectoryRequest::listNextDirectory() {
 	Common::String dir = _currentDirectory;
 	dir.deleteLastChar();
 	Common::String url = Common::String::format(ONEDRIVE_API_SPECIAL_APPROOT_CHILDREN, ConnMan.urlEncode(dir).c_str());
+	if (dir == "") url = Common::String(ONEDRIVE_API_SPECIAL_APPROOT_CHILDREN_ROOT_ITSELF);
 	makeRequest(url);
 }
 
@@ -84,7 +86,7 @@ void OneDriveListDirectoryRequest::makeRequest(Common::String url) {
 	Networking::JsonCallback callback = new Common::Callback<OneDriveListDirectoryRequest, Networking::JsonResponse>(this, &OneDriveListDirectoryRequest::listedDirectoryCallback);
 	Networking::ErrorCallback failureCallback = new Common::Callback<OneDriveListDirectoryRequest, Networking::ErrorResponse>(this, &OneDriveListDirectoryRequest::listedDirectoryErrorCallback);
 	Networking::CurlJsonRequest *request = new OneDriveTokenRefresher(_storage, callback, failureCallback, url.c_str());
-	request->addHeader("Authorization: Bearer " + _storage->accessToken());
+	request->addHeader("Authorization: bearer " + _storage->accessToken());
 	_workingRequest = ConnMan.addRequest(request);
 }
 
diff --git a/backends/cloud/onedrive/onedrivestorage.cpp b/backends/cloud/onedrive/onedrivestorage.cpp
index fbaa675..48c3a10 100644
--- a/backends/cloud/onedrive/onedrivestorage.cpp
+++ b/backends/cloud/onedrive/onedrivestorage.cpp
@@ -39,8 +39,8 @@
 namespace Cloud {
 namespace OneDrive {
 
-#define ONEDRIVE_API_SPECIAL_APPROOT_ID "https://api.onedrive.com/v1.0/drive/special/approot:/"
-#define ONEDRIVE_API_SPECIAL_APPROOT "https://api.onedrive.com/v1.0/drive/special/approot"
+#define ONEDRIVE_API_SPECIAL_APPROOT_ID "https://graph.microsoft.com/v1.0/drive/special/approot:/"
+#define ONEDRIVE_API_SPECIAL_APPROOT "https://graph.microsoft.com/v1.0/drive/special/approot"
 
 OneDriveStorage::OneDriveStorage(Common::String token, Common::String refreshToken):
 	BaseStorage(token, refreshToken) {}
@@ -136,7 +136,7 @@ void OneDriveStorage::fileInfoCallback(Networking::NetworkReadStreamCallback out
 	}
 
 	Common::JSONObject result = response.value->asObject();
-	if (!Networking::CurlJsonRequest::jsonContainsString(result, "@content.downloadUrl", "OneDriveStorage::fileInfoCallback")) {
+	if (!Networking::CurlJsonRequest::jsonContainsString(result, "@microsoft.graph.downloadUrl", "OneDriveStorage::fileInfoCallback")) {
 		warning("OneDriveStorage: downloadUrl not found in passed JSON");
 		debug(9, "%s", response.value->stringify().c_str());
 		if (outerCallback)
@@ -146,7 +146,7 @@ void OneDriveStorage::fileInfoCallback(Networking::NetworkReadStreamCallback out
 		return;
 	}
 
-	const char *url = result.getVal("@content.downloadUrl")->asString().c_str();
+	const char *url = result.getVal("@microsoft.graph.downloadUrl")->asString().c_str();
 	if (outerCallback)
 		(*outerCallback)(Networking::NetworkReadStreamResponse(
 			response.request,
@@ -158,28 +158,33 @@ void OneDriveStorage::fileInfoCallback(Networking::NetworkReadStreamCallback out
 }
 
 Networking::Request *OneDriveStorage::listDirectory(Common::String path, ListDirectoryCallback callback, Networking::ErrorCallback errorCallback, bool recursive) {
+	debug(9, "OneDrive: `ls \"%s\"`", path.c_str());
 	return addRequest(new OneDriveListDirectoryRequest(this, path, callback, errorCallback, recursive));
 }
 
 Networking::Request *OneDriveStorage::upload(Common::String path, Common::SeekableReadStream *contents, UploadCallback callback, Networking::ErrorCallback errorCallback) {
+	debug(9, "OneDrive: `upload \"%s\"`", path.c_str());
 	return addRequest(new OneDriveUploadRequest(this, path, contents, callback, errorCallback));
 }
 
 Networking::Request *OneDriveStorage::streamFileById(Common::String path, Networking::NetworkReadStreamCallback outerCallback, Networking::ErrorCallback errorCallback) {
+	debug(9, "OneDrive: `download \"%s\"`", path.c_str());
 	Common::String url = ONEDRIVE_API_SPECIAL_APPROOT_ID + ConnMan.urlEncode(path);
 	Networking::JsonCallback innerCallback = new Common::CallbackBridge<OneDriveStorage, Networking::NetworkReadStreamResponse, Networking::JsonResponse>(this, &OneDriveStorage::fileInfoCallback, outerCallback);
 	Networking::CurlJsonRequest *request = new OneDriveTokenRefresher(this, innerCallback, errorCallback, url.c_str());
-	request->addHeader("Authorization: Bearer " + _token);
+	request->addHeader("Authorization: bearer " + _token);
 	return addRequest(request);
 }
 
 Networking::Request *OneDriveStorage::createDirectory(Common::String path, BoolCallback callback, Networking::ErrorCallback errorCallback) {
+	debug(9, "OneDrive: `mkdir \"%s\"`", path.c_str());
 	if (!errorCallback)
 		errorCallback = getErrorPrintingCallback();
 	return addRequest(new OneDriveCreateDirectoryRequest(this, path, callback, errorCallback));
 }
 
 Networking::Request *OneDriveStorage::info(StorageInfoCallback callback, Networking::ErrorCallback errorCallback) {
+	debug(9, "OneDrive: `info`");
 	Networking::JsonCallback innerCallback = new Common::CallbackBridge<OneDriveStorage, StorageInfoResponse, Networking::JsonResponse>(this, &OneDriveStorage::infoInnerCallback, callback);
 	Networking::CurlJsonRequest *request = new OneDriveTokenRefresher(this, innerCallback, errorCallback, ONEDRIVE_API_SPECIAL_APPROOT);
 	request->addHeader("Authorization: bearer " + _token);
diff --git a/backends/cloud/onedrive/onedrivetokenrefresher.cpp b/backends/cloud/onedrive/onedrivetokenrefresher.cpp
index 1404f76..d17e352 100644
--- a/backends/cloud/onedrive/onedrivetokenrefresher.cpp
+++ b/backends/cloud/onedrive/onedrivetokenrefresher.cpp
@@ -94,7 +94,7 @@ void OneDriveTokenRefresher::finishJson(Common::JSONValue *json) {
 					irrecoverable = false;
 			}
 
-			if (code == "unauthenticated")
+			if (code == "unauthenticated" || code == "InvalidAuthenticationToken")
 				irrecoverable = false;
 
 			if (irrecoverable) {
@@ -114,6 +114,30 @@ void OneDriveTokenRefresher::finishJson(Common::JSONValue *json) {
 	CurlJsonRequest::finishJson(json);
 }
 
+void OneDriveTokenRefresher::finishError(Networking::ErrorResponse error) {
+	bool irrecoverable = error.interrupted || error.failed;
+	if (error.failed) {
+		Common::JSONValue *value = Common::JSON::parse(error.response.c_str());
+
+		//somehow OneDrive returns JSON with '.' in unexpected places, try fixing it
+		if (!value) {
+			Common::String fixedResponse = error.response;
+			for (uint32 i = 0; i < fixedResponse.size(); ++i) {
+				if (fixedResponse[i] == '.')
+					fixedResponse.replace(i, 1, " ");
+			}
+			value = Common::JSON::parse(fixedResponse.c_str());
+		}
+
+		if (value) {
+			finishJson(value);
+			return;
+		}
+	}
+
+	Request::finishError(error); //call closest base class's method
+}
+
 void OneDriveTokenRefresher::setHeaders(Common::Array<Common::String> &headers) {
 	_headers = headers;
 	curl_slist_free_all(_headersList);
diff --git a/backends/cloud/onedrive/onedrivetokenrefresher.h b/backends/cloud/onedrive/onedrivetokenrefresher.h
index d190bc4..b447379 100644
--- a/backends/cloud/onedrive/onedrivetokenrefresher.h
+++ b/backends/cloud/onedrive/onedrivetokenrefresher.h
@@ -38,6 +38,7 @@ class OneDriveTokenRefresher: public Networking::CurlJsonRequest {
 	void tokenRefreshed(Storage::BoolResponse response);
 
 	virtual void finishJson(Common::JSONValue *json);
+	virtual void finishError(Networking::ErrorResponse error);
 public:
 	OneDriveTokenRefresher(OneDriveStorage *parent, Networking::JsonCallback callback, Networking::ErrorCallback ecb, const char *url);
 	virtual ~OneDriveTokenRefresher();
diff --git a/backends/cloud/onedrive/onedriveuploadrequest.cpp b/backends/cloud/onedrive/onedriveuploadrequest.cpp
index ebf387f..85e0525 100644
--- a/backends/cloud/onedrive/onedriveuploadrequest.cpp
+++ b/backends/cloud/onedrive/onedriveuploadrequest.cpp
@@ -33,8 +33,8 @@
 namespace Cloud {
 namespace OneDrive {
 
-#define ONEDRIVE_API_SPECIAL_APPROOT_UPLOAD "https://api.onedrive.com/v1.0/drive/special/approot:/%s:/upload.createSession"
-#define ONEDRIVE_API_SPECIAL_APPROOT_CONTENT "https://api.onedrive.com/v1.0/drive/special/approot:/%s:/content"
+#define ONEDRIVE_API_SPECIAL_APPROOT_UPLOAD "https://graph.microsoft.com/v1.0/drive/special/approot:/%s:/upload.createSession"
+#define ONEDRIVE_API_SPECIAL_APPROOT_CONTENT "https://graph.microsoft.com/v1.0/drive/special/approot:/%s:/content"
 
 OneDriveUploadRequest::OneDriveUploadRequest(OneDriveStorage *storage, Common::String path, Common::SeekableReadStream *contents, Storage::UploadCallback callback, Networking::ErrorCallback ecb):
 	Networking::Request(nullptr, ecb), _storage(storage), _savePath(path), _contentsStream(contents), _uploadCallback(callback),
diff --git a/backends/cloud/savessyncrequest.cpp b/backends/cloud/savessyncrequest.cpp
index f9b16b3..00cc814 100644
--- a/backends/cloud/savessyncrequest.cpp
+++ b/backends/cloud/savessyncrequest.cpp
@@ -126,11 +126,11 @@ void SavesSyncRequest::directoryListedCallback(Storage::ListDirectoryResponse re
 			_filesToUpload.push_back(i->_key);
 	}
 
-	debug(9, "\nSavesSyncRequest: download files:");
+	debug(9, (_filesToDownload.size() > 0 ? "\nSavesSyncRequest: download files:" : "\nSavesSyncRequest: nothing to download"));
 	for (uint32 i = 0; i < _filesToDownload.size(); ++i) {
 		debug(9, "%s", _filesToDownload[i].name().c_str());
 	}
-	debug(9, "\nSavesSyncRequest: upload files:");
+	debug(9, (_filesToUpload.size() > 0 ? "\nSavesSyncRequest: upload files:" : "\nSavesSyncRequest: nothing to upload"));
 	for (uint32 i = 0; i < _filesToUpload.size(); ++i) {
 		debug(9, "%s", _filesToUpload[i].c_str());
 	}
@@ -145,9 +145,22 @@ void SavesSyncRequest::directoryListedErrorCallback(Networking::ErrorResponse er
 	if (_ignoreCallback)
 		return;
 
+	if (error.failed) debug(9, "%s", error.response.c_str());
+
 	bool irrecoverable = error.interrupted || error.failed;
 	if (error.failed) {
 		Common::JSONValue *value = Common::JSON::parse(error.response.c_str());
+
+		// somehow OneDrive returns JSON with '.' in unexpected places, try fixing it
+		if (!value) {
+			Common::String fixedResponse = error.response;
+			for (uint32 i = 0; i < fixedResponse.size(); ++i) {
+				if (fixedResponse[i] == '.')
+					fixedResponse.replace(i, 1, " ");
+			}
+			value = Common::JSON::parse(fixedResponse.c_str());
+		}
+
 		if (value) {
 			if (value->isObject()) {
 				Common::JSONObject object = value->asObject();
@@ -174,11 +187,13 @@ void SavesSyncRequest::directoryListedErrorCallback(Networking::ErrorResponse er
 			delete value;
 		}
 
-		//Google Drive and Box-related ScummVM-based error
+		//Google Drive, Box and OneDrive-related ScummVM-based error
 		if (error.response.contains("subdirectory not found")) {
 			irrecoverable = false; //base "/ScummVM/" folder not found
 		} else if (error.response.contains("no such file found in its parent directory")) {
 			irrecoverable = false; //"Saves" folder within "/ScummVM/" not found
+		} else if (error.response.contains("itemNotFound") && error.response.contains("Item does not exist")) {
+			irrecoverable = false; //"saves" folder within application folder is not found
 		}
 	}
 


Commit: c1124c9cdae37271413842bce79a992bf4238cf4
    https://github.com/scummvm/scummvm/commit/c1124c9cdae37271413842bce79a992bf4238cf4
Author: Alexander Tkachev (alexander at tkachov.ru)
Date: 2019-07-30T14:51:41-04:00

Commit Message:
CLOUD: Handle Cloud requests more frequently

ConnectionManager used to poll Cloud requests every second, while curl
requests were polled every 1/20th of a second. If curl request was over
at, say, frame #21, corresponding Cloud request would've only work with
that at frame #40 (950 ms later), which was making everything
cloud-related slower than it could be. This commit fixes it by making
Cloud polling period the same as curl polling period, and additionally
raises FPS to 25.

Changed paths:
    backends/networking/curl/connectionmanager.cpp
    backends/networking/curl/connectionmanager.h


diff --git a/backends/networking/curl/connectionmanager.cpp b/backends/networking/curl/connectionmanager.cpp
index 7698dda..34a9701 100644
--- a/backends/networking/curl/connectionmanager.cpp
+++ b/backends/networking/curl/connectionmanager.cpp
@@ -151,7 +151,8 @@ void ConnectionManager::interateRequests() {
 	_addedRequestsMutex.unlock();
 
 	//call handle() of all running requests (so they can do their work)
-	debug(9, "handling %d request(s)", _requests.size());
+	if (_frame % DEBUG_PRINT_PERIOD == 0)
+		debug(9, "handling %d request(s)", _requests.size());
 	for (Common::Array<RequestWithCallback>::iterator i = _requests.begin(); i != _requests.end();) {
 		Request *request = i->request;
 		if (request) {
diff --git a/backends/networking/curl/connectionmanager.h b/backends/networking/curl/connectionmanager.h
index f6a9fcb..6c261b8 100644
--- a/backends/networking/curl/connectionmanager.h
+++ b/backends/networking/curl/connectionmanager.h
@@ -38,10 +38,11 @@ namespace Networking {
 class NetworkReadStream;
 
 class ConnectionManager : public Common::Singleton<ConnectionManager> {
-	static const uint32 FRAMES_PER_SECOND = 20;
+	static const uint32 FRAMES_PER_SECOND = 25;
 	static const uint32 TIMER_INTERVAL = 1000000 / FRAMES_PER_SECOND;
-	static const uint32 CLOUD_PERIOD = 20; //every 20th frame
+	static const uint32 CLOUD_PERIOD = 1; //every frame
 	static const uint32 CURL_PERIOD = 1; //every frame
+	static const uint32 DEBUG_PRINT_PERIOD = FRAMES_PER_SECOND; // once per second
 
 	friend void connectionsThread(void *); //calls handle()
 


Commit: 1e92e7b9f3bd7a2fb55d77a7e28934abf3d02f19
    https://github.com/scummvm/scummvm/commit/1e92e7b9f3bd7a2fb55d77a7e28934abf3d02f19
Author: Alexander Tkachev (alexander at tkachov.ru)
Date: 2019-07-30T14:51:41-04:00

Commit Message:
CLOUD: Fix saves syncing

This mostly affects OneDrive saves syncing, because it is the only cloud
provider to return 0 as timestamp of (non-)created file. 0 is treated as
EOF in /saves/timestamps file, thus all timestamps after such 0
timestamps were ignored and files were reuploaded as being "new". This
commit also adds more verbose debug information on SavesSyncRequest
decisions making for easier debugging.

Changed paths:
    backends/cloud/savessyncrequest.cpp
    backends/saves/default/default-saves.cpp


diff --git a/backends/cloud/savessyncrequest.cpp b/backends/cloud/savessyncrequest.cpp
index 00cc814..8d34c21 100644
--- a/backends/cloud/savessyncrequest.cpp
+++ b/backends/cloud/savessyncrequest.cpp
@@ -90,6 +90,7 @@ void SavesSyncRequest::directoryListedCallback(Storage::ListDirectoryResponse re
 	//determine which files to download and which files to upload
 	Common::Array<StorageFile> &remoteFiles = response.value;
 	uint64 totalSize = 0;
+	debug(9, "SavesSyncRequest decisions:");
 	for (uint32 i = 0; i < remoteFiles.size(); ++i) {
 		StorageFile &file = remoteFiles[i];
 		if (file.isDirectory())
@@ -101,6 +102,7 @@ void SavesSyncRequest::directoryListedCallback(Storage::ListDirectoryResponse re
 		Common::String name = file.name();
 		if (!_localFilesTimestamps.contains(name)) {
 			_filesToDownload.push_back(file);
+			debug(9, "- downloading file %s, because it is not present on local", name.c_str());
 		} else {
 			localFileNotAvailableInCloud[name] = false;
 
@@ -113,6 +115,13 @@ void SavesSyncRequest::directoryListedCallback(Storage::ListDirectoryResponse re
 				_filesToUpload.push_back(file.name());
 			else
 				_filesToDownload.push_back(file);
+
+			if (_localFilesTimestamps[name] == DefaultSaveFileManager::INVALID_TIMESTAMP)
+				debug(9, "- uploading file %s, because it is has invalid timestamp", name.c_str());
+			else if (_localFilesTimestamps[name] > file.timestamp())
+				debug(9, "- uploading file %s, because it is %d seconds newer than remote\n\tlocal = %d; \tremote = %d", name.c_str(), _localFilesTimestamps[name] - file.timestamp(), _localFilesTimestamps[name], file.timestamp());
+			else
+				debug(9, "- downloading file %s, because it is %d seconds older than remote\n\tlocal = %d; \tremote = %", name.c_str(), file.timestamp() - _localFilesTimestamps[name], _localFilesTimestamps[name], file.timestamp());
 		}
 	}
 
@@ -122,17 +131,20 @@ void SavesSyncRequest::directoryListedCallback(Storage::ListDirectoryResponse re
 	for (Common::HashMap<Common::String, bool>::iterator i = localFileNotAvailableInCloud.begin(); i != localFileNotAvailableInCloud.end(); ++i) {
 		if (i->_key == DefaultSaveFileManager::TIMESTAMPS_FILENAME)
 			continue;
-		if (i->_value)
+		if (i->_value) {
 			_filesToUpload.push_back(i->_key);
+			debug(9, "- uploading file %s, because it is not present on remote", i->_key.c_str());
+		}
 	}
 
 	debug(9, (_filesToDownload.size() > 0 ? "\nSavesSyncRequest: download files:" : "\nSavesSyncRequest: nothing to download"));
 	for (uint32 i = 0; i < _filesToDownload.size(); ++i) {
-		debug(9, "%s", _filesToDownload[i].name().c_str());
+		debug(9, " %s", _filesToDownload[i].name().c_str());
 	}
-	debug(9, (_filesToUpload.size() > 0 ? "\nSavesSyncRequest: upload files:" : "\nSavesSyncRequest: nothing to upload"));
+	if (_filesToDownload.size() > 0) debug(9, "");
+	debug(9, (_filesToUpload.size() > 0 ? "SavesSyncRequest: upload files:" : "SavesSyncRequest: nothing to upload"));
 	for (uint32 i = 0; i < _filesToUpload.size(); ++i) {
-		debug(9, "%s", _filesToUpload[i].c_str());
+		debug(9, " %s", _filesToUpload[i].c_str());
 	}
 	_totalFilesToHandle = _filesToDownload.size() + _filesToUpload.size();
 
@@ -206,7 +218,7 @@ void SavesSyncRequest::directoryListedErrorCallback(Networking::ErrorResponse er
 	Common::String dir = _storage->savesDirectoryPath();
 	if (dir.lastChar() == '/')
 		dir.deleteLastChar();
-	debug(9, "SavesSyncRequest: creating %s", dir.c_str());
+	debug(9, "\nSavesSyncRequest: creating %s", dir.c_str());
 	_workingRequest = _storage->createDirectory(
 		dir,
 		new Common::Callback<SavesSyncRequest, Storage::BoolResponse>(this, &SavesSyncRequest::directoryCreatedCallback),
@@ -254,7 +266,7 @@ void SavesSyncRequest::downloadNextFile() {
 
 	sendCommand(GUI::kSavesSyncProgressCmd, (int)(getDownloadingProgress() * 100));
 
-	debug(9, "SavesSyncRequest: downloading %s (%d %%)", _currentDownloadingFile.name().c_str(), (int)(getProgress() * 100));
+	debug(9, "\nSavesSyncRequest: downloading %s (%d %%)", _currentDownloadingFile.name().c_str(), (int)(getProgress() * 100));
 	_workingRequest = _storage->downloadById(
 		_currentDownloadingFile.id(),
 		DefaultSaveFileManager::concatWithSavesPath(_currentDownloadingFile.name()),
@@ -305,7 +317,7 @@ void SavesSyncRequest::uploadNextFile() {
 	_currentUploadingFile = _filesToUpload.back();
 	_filesToUpload.pop_back();
 
-	debug(9, "SavesSyncRequest: uploading %s (%d %%)", _currentUploadingFile.c_str(), (int)(getProgress() * 100));
+	debug(9, "\nSavesSyncRequest: uploading %s (%d %%)", _currentUploadingFile.c_str(), (int)(getProgress() * 100));
 	if (_storage->uploadStreamSupported()) {
 		_workingRequest = _storage->upload(
 			_storage->savesDirectoryPath() + _currentUploadingFile,
diff --git a/backends/saves/default/default-saves.cpp b/backends/saves/default/default-saves.cpp
index 44fd48b..e9a4165 100644
--- a/backends/saves/default/default-saves.cpp
+++ b/backends/saves/default/default-saves.cpp
@@ -360,7 +360,10 @@ void DefaultSaveFileManager::saveTimestamps(Common::HashMap<Common::String, uint
 	}
 
 	for (Common::HashMap<Common::String, uint32>::iterator i = timestamps.begin(); i != timestamps.end(); ++i) {
-		Common::String data = i->_key + Common::String::format(" %u\n", i->_value);
+		uint32 v = i->_value;
+		if (v < 1) v = 1; // 0 timestamp is treated as EOF up there, so we should never save zeros
+
+		Common::String data = i->_key + Common::String::format(" %u\n", v);
 		if (f.write(data.c_str(), data.size()) != data.size()) {
 			warning("DefaultSaveFileManager: failed to write timestamps data into '%s'", filename.c_str());
 			return;


Commit: 72c7f8226c4295fb103e5e32d4e96b12659ab67b
    https://github.com/scummvm/scummvm/commit/72c7f8226c4295fb103e5e32d4e96b12659ab67b
Author: Alexander Tkachev (alexander at tkachov.ru)
Date: 2019-07-30T14:51:41-04:00

Commit Message:
CLOUD: Minor cleanup in OneDriveTokenRefresher

Changed paths:
    backends/cloud/onedrive/onedrivetokenrefresher.cpp


diff --git a/backends/cloud/onedrive/onedrivetokenrefresher.cpp b/backends/cloud/onedrive/onedrivetokenrefresher.cpp
index d17e352..1654869 100644
--- a/backends/cloud/onedrive/onedrivetokenrefresher.cpp
+++ b/backends/cloud/onedrive/onedrivetokenrefresher.cpp
@@ -115,7 +115,6 @@ void OneDriveTokenRefresher::finishJson(Common::JSONValue *json) {
 }
 
 void OneDriveTokenRefresher::finishError(Networking::ErrorResponse error) {
-	bool irrecoverable = error.interrupted || error.failed;
 	if (error.failed) {
 		Common::JSONValue *value = Common::JSON::parse(error.response.c_str());
 


Commit: 31628d642881499f7d6833732b096c028087e14e
    https://github.com/scummvm/scummvm/commit/31628d642881499f7d6833732b096c028087e14e
Author: Alexander Tkachev (alexander at tkachov.ru)
Date: 2019-07-30T14:51:41-04:00

Commit Message:
CLOUD: Refactor BaseStorage largest methods

Not sure if that's really better, but it was really annoying to
copy-paste `delete a; delete b; return;` in every error-handling
section.

Changed paths:
    backends/cloud/basestorage.cpp


diff --git a/backends/cloud/basestorage.cpp b/backends/cloud/basestorage.cpp
index e856bbe..805cb47 100644
--- a/backends/cloud/basestorage.cpp
+++ b/backends/cloud/basestorage.cpp
@@ -48,67 +48,68 @@ void BaseStorage::getAccessToken(Common::String code) {
 }
 
 void BaseStorage::codeFlowComplete(Networking::JsonResponse response) {
+	bool success = true;
+
 	Common::JSONValue *json = (Common::JSONValue *)response.value;
 	if (json == nullptr) {
 		debug(9, "BaseStorage::codeFlowComplete: got NULL instead of JSON!");
-		CloudMan.removeStorage(this);
-		return;
+		success = false;
 	}
 
-	if (!json->isObject()) {
+	if (success && !json->isObject()) {
 		debug(9, "BaseStorage::codeFlowComplete: passed JSON is not an object!");
-		CloudMan.removeStorage(this);
-		delete json;
-		return;
+		success = false;
 	}
 
-	Common::JSONObject result = json->asObject();
-	if (!Networking::CurlJsonRequest::jsonContainsAttribute(result, "error", "BaseStorage::codeFlowComplete")) {
-		warning("BaseStorage: bad response, no 'error' attribute passed");
-		debug(9, "%s", json->stringify(true).c_str());
-		CloudMan.removeStorage(this);
-		delete json;
-		return;
+	Common::JSONObject result;
+	if (success) {
+		result = json->asObject();
+		if (!Networking::CurlJsonRequest::jsonContainsAttribute(result, "error", "BaseStorage::codeFlowComplete")) {
+			warning("BaseStorage: bad response, no 'error' attribute passed");
+			debug(9, "%s", json->stringify(true).c_str());
+			success = false;
+		}
 	}
 
-	if (result.getVal("error")->asBool()) {
+	if (success && result.getVal("error")->asBool()) {
 		Common::String errorMessage = "{error: true}, message is missing";
 		if (Networking::CurlJsonRequest::jsonContainsString(result, "message", "BaseStorage::codeFlowComplete")) {
 			errorMessage = result.getVal("message")->asString();
 		}
 		warning("BaseStorage: response says error occurred: %s", errorMessage.c_str());
-		CloudMan.removeStorage(this);
-		delete json;
-		return;
+		success = false;
 	}
 
-	if (!Networking::CurlJsonRequest::jsonContainsObject(result, "oauth", "BaseStorage::codeFlowComplete")) {
+	if (success && !Networking::CurlJsonRequest::jsonContainsObject(result, "oauth", "BaseStorage::codeFlowComplete")) {
 		warning("BaseStorage: bad response, no 'oauth' attribute passed");
 		debug(9, "%s", json->stringify(true).c_str());
-		CloudMan.removeStorage(this);
-		delete json;
-		return;
+		success = false;
 	}
 
-	Common::JSONObject oauth = result.getVal("oauth")->asObject();
+	Common::JSONObject oauth;
 	bool requiresRefreshToken = needsRefreshToken();
-	if (!Networking::CurlJsonRequest::jsonContainsString(oauth, "access_token", "BaseStorage::codeFlowComplete") ||
-		!Networking::CurlJsonRequest::jsonContainsString(oauth, "refresh_token", "BaseStorage::codeFlowComplete", !requiresRefreshToken)) {
-		warning("BaseStorage: bad response, no 'access_token' or 'refresh_token' attribute passed");
-		debug(9, "%s", json->stringify(true).c_str());
-		CloudMan.removeStorage(this);
-		delete json;
-		return;
+	if (success) {
+		oauth = result.getVal("oauth")->asObject();
+		if (!Networking::CurlJsonRequest::jsonContainsString(oauth, "access_token", "BaseStorage::codeFlowComplete") ||
+			!Networking::CurlJsonRequest::jsonContainsString(oauth, "refresh_token", "BaseStorage::codeFlowComplete", !requiresRefreshToken)) {
+			warning("BaseStorage: bad response, no 'access_token' or 'refresh_token' attribute passed");
+			debug(9, "%s", json->stringify(true).c_str());
+			success = false;
+		}
 	}
 
-	debug(9, "%s", json->stringify(true).c_str()); // TODO: remove when done testing against cloud.scummvm.org
-	_token = oauth.getVal("access_token")->asString();
-	if (requiresRefreshToken) {
-		_refreshToken = oauth.getVal("refresh_token")->asString();
+	if (success) {
+		debug(9, "%s", json->stringify(true).c_str()); // TODO: remove when done testing against cloud.scummvm.org
+		_token = oauth.getVal("access_token")->asString();
+		if (requiresRefreshToken) {
+			_refreshToken = oauth.getVal("refresh_token")->asString();
+		}
+		CloudMan.replaceStorage(this, storageIndex());
+		ConfMan.flushToDisk();
 	}
-	CloudMan.replaceStorage(this, storageIndex());
-	ConfMan.flushToDisk();
 
+	if (!success)
+		CloudMan.removeStorage(this);
 	delete json;
 }
 
@@ -135,80 +136,68 @@ void BaseStorage::refreshAccessToken(BoolCallback callback, Networking::ErrorCal
 }
 
 void BaseStorage::tokenRefreshed(BoolCallback callback, Networking::JsonResponse response) {
+	bool success = true;
+
 	Common::JSONValue *json = response.value;
 	if (json == nullptr) {
 		debug(9, "BaseStorage::tokenRefreshed: got NULL instead of JSON!");
-		if (callback)
-			(*callback)(BoolResponse(nullptr, false));
-		delete callback;
-		return;
+		success = false;
 	}
 
-	if (!json->isObject()) {
+	if (success && !json->isObject()) {
 		debug(9, "BaseStorage::tokenRefreshed: passed JSON is not an object!");
-		if (callback)
-			(*callback)(BoolResponse(nullptr, false));
-		delete json;
-		delete callback;
-		return;
+		success = false;
 	}
 
-	Common::JSONObject result = json->asObject();
-	if (!Networking::CurlJsonRequest::jsonContainsAttribute(result, "error", "BaseStorage::tokenRefreshed")) {
-		warning("BaseStorage: bad response, no 'error' attribute passed");
-		debug(9, "%s", json->stringify(true).c_str());
-		if (callback)
-			(*callback)(BoolResponse(nullptr, false));
-		delete json;
-		delete callback;
-		return;
+	Common::JSONObject result;
+	if (success) {
+		result = json->asObject();
+		if (!Networking::CurlJsonRequest::jsonContainsAttribute(result, "error", "BaseStorage::tokenRefreshed")) {
+			warning("BaseStorage: bad response, no 'error' attribute passed");
+			debug(9, "%s", json->stringify(true).c_str());
+			success = false;
+		}
 	}
 
-	if (result.getVal("error")->asBool()) {
+	if (success && result.getVal("error")->asBool()) {
 		Common::String errorMessage = "{error: true}, message is missing";
 		if (Networking::CurlJsonRequest::jsonContainsString(result, "message", "BaseStorage::tokenRefreshed")) {
 			errorMessage = result.getVal("message")->asString();
 		}
 		warning("BaseStorage: response says error occurred: %s", errorMessage.c_str());
-		if (callback)
-			(*callback)(BoolResponse(nullptr, false));
-		delete json;
-		delete callback;
-		return;
+		success = false;
 	}
 
-	if (!Networking::CurlJsonRequest::jsonContainsObject(result, "oauth", "BaseStorage::tokenRefreshed")) {
+	if (success && !Networking::CurlJsonRequest::jsonContainsObject(result, "oauth", "BaseStorage::tokenRefreshed")) {
 		warning("BaseStorage: bad response, no 'oauth' attribute passed");
 		debug(9, "%s", json->stringify(true).c_str());
-		if (callback)
-			(*callback)(BoolResponse(nullptr, false));
-		delete json;
-		delete callback;
-		return;
+		success = false;
 	}
 
-	Common::JSONObject oauth = result.getVal("oauth")->asObject();
+	Common::JSONObject oauth;
 	bool requiresRefreshToken = !canReuseRefreshToken();
-	if (!Networking::CurlJsonRequest::jsonContainsString(oauth, "access_token", "BaseStorage::tokenRefreshed") ||
-		!Networking::CurlJsonRequest::jsonContainsString(oauth, "refresh_token", "BaseStorage::tokenRefreshed", !requiresRefreshToken)) {
-		warning("BaseStorage: bad response, no 'access_token' or 'refresh_token' attribute passed");
-		debug(9, "%s", json->stringify(true).c_str());
-		if (callback)
-			(*callback)(BoolResponse(nullptr, false));
-		delete json;
-		delete callback;
-		return;
+	if (success) {
+		oauth = result.getVal("oauth")->asObject();		
+		if (!Networking::CurlJsonRequest::jsonContainsString(oauth, "access_token", "BaseStorage::tokenRefreshed") ||
+			!Networking::CurlJsonRequest::jsonContainsString(oauth, "refresh_token", "BaseStorage::tokenRefreshed", !requiresRefreshToken)) {
+			warning("BaseStorage: bad response, no 'access_token' or 'refresh_token' attribute passed");
+			debug(9, "%s", json->stringify(true).c_str());
+			success = false;
+		}
 	}
 
-	debug(9, "%s", json->stringify(true).c_str()); // TODO: remove when done testing against cloud.scummvm.org
+	if (success) {
+		debug(9, "%s", json->stringify(true).c_str()); // TODO: remove when done testing against cloud.scummvm.org
 
-	_token = oauth.getVal("access_token")->asString();
-	if (requiresRefreshToken) {
-		_refreshToken = oauth.getVal("refresh_token")->asString();
+		_token = oauth.getVal("access_token")->asString();
+		if (requiresRefreshToken) {
+			_refreshToken = oauth.getVal("refresh_token")->asString();
+		}
+		CloudMan.save(); //ask CloudManager to save our new access_token and refresh_token
 	}
-	CloudMan.save(); //ask CloudManager to save our new access_token and refresh_token
+
 	if (callback)
-		(*callback)(BoolResponse(nullptr, true));
+		(*callback)(BoolResponse(nullptr, success));
 	delete json;
 	delete callback;
 }


Commit: 99c2418d1a270c4496b21d6d6c8035b6ef73e8a1
    https://github.com/scummvm/scummvm/commit/99c2418d1a270c4496b21d6d6c8035b6ef73e8a1
Author: Alexander Tkachev (alexander at tkachov.ru)
Date: 2019-07-30T14:51:41-04:00

Commit Message:
GUI: Rewrite Cloud tab

- StorageWizardDialog is removed, along with bmps it was using;
- EditTextWidget now accepts custom font in constructor;
- ScrollContainer scrollbar now jumps to top when content height changes
so it's "overscrolled";
- IndexPageHandler now does not awaits for `code` GET-parameter, as
local webserver is no longer used to connect Storages;
- CloudManager and all corresponding Storages are updated to support
disconnecting and to notify about successful connection.

Changed paths:
  R gui/storagewizarddialog.cpp
  R gui/storagewizarddialog.h
  R gui/themes/scummmodern/box.bmp
  R gui/themes/scummmodern/dropbox.bmp
  R gui/themes/scummmodern/googledrive.bmp
  R gui/themes/scummmodern/onedrive.bmp
  R gui/themes/scummremastered/box.bmp
  R gui/themes/scummremastered/dropbox.bmp
  R gui/themes/scummremastered/googledrive.bmp
  R gui/themes/scummremastered/onedrive.bmp
    backends/cloud/basestorage.cpp
    backends/cloud/basestorage.h
    backends/cloud/box/boxstorage.cpp
    backends/cloud/box/boxstorage.h
    backends/cloud/cloudmanager.cpp
    backends/cloud/cloudmanager.h
    backends/cloud/dropbox/dropboxstorage.cpp
    backends/cloud/dropbox/dropboxstorage.h
    backends/cloud/googledrive/googledrivestorage.cpp
    backends/cloud/googledrive/googledrivestorage.h
    backends/cloud/onedrive/onedrivestorage.cpp
    backends/cloud/onedrive/onedrivestorage.h
    backends/networking/sdl_net/handlers/indexpagehandler.cpp
    backends/networking/sdl_net/handlers/indexpagehandler.h
    gui/ThemeEngine.cpp
    gui/ThemeEngine.h
    gui/module.mk
    gui/options.cpp
    gui/options.h
    gui/themes/scummclassic.zip
    gui/themes/scummclassic/classic_layout.stx
    gui/themes/scummclassic/classic_layout_lowres.stx
    gui/themes/scummmodern.zip
    gui/themes/scummmodern/scummmodern_gfx.stx
    gui/themes/scummmodern/scummmodern_layout.stx
    gui/themes/scummmodern/scummmodern_layout_lowres.stx
    gui/themes/scummremastered.zip
    gui/themes/scummremastered/remastered_gfx.stx
    gui/themes/scummremastered/remastered_layout.stx
    gui/themes/scummremastered/remastered_layout_lowres.stx
    gui/widgets/edittext.cpp
    gui/widgets/edittext.h
    gui/widgets/scrollcontainer.cpp


diff --git a/backends/cloud/basestorage.cpp b/backends/cloud/basestorage.cpp
index 805cb47..bb19812 100644
--- a/backends/cloud/basestorage.cpp
+++ b/backends/cloud/basestorage.cpp
@@ -37,18 +37,19 @@ BaseStorage::BaseStorage(Common::String token, Common::String refreshToken):
 
 BaseStorage::~BaseStorage() {}
 
-void BaseStorage::getAccessToken(Common::String code) {
-	Networking::JsonCallback callback = new Common::Callback<BaseStorage, Networking::JsonResponse>(this, &BaseStorage::codeFlowComplete);
-	Networking::ErrorCallback errorCallback = new Common::Callback<BaseStorage, Networking::ErrorResponse>(this, &BaseStorage::codeFlowFailed);
+void BaseStorage::getAccessToken(Common::String code, Networking::ErrorCallback callback) {
+	Networking::JsonCallback innerCallback = new Common::CallbackBridge<BaseStorage, Networking::ErrorResponse, Networking::JsonResponse>(this, &BaseStorage::codeFlowComplete, callback);
+	Networking::ErrorCallback errorCallback = new Common::CallbackBridge<BaseStorage, Networking::ErrorResponse, Networking::ErrorResponse>(this, &BaseStorage::codeFlowFailed, callback);
 
 	Common::String url = Common::String::format("https://cloud.scummvm.org/%s/token/%s", cloudProvider().c_str(), code.c_str());
-	Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(callback, errorCallback, url);
+	Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(innerCallback, errorCallback, url);
 
 	addRequest(request);
 }
 
-void BaseStorage::codeFlowComplete(Networking::JsonResponse response) {
+void BaseStorage::codeFlowComplete(Networking::ErrorCallback callback, Networking::JsonResponse response) {
 	bool success = true;
+	Common::String callbackMessage = "OK";
 
 	Common::JSONValue *json = (Common::JSONValue *)response.value;
 	if (json == nullptr) {
@@ -78,6 +79,7 @@ void BaseStorage::codeFlowComplete(Networking::JsonResponse response) {
 		}
 		warning("BaseStorage: response says error occurred: %s", errorMessage.c_str());
 		success = false;
+		callbackMessage = errorMessage;
 	}
 
 	if (success && !Networking::CurlJsonRequest::jsonContainsObject(result, "oauth", "BaseStorage::codeFlowComplete")) {
@@ -110,13 +112,20 @@ void BaseStorage::codeFlowComplete(Networking::JsonResponse response) {
 
 	if (!success)
 		CloudMan.removeStorage(this);
+	if (callback)
+		(*callback)(Networking::ErrorResponse(nullptr, false, !success, callbackMessage, -1));
 	delete json;
+	delete callback;
 }
 
-void BaseStorage::codeFlowFailed(Networking::ErrorResponse error) {
+void BaseStorage::codeFlowFailed(Networking::ErrorCallback callback, Networking::ErrorResponse error) {
 	debug(9, "BaseStorage: code flow failed (%s, %ld):", (error.failed ? "failed" : "interrupted"), error.httpResponseCode);
 	debug(9, "%s", error.response.c_str());
 	CloudMan.removeStorage(this);
+
+	if (callback)
+		(*callback)(error);
+	delete callback;
 }
 
 void BaseStorage::refreshAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback) {
diff --git a/backends/cloud/basestorage.h b/backends/cloud/basestorage.h
index 243e7f4..aae1a6e 100644
--- a/backends/cloud/basestorage.h
+++ b/backends/cloud/basestorage.h
@@ -37,19 +37,19 @@ protected:
 	 * Gets token from cloud.scummvm.org using given code.
 	 * Base implementation for storages with common auth procedure.
 	 */
-	virtual void getAccessToken(Common::String code);
+	virtual void getAccessToken(Common::String code, Networking::ErrorCallback callback);
 
 	/**
 	 * Handles JSON response which should contain access token requested 
 	 * with getAccessToken().
 	 */
-	virtual void codeFlowComplete(Networking::JsonResponse response);
+	virtual void codeFlowComplete(Networking::ErrorCallback callback, Networking::JsonResponse response);
 
 	/**
 	 * Handles network errors occurred while getting access token requested
 	 * with getAccessToken().
 	 */
-	virtual void codeFlowFailed(Networking::ErrorResponse error);
+	virtual void codeFlowFailed(Networking::ErrorCallback callback, Networking::ErrorResponse error);
 
 	/**
 	 * Return cloud provider name, used in cloud.scummvm.org endpoints.
diff --git a/backends/cloud/box/boxstorage.cpp b/backends/cloud/box/boxstorage.cpp
index f76fa3a..13046a0 100644
--- a/backends/cloud/box/boxstorage.cpp
+++ b/backends/cloud/box/boxstorage.cpp
@@ -45,8 +45,8 @@ namespace Box {
 BoxStorage::BoxStorage(Common::String token, Common::String refreshToken):
 	IdStorage(token, refreshToken) {}
 
-BoxStorage::BoxStorage(Common::String code) {
-	getAccessToken(code);
+BoxStorage::BoxStorage(Common::String code, Networking::ErrorCallback cb) {
+	getAccessToken(code, cb);
 }
 
 BoxStorage::~BoxStorage() {}
@@ -227,6 +227,11 @@ BoxStorage *BoxStorage::loadFromConfig(Common::String keyPrefix) {
 	return new BoxStorage(accessToken, refreshToken);
 }
 
+void BoxStorage::removeFromConfig(Common::String keyPrefix) {
+	ConfMan.removeKey(keyPrefix + "access_token", ConfMan.kCloudDomain);
+	ConfMan.removeKey(keyPrefix + "refresh_token", ConfMan.kCloudDomain);
+}
+
 Common::String BoxStorage::getRootDirectoryId() {
 	return "0";
 }
diff --git a/backends/cloud/box/boxstorage.h b/backends/cloud/box/boxstorage.h
index a8fd32c..ce77192 100644
--- a/backends/cloud/box/boxstorage.h
+++ b/backends/cloud/box/boxstorage.h
@@ -55,7 +55,7 @@ protected:
 
 public:
 	/** This constructor uses OAuth code flow to get tokens. */
-	BoxStorage(Common::String code);
+	BoxStorage(Common::String code, Networking::ErrorCallback cb);
 	virtual ~BoxStorage();
 
 	/**
@@ -104,6 +104,11 @@ public:
 	 */
 	static BoxStorage *loadFromConfig(Common::String keyPrefix);
 
+	/**
+	 * Remove all BoxStorage-related data from config.
+	 */
+	static void removeFromConfig(Common::String keyPrefix);
+
 	virtual Common::String getRootDirectoryId();
 
 	Common::String accessToken() const { return _token; }
diff --git a/backends/cloud/cloudmanager.cpp b/backends/cloud/cloudmanager.cpp
index 20c2793..432a63b 100644
--- a/backends/cloud/cloudmanager.cpp
+++ b/backends/cloud/cloudmanager.cpp
@@ -148,6 +148,12 @@ void CloudManager::replaceStorage(Storage *storage, uint32 index) {
 	}
 	_activeStorage = storage;
 	_currentStorageIndex = index;
+	if (_storages[index].username == "") {
+		// options' Cloud tab believes Storage is connected once it has non-empty username
+		_storages[index].username = _("<syncing...>");
+		_storages[index].lastSyncDate = _("<right now>");
+		_storages[index].usedBytes = 0;
+	}
 	save();
 
 	//do what should be done on first Storage connect
@@ -250,21 +256,21 @@ void CloudManager::setStorageLastSync(uint32 index, Common::String date) {
 	save();
 }
 
-void CloudManager::connectStorage(uint32 index, Common::String code) {
+void CloudManager::connectStorage(uint32 index, Common::String code, Networking::ErrorCallback cb) {
 	freeStorages();
 
 	switch (index) {
 	case kStorageDropboxId:
-		new Dropbox::DropboxStorage(code);
+		new Dropbox::DropboxStorage(code, cb);
 		break;
 	case kStorageOneDriveId:
-		new OneDrive::OneDriveStorage(code);
+		new OneDrive::OneDriveStorage(code, cb);
 		break;
 	case kStorageGoogleDriveId:
-		new GoogleDrive::GoogleDriveStorage(code);
+		new GoogleDrive::GoogleDriveStorage(code, cb);
 		break;
 	case kStorageBoxId:
-		new Box::BoxStorage(code);
+		new Box::BoxStorage(code, cb);
 		break;
 	}
 	// in these constructors Storages request token using the passed code
@@ -273,6 +279,42 @@ void CloudManager::connectStorage(uint32 index, Common::String code) {
 	// thus, no memory leak happens
 }
 
+void CloudManager::disconnectStorage(uint32 index) {
+	if (index >= kStorageTotal)
+		error("CloudManager::disconnectStorage: invalid index passed");
+
+	Common::String name = getStorageConfigName(index);
+	switch (index) {
+	case kStorageDropboxId:
+		Dropbox::DropboxStorage::removeFromConfig(kStoragePrefix + name + "_");
+		break;
+	case kStorageOneDriveId:
+		OneDrive::OneDriveStorage::removeFromConfig(kStoragePrefix + name + "_");
+		break;
+	case kStorageGoogleDriveId:
+		GoogleDrive::GoogleDriveStorage::removeFromConfig(kStoragePrefix + name + "_");
+		break;
+	case kStorageBoxId:
+		Box::BoxStorage::removeFromConfig(kStoragePrefix + name + "_");
+		break;
+	}
+
+	switchStorage(kStorageNoneId);
+
+	ConfMan.removeKey(kStoragePrefix + name + "_username", ConfMan.kCloudDomain);
+	ConfMan.removeKey(kStoragePrefix + name + "_lastSync", ConfMan.kCloudDomain);
+	ConfMan.removeKey(kStoragePrefix + name + "_usedBytes", ConfMan.kCloudDomain);
+
+	StorageConfig config;
+	config.name = _(name);
+	config.username = "";
+	config.lastSyncDate = "";
+	config.usedBytes = 0;
+
+	_storages[index] = config;
+}
+
+
 Networking::Request *CloudManager::listDirectory(Common::String path, Storage::ListDirectoryCallback callback, Networking::ErrorCallback errorCallback, bool recursive) {
 	Storage *storage = getCurrentStorage();
 	if (storage) {
diff --git a/backends/cloud/cloudmanager.h b/backends/cloud/cloudmanager.h
index eb882a6..131af9b 100644
--- a/backends/cloud/cloudmanager.h
+++ b/backends/cloud/cloudmanager.h
@@ -204,8 +204,16 @@ public:
 	 *
 	 * @param   index   Storage's index
 	 * @param   code    OAuth2 code received from user
+	 * @param	cb		callback to notify of success or error
 	 */
-	void connectStorage(uint32 index, Common::String code);
+	void connectStorage(uint32 index, Common::String code, Networking::ErrorCallback cb = nullptr);
+
+	/**
+	 * Remove Storage with a given index from config.
+	 *
+	 * @param   index   Storage's index
+	 */
+	void disconnectStorage(uint32 index);
 
 	/** Returns ListDirectoryResponse with list of files. */
 	Networking::Request *listDirectory(Common::String path, Storage::ListDirectoryCallback callback, Networking::ErrorCallback errorCallback, bool recursive = false);
diff --git a/backends/cloud/dropbox/dropboxstorage.cpp b/backends/cloud/dropbox/dropboxstorage.cpp
index c12dec9..5d8b9e0 100644
--- a/backends/cloud/dropbox/dropboxstorage.cpp
+++ b/backends/cloud/dropbox/dropboxstorage.cpp
@@ -42,8 +42,8 @@ namespace Dropbox {
 
 DropboxStorage::DropboxStorage(Common::String accessToken, bool unused): BaseStorage(accessToken, "") {}
 
-DropboxStorage::DropboxStorage(Common::String code): BaseStorage() {
-	getAccessToken(code);
+DropboxStorage::DropboxStorage(Common::String code, Networking::ErrorCallback cb): BaseStorage() {
+	getAccessToken(code, cb);
 }
 
 DropboxStorage::~DropboxStorage() {}
@@ -112,5 +112,9 @@ DropboxStorage *DropboxStorage::loadFromConfig(Common::String keyPrefix) {
 	return new DropboxStorage(accessToken, true);
 }
 
+void DropboxStorage::removeFromConfig(Common::String keyPrefix) {
+	ConfMan.removeKey(keyPrefix + "access_token", ConfMan.kCloudDomain);
+}
+
 } // End of namespace Dropbox
 } // End of namespace Cloud
diff --git a/backends/cloud/dropbox/dropboxstorage.h b/backends/cloud/dropbox/dropboxstorage.h
index bca83d2..0b76bb5 100644
--- a/backends/cloud/dropbox/dropboxstorage.h
+++ b/backends/cloud/dropbox/dropboxstorage.h
@@ -51,7 +51,7 @@ protected:
 
 public:
 	/** This constructor uses OAuth code flow to get tokens. */
-	DropboxStorage(Common::String code);
+	DropboxStorage(Common::String code, Networking::ErrorCallback cb);
 	virtual ~DropboxStorage();
 
 	/**
@@ -98,6 +98,11 @@ public:
 	 * @return pointer to the newly created DropboxStorage or 0 if some problem occured.
 	 */
 	static DropboxStorage *loadFromConfig(Common::String keyPrefix);
+
+	/**
+	 * Remove all DropboxStorage-related data from config.
+	 */
+	static void removeFromConfig(Common::String keyPrefix);
 };
 
 } // End of namespace Dropbox
diff --git a/backends/cloud/googledrive/googledrivestorage.cpp b/backends/cloud/googledrive/googledrivestorage.cpp
index 67d1577..a6e17e6 100644
--- a/backends/cloud/googledrive/googledrivestorage.cpp
+++ b/backends/cloud/googledrive/googledrivestorage.cpp
@@ -46,8 +46,8 @@ namespace GoogleDrive {
 GoogleDriveStorage::GoogleDriveStorage(Common::String token, Common::String refreshToken):
 	IdStorage(token, refreshToken) {}
 
-GoogleDriveStorage::GoogleDriveStorage(Common::String code) {
-	getAccessToken(code);
+GoogleDriveStorage::GoogleDriveStorage(Common::String code, Networking::ErrorCallback cb) {
+	getAccessToken(code, cb);
 }
 
 GoogleDriveStorage::~GoogleDriveStorage() {}
@@ -231,6 +231,11 @@ GoogleDriveStorage *GoogleDriveStorage::loadFromConfig(Common::String keyPrefix)
 	return new GoogleDriveStorage(accessToken, refreshToken);
 }
 
+void GoogleDriveStorage::removeFromConfig(Common::String keyPrefix) {
+	ConfMan.removeKey(keyPrefix + "access_token", ConfMan.kCloudDomain);
+	ConfMan.removeKey(keyPrefix + "refresh_token", ConfMan.kCloudDomain);
+}
+
 Common::String GoogleDriveStorage::getRootDirectoryId() {
 	return "root";
 }
diff --git a/backends/cloud/googledrive/googledrivestorage.h b/backends/cloud/googledrive/googledrivestorage.h
index 21e027c..db47e7c 100644
--- a/backends/cloud/googledrive/googledrivestorage.h
+++ b/backends/cloud/googledrive/googledrivestorage.h
@@ -58,7 +58,7 @@ protected:
 
 public:
 	/** This constructor uses OAuth code flow to get tokens. */
-	GoogleDriveStorage(Common::String code);
+	GoogleDriveStorage(Common::String code, Networking::ErrorCallback cb);
 	virtual ~GoogleDriveStorage();
 
 	/**
@@ -106,6 +106,11 @@ public:
 	 */
 	static GoogleDriveStorage *loadFromConfig(Common::String keyPrefix);
 
+	/**
+	 * Remove all GoogleDriveStorage-related data from config.
+	 */
+	static void removeFromConfig(Common::String keyPrefix);
+
 	virtual Common::String getRootDirectoryId();
 
 	Common::String accessToken() const { return _token; }
diff --git a/backends/cloud/onedrive/onedrivestorage.cpp b/backends/cloud/onedrive/onedrivestorage.cpp
index 48c3a10..6d05d84 100644
--- a/backends/cloud/onedrive/onedrivestorage.cpp
+++ b/backends/cloud/onedrive/onedrivestorage.cpp
@@ -45,8 +45,8 @@ namespace OneDrive {
 OneDriveStorage::OneDriveStorage(Common::String token, Common::String refreshToken):
 	BaseStorage(token, refreshToken) {}
 
-OneDriveStorage::OneDriveStorage(Common::String code) {
-	getAccessToken(code);
+OneDriveStorage::OneDriveStorage(Common::String code, Networking::ErrorCallback cb) {
+	getAccessToken(code, cb);
 }
 
 OneDriveStorage::~OneDriveStorage() {}
@@ -209,5 +209,10 @@ OneDriveStorage *OneDriveStorage::loadFromConfig(Common::String keyPrefix) {
 	return new OneDriveStorage(accessToken, refreshToken);
 }
 
+void OneDriveStorage::removeFromConfig(Common::String keyPrefix) {
+	ConfMan.removeKey(keyPrefix + "access_token", ConfMan.kCloudDomain);
+	ConfMan.removeKey(keyPrefix + "refresh_token", ConfMan.kCloudDomain);
+}
+
 } // End of namespace OneDrive
 } // End of namespace Cloud
diff --git a/backends/cloud/onedrive/onedrivestorage.h b/backends/cloud/onedrive/onedrivestorage.h
index 4b18929..cc46772 100644
--- a/backends/cloud/onedrive/onedrivestorage.h
+++ b/backends/cloud/onedrive/onedrivestorage.h
@@ -55,7 +55,7 @@ protected:
 
 public:
 	/** This constructor uses OAuth code flow to get tokens. */
-	OneDriveStorage(Common::String code);
+	OneDriveStorage(Common::String code, Networking::ErrorCallback cb);
 	virtual ~OneDriveStorage();
 
 	/**
@@ -103,6 +103,11 @@ public:
 	 */
 	static OneDriveStorage *loadFromConfig(Common::String keyPrefix);
 
+	/**
+	 * Remove all OneDriveStorage-related data from config.
+	 */
+	static void removeFromConfig(Common::String keyPrefix);
+
 	Common::String accessToken() const { return _token; }
 };
 
diff --git a/backends/networking/sdl_net/handlers/indexpagehandler.cpp b/backends/networking/sdl_net/handlers/indexpagehandler.cpp
index 17e5159..876bdde 100644
--- a/backends/networking/sdl_net/handlers/indexpagehandler.cpp
+++ b/backends/networking/sdl_net/handlers/indexpagehandler.cpp
@@ -24,7 +24,6 @@
 #include "backends/networking/sdl_net/handlerutils.h"
 #include "backends/networking/sdl_net/localwebserver.h"
 #include "common/translation.h"
-#include "gui/storagewizarddialog.h"
 
 namespace Networking {
 
@@ -34,28 +33,17 @@ IndexPageHandler::~IndexPageHandler() {}
 
 /// public
 
-Common::String IndexPageHandler::code() const { return _code; }
-
 void IndexPageHandler::handle(Client &client) {
-	Common::String queryCode = client.queryParameter("code");
-
-	if (queryCode == "") {
-		// redirect to "/filesAJAX"
-		HandlerUtils::setMessageHandler(
-			client,
-			Common::String::format(
-				"%s<br/><a href=\"files\">%s</a>",
-				_("This is a local webserver index page."),
-				_("Open Files manager")
-			),
-			"/filesAJAX"
-		);
-		return;
-	}
-
-	_code = queryCode;
-	sendCommand(GUI::kStorageCodePassedCmd, 0);
-	HandlerUtils::setMessageHandler(client, _("ScummVM got the code and already connects to your cloud storage!"));
+	// redirect to "/filesAJAX"
+	HandlerUtils::setMessageHandler(
+		client,
+		Common::String::format(
+			"%s<br/><a href=\"files\">%s</a>",
+			_("This is a local webserver index page."),
+			_("Open Files manager")
+		),
+		"/filesAJAX"
+	);
 }
 
 bool IndexPageHandler::minimalModeSupported() {
diff --git a/backends/networking/sdl_net/handlers/indexpagehandler.h b/backends/networking/sdl_net/handlers/indexpagehandler.h
index 0d8e616..b4841bc 100644
--- a/backends/networking/sdl_net/handlers/indexpagehandler.h
+++ b/backends/networking/sdl_net/handlers/indexpagehandler.h
@@ -30,12 +30,10 @@ namespace Networking {
 class LocalWebserver;
 
 class IndexPageHandler: public BaseHandler, public GUI::CommandSender {
-	Common::String _code;
 public:
 	IndexPageHandler();
 	virtual ~IndexPageHandler();
 
-	Common::String code() const;
 	virtual void handle(Client &client);
 	virtual bool minimalModeSupported();
 };
diff --git a/gui/ThemeEngine.cpp b/gui/ThemeEngine.cpp
index 8117fbe..7e42bc0 100644
--- a/gui/ThemeEngine.cpp
+++ b/gui/ThemeEngine.cpp
@@ -61,10 +61,6 @@ const char *const ThemeEngine::kImageStopSmallButton = "stopbtn_small.bmp";
 const char *const ThemeEngine::kImageEditSmallButton = "editbtn_small.bmp";
 const char *const ThemeEngine::kImageSwitchModeSmallButton = "switchbtn_small.bmp";
 const char *const ThemeEngine::kImageFastReplaySmallButton = "fastreplay_small.bmp";
-const char *const ThemeEngine::kImageDropboxLogo = "dropbox.bmp";
-const char *const ThemeEngine::kImageOneDriveLogo = "onedrive.bmp";
-const char *const ThemeEngine::kImageGoogleDriveLogo = "googledrive.bmp";
-const char *const ThemeEngine::kImageBoxLogo = "box.bmp";
 
 struct TextDrawData {
 	const Graphics::Font *_fontPtr;
diff --git a/gui/ThemeEngine.h b/gui/ThemeEngine.h
index 0fd7e9e..367f5cb 100644
--- a/gui/ThemeEngine.h
+++ b/gui/ThemeEngine.h
@@ -263,10 +263,6 @@ public:
 	static const char *const kImageEditSmallButton; ///< Edit recording metadata in recorder onscreen dialog (for 320xY)
 	static const char *const kImageSwitchModeSmallButton; ///< Switch mode button in recorder onscreen dialog (for 320xY)
 	static const char *const kImageFastReplaySmallButton; ///< Fast playback mode button in recorder onscreen dialog (for 320xY)
-	static const char *const kImageDropboxLogo;      ///< Dropbox logo used in the StorageWizardDialog
-	static const char *const kImageOneDriveLogo;      ///< OneDrive logo used in the StorageWizardDialog
-	static const char *const kImageGoogleDriveLogo;      ///< Google Drive logo used in the StorageWizardDialog
-	static const char *const kImageBoxLogo;      ///< Box logo used in the StorageWizardDialog
 
 	/**
 	 * Graphics mode enumeration.
diff --git a/gui/module.mk b/gui/module.mk
index 87f8dec..799c4e7 100644
--- a/gui/module.mk
+++ b/gui/module.mk
@@ -43,8 +43,7 @@ ifdef USE_CLOUD
 ifdef USE_LIBCURL
 MODULE_OBJS += \
 	downloaddialog.o \
-	remotebrowser.o \
-	storagewizarddialog.o
+	remotebrowser.o
 endif
 endif
 
diff --git a/gui/options.cpp b/gui/options.cpp
index 5d90b70..38bfc3b 100644
--- a/gui/options.cpp
+++ b/gui/options.cpp
@@ -51,7 +51,6 @@
 #ifdef USE_LIBCURL
 #include "backends/cloud/cloudmanager.h"
 #include "gui/downloaddialog.h"
-#include "gui/storagewizarddialog.h"
 #endif
 
 #ifdef USE_SDL_NET
@@ -105,14 +104,17 @@ enum {
 
 #ifdef USE_CLOUD
 enum {
-	kConfigureStorageCmd = 'cfst',
-	kRefreshStorageCmd = 'rfst',
+	kSyncSavesStorageCmd = 'ssst',
 	kDownloadStorageCmd = 'dlst',
 	kRunServerCmd = 'rnsv',
 	kCloudTabContainerReflowCmd = 'ctcr',
 	kServerPortClearCmd = 'spcl',
 	kChooseRootDirCmd = 'chrp',
-	kRootPathClearCmd = 'clrp'
+	kRootPathClearCmd = 'clrp',
+	kConnectStorageCmd = 'Cnnt',
+	kOpenUrlStorageCmd = 'OpUr',
+	kPasteCodeStorageCmd = 'PsCd',
+	kDisconnectStorageCmd = 'DcSt',
 };
 #endif
 
@@ -1486,11 +1488,25 @@ GlobalOptionsDialog::GlobalOptionsDialog(LauncherDialog *launcher)
 	_storageUsername = 0;
 	_storageUsedSpaceDesc = 0;
 	_storageUsedSpace = 0;
+	_storageSyncHint = 0;
 	_storageLastSyncDesc = 0;
 	_storageLastSync = 0;
-	_storageConnectButton = 0;
-	_storageRefreshButton = 0;
+	_storageSyncSavesButton = 0;
+	_storageDownloadHint = 0;
 	_storageDownloadButton = 0;
+	_storageDisconnectHint = 0;
+	_storageDisconnectButton = 0;
+
+	_connectingStorage = false;
+	_storageWizardNotConnectedHint = 0;
+	_storageWizardOpenLinkHint = 0;
+	_storageWizardLink = 0;
+	_storageWizardCodeHint = 0;
+	_storageWizardCodeBox = 0;
+	_storageWizardPasteButton = 0;
+	_storageWizardConnectButton = 0;
+	_storageWizardConnectionStatusHint = 0;
+
 	_runServerButton = 0;
 	_serverInfoLabel = 0;
 	_rootPathButton = 0;
@@ -1744,7 +1760,7 @@ void GlobalOptionsDialog::build() {
 	container->setTarget(this);
 	container->setBackgroundType(ThemeEngine::kDialogBackgroundNone);
 
-	_storagePopUpDesc = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StoragePopupDesc", _("Storage:"), _("Active cloud storage"));
+	_storagePopUpDesc = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StoragePopupDesc", _("Active storage:"), _("Active cloud storage"));
 	_storagePopUp = new PopUpWidget(container, "GlobalOptions_Cloud_Container.StoragePopup");
 #ifdef USE_LIBCURL
 	Common::StringArray list = CloudMan.listStorages();
@@ -1755,27 +1771,39 @@ void GlobalOptionsDialog::build() {
 #endif
 	_storagePopUp->setSelected(_selectedStorageIndex);
 
+	const char* context = (g_system->getOverlayWidth() > 320 ? nullptr : "lowres");
+
 	_storageUsernameDesc = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StorageUsernameDesc", _("Username:"), _("Username used by this storage"));
-	_storageUsername = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StorageUsernameLabel", "<none>");
+	_storageUsername = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StorageUsernameLabel", "<none>", "", ThemeEngine::kFontStyleNormal);
 
 	_storageUsedSpaceDesc = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StorageUsedSpaceDesc", _("Used space:"), _("Space used by ScummVM's saved games on this storage"));
-	_storageUsedSpace = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StorageUsedSpaceLabel", "0 bytes");
-
-	_storageLastSyncDesc = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StorageLastSyncDesc", _("Last sync time:"), _("When the last saved games sync for this storage occured"));
-	_storageLastSync = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StorageLastSyncLabel", "<never>");
-
-	_storageConnectButton = new ButtonWidget(container, "GlobalOptions_Cloud_Container.ConnectButton", _("Connect"), _("Open wizard dialog to connect your cloud storage account"), kConfigureStorageCmd);
-	_storageRefreshButton = new ButtonWidget(container, "GlobalOptions_Cloud_Container.RefreshButton", _("Refresh"), _("Refresh current cloud storage information (username and usage)"), kRefreshStorageCmd);
-	_storageDownloadButton = new ButtonWidget(container, "GlobalOptions_Cloud_Container.DownloadButton", _("Download"), _("Open downloads manager dialog"), kDownloadStorageCmd);
+	_storageUsedSpace = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StorageUsedSpaceLabel", "0 bytes", "", ThemeEngine::kFontStyleNormal);
+	
+	_storageLastSyncDesc = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StorageLastSyncDesc", _("Last sync:"), _("When was the last time saved games were synced with this storage"));
+	_storageLastSync = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StorageLastSyncLabel", "<never>", "", ThemeEngine::kFontStyleNormal);
+	_storageSyncHint = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StorageSyncHint", _c("Saves sync automatically on launch, after saving and on loading.", context), "", ThemeEngine::kFontStyleNormal);
+	_storageSyncSavesButton = new ButtonWidget(container, "GlobalOptions_Cloud_Container.SyncSavesButton", _("Sync now"), _("Start saves sync"), kSyncSavesStorageCmd);
+
+	_storageDownloadHint = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StorageDownloadHint", _c("You can download game files from your cloud ScummVM folder:", context));
+	_storageDownloadButton = new ButtonWidget(container, "GlobalOptions_Cloud_Container.DownloadButton", _("Download game files"), _("Open downloads manager dialog"), kDownloadStorageCmd);
+
+	_storageDisconnectHint = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StorageDisconnectHint", _c("To change account for this storage, disconnect and connect again:", context));
+	_storageDisconnectButton = new ButtonWidget(container, "GlobalOptions_Cloud_Container.DisconnectButton", _("Disconnect"), _("Stop using this storage on this device"), kDisconnectStorageCmd);
+
+	_storageWizardNotConnectedHint = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StorageWizardNotConnectedHint", _c("This storage is not connected yet! To connect,", context));
+	_storageWizardOpenLinkHint = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StorageWizardOpenLinkHint", "1. Open this link:");
+	_storageWizardLink = new ButtonWidget(container, "GlobalOptions_Cloud_Container.StorageWizardLink", "https://cloud.scummvm.org/", _("Open URL"), kOpenUrlStorageCmd);
+	_storageWizardCodeHint = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StorageWizardCodeHint", _c("2. Get the code and enter it here:", context));
+	_storageWizardCodeBox = new EditTextWidget(container, "GlobalOptions_Cloud_Container.StorageWizardCodeBox", "", 0, 0, 0, ThemeEngine::kFontStyleConsole);
+	_storageWizardPasteButton = new ButtonWidget(container, "GlobalOptions_Cloud_Container.StorageWizardPasteButton", _("Paste"), _("Paste code from clipboard"), kPasteCodeStorageCmd);
+	_storageWizardConnectButton = new ButtonWidget(container, "GlobalOptions_Cloud_Container.StorageWizardConnectButton", _("3. Connect"), _("Connect your cloud storage account"), kConnectStorageCmd);
+	_storageWizardConnectionStatusHint = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StorageWizardConnectionStatusHint", "...");
 
 	_runServerButton = new ButtonWidget(container, "GlobalOptions_Cloud_Container.RunServerButton", _("Run server"), _("Run local webserver"), kRunServerCmd);
 	_serverInfoLabel = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.ServerInfoLabel", _("Not running"));
 
 	// Root path
-	if (g_system->getOverlayWidth() > 320)
-		_rootPathButton = new ButtonWidget(container, "GlobalOptions_Cloud_Container.RootPathButton", _("/root/ Path:"), _("Specifies which directory the Files Manager can access"), kChooseRootDirCmd);
-	else
-		_rootPathButton = new ButtonWidget(container, "GlobalOptions_Cloud_Container.RootPathButton", _c("/root/ Path:", "lowres"), _("Specifies which directory the Files Manager can access"), kChooseRootDirCmd);
+	_rootPathButton = new ButtonWidget(container, "GlobalOptions_Cloud_Container.RootPathButton", _c("/root/ Path:", context), _("Specifies which directory the Files Manager can access"), kChooseRootDirCmd);
 	_rootPath = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.RootPath", "/foo/bar", _("Specifies which directory the Files Manager can access"));
 
 	_rootPathClearButton = addClearButton(container, "GlobalOptions_Cloud_Container.RootPathClearButton", kRootPathClearCmd);
@@ -1882,6 +1910,17 @@ void GlobalOptionsDialog::clean() {
 	OptionsDialog::clean();
 }
 
+void GlobalOptionsDialog::shiftWidget(Widget *widget, const char *widgetName, int32 xOffset, int32 yOffset) {
+	if (!widget) return;
+
+	int16 x, y;
+	uint16 w, h;
+	if (!g_gui.xmlEval()->getWidgetData(widgetName, x, y, w, h))
+		warning("%s's position is undefined", widgetName);
+	
+	widget->setPos(x + xOffset, y + yOffset);
+}
+
 void GlobalOptionsDialog::apply() {
 	OptionsDialog::apply();
 
@@ -2167,47 +2206,120 @@ void GlobalOptionsDialog::handleCommand(CommandSender *sender, uint32 cmd, uint3
 		reflowLayout();
 		break;
 	}
-	case kConfigureStorageCmd:
-	{
-#ifdef NETWORKING_LOCALWEBSERVER_ENABLE_PORT_OVERRIDE
-		// save server's port
-		uint32 port = Networking::LocalWebserver::getPort();
-		if (_serverPort) {
-			uint64 contents = _serverPort->getEditString().asUint64();
-			if (contents != 0)
-				port = contents;
-		}
-		ConfMan.setInt("local_server_port", port);
-		ConfMan.flushToDisk();
-#endif // NETWORKING_LOCALWEBSERVER_ENABLE_PORT_OVERRIDE
-		StorageWizardDialog dialog(_selectedStorageIndex);
+	case kSyncSavesStorageCmd: {
+		CloudMan.syncSaves(
+			new Common::Callback<GlobalOptionsDialog, Cloud::Storage::BoolResponse>(this, &GlobalOptionsDialog::storageSavesSyncedCallback)
+		);
+		break;
+	}
+	case kDownloadStorageCmd: {
+		DownloadDialog dialog(_selectedStorageIndex, _launcher);
 		dialog.runModal();
-		//update container's scrollbar
-		reflowLayout();
 		break;
 	}
-	case kRefreshStorageCmd:
-	{
-		CloudMan.info(
-			new Common::Callback<GlobalOptionsDialog, Cloud::Storage::StorageInfoResponse>(this, &GlobalOptionsDialog::storageInfoCallback),
-			new Common::Callback<GlobalOptionsDialog, Networking::ErrorResponse>(this, &GlobalOptionsDialog::storageErrorCallback)
-		);
-		Common::String dir = CloudMan.savesDirectoryPath();
-		if (dir.lastChar() == '/')
-			dir.deleteLastChar();
-		CloudMan.listDirectory(
-			dir,
-			new Common::Callback<GlobalOptionsDialog, Cloud::Storage::ListDirectoryResponse>(this, &GlobalOptionsDialog::storageListDirectoryCallback),
-			new Common::Callback<GlobalOptionsDialog, Networking::ErrorResponse>(this, &GlobalOptionsDialog::storageErrorCallback)
+	case kOpenUrlStorageCmd: {
+		Common::String url = "https://cloud.scummvm.org/";
+		switch (_selectedStorageIndex) {
+		case Cloud::kStorageDropboxId:
+			url += "dropbox";
+			break;
+		case Cloud::kStorageOneDriveId:
+			url += "onedrive";
+			break;
+		case Cloud::kStorageGoogleDriveId:
+			url += "gdrive";
+			break;
+		case Cloud::kStorageBoxId:
+			url += "box";
+			break;
+		}
+
+		if (!g_system->openUrl(url)) {
+			MessageDialog alert(_("Failed to open URL!\nPlease navigate to this page manually."));
+			alert.runModal();
+		}
+		break;
+	}
+	case kPasteCodeStorageCmd: {
+		if (g_system->hasTextInClipboard()) {
+			Common::String message = g_system->getTextFromClipboard();
+			if (!message.empty()) {
+				_storageWizardCodeBox->setEditString(message);
+				_redrawCloudTab = true;
+			}
+		}
+		break;
+	}
+	case kConnectStorageCmd: {
+		Common::String code = "";
+		if (_storageWizardCodeBox)
+			code = _storageWizardCodeBox->getEditString();
+		if (code.size() == 0)
+			return;
+
+		if (CloudMan.isWorking()) {
+			bool cancel = true;
+
+			MessageDialog alert(_("Another Storage is working now. Do you want to interrupt it?"), _("Yes"), _("No"));
+			if (alert.runModal() == GUI::kMessageOK) {
+				if (CloudMan.isDownloading())
+					CloudMan.cancelDownload();
+				if (CloudMan.isSyncing())
+					CloudMan.cancelSync();
+
+				// I believe it still would return `true` here, but just in case
+				if (CloudMan.isWorking()) {
+					MessageDialog alert2(_("Wait until current Storage finishes up and try again."));
+					alert2.runModal();
+				} else {
+					cancel = false;
+				}
+			}
+
+			if (cancel) {
+				return;
+			}
+		}
+
+		if (_storageWizardConnectionStatusHint)
+			_storageWizardConnectionStatusHint->setLabel(_("Connecting..."));
+		CloudMan.connectStorage(
+			_selectedStorageIndex, code,
+			new Common::Callback<GlobalOptionsDialog, Networking::ErrorResponse>(this, &GlobalOptionsDialog::storageConnectionCallback)
 		);
+		_connectingStorage = true;
+		_redrawCloudTab = true;
 		break;
 	}
-	case kDownloadStorageCmd:
-		{
-			DownloadDialog dialog(_selectedStorageIndex, _launcher);
-			dialog.runModal();
-			break;
+	case kDisconnectStorageCmd: {
+		if (_selectedStorageIndex == CloudMan.getStorageIndex() && CloudMan.isWorking()) {
+			bool cancel = true;
+
+			MessageDialog alert(_("This Storage is working now. Do you want to interrupt it?"), _("Yes"), _("No"));
+			if (alert.runModal() == GUI::kMessageOK) {
+				if (CloudMan.isDownloading())
+					CloudMan.cancelDownload();
+				if (CloudMan.isSyncing())
+					CloudMan.cancelSync();
+
+				// I believe it still would return `true` here, but just in case
+				if (CloudMan.isWorking()) {
+					MessageDialog alert2(_("Wait until current Storage finishes up and try again."));
+					alert2.runModal();
+				} else {
+					cancel = false;
+				}
+			}
+
+			if (cancel) {
+				return;
+			}
 		}
+
+		CloudMan.disconnectStorage(_selectedStorageIndex);
+		_redrawCloudTab = true;
+		break;
+	}
 #endif // USE_LIBCURL
 #ifdef USE_SDL_NET
 	case kRunServerCmd:
@@ -2329,24 +2441,29 @@ void GlobalOptionsDialog::setupCloudTab() {
 	if (_storagePopUpDesc) _storagePopUpDesc->setVisible(true);
 	if (_storagePopUp) _storagePopUp->setVisible(true);
 
+	Common::String username = CloudMan.getStorageUsername(_selectedStorageIndex);
+	bool storageConnected = (username != "");
 	bool shown = (_selectedStorageIndex != Cloud::kStorageNoneId);
-	if (_storageUsernameDesc) _storageUsernameDesc->setVisible(shown);
+	bool shownConnectedInfo = (shown && storageConnected);
+	
+	if (_storageUsernameDesc) _storageUsernameDesc->setVisible(shownConnectedInfo);
 	if (_storageUsername) {
-		Common::String username = CloudMan.getStorageUsername(_selectedStorageIndex);
-		if (username == "")
-			username = _("<none>");
 		_storageUsername->setLabel(username);
-		_storageUsername->setVisible(shown);
+		_storageUsername->setVisible(shownConnectedInfo);
 	}
-	if (_storageUsedSpaceDesc) _storageUsedSpaceDesc->setVisible(shown);
+	if (_storageUsedSpaceDesc) _storageUsedSpaceDesc->setVisible(shownConnectedInfo);
 	if (_storageUsedSpace) {
 		uint64 usedSpace = CloudMan.getStorageUsedSpace(_selectedStorageIndex);
 		Common::String usedSpaceNumber, usedSpaceUnits;
 		usedSpaceNumber = Common::getHumanReadableBytes(usedSpace, usedSpaceUnits);
 		_storageUsedSpace->setLabel(Common::String::format("%s %s", usedSpaceNumber.c_str(), _(usedSpaceUnits.c_str())));
-		_storageUsedSpace->setVisible(shown);
+		_storageUsedSpace->setVisible(shownConnectedInfo);
 	}
-	if (_storageLastSyncDesc) _storageLastSyncDesc->setVisible(shown);
+	if (_storageSyncHint) {
+		_storageSyncHint->setVisible(shownConnectedInfo);
+		_storageSyncHint->setEnabled(false);
+	}
+	if (_storageLastSyncDesc) _storageLastSyncDesc->setVisible(shownConnectedInfo);
 	if (_storageLastSync) {
 		Common::String sync = CloudMan.getStorageLastSync(_selectedStorageIndex);
 		if (sync == "") {
@@ -2356,16 +2473,96 @@ void GlobalOptionsDialog::setupCloudTab() {
 				sync = _("<never>");
 		}
 		_storageLastSync->setLabel(sync);
-		_storageLastSync->setVisible(shown);
+		_storageLastSync->setVisible(shownConnectedInfo);
+	}
+	if (_storageSyncSavesButton)
+		_storageSyncSavesButton->setVisible(shownConnectedInfo && _selectedStorageIndex == CloudMan.getStorageIndex());
+
+	{
+		int16 x, y;
+		uint16 w, h;
+		int16 downloadHintY, downloadButtonY, disconnectHintY;
+		if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.StorageDownloadHint", x, y, w, h))
+			warning("GlobalOptions_Cloud_Container.StorageDownloadHint's position is undefined");
+		downloadHintY = y;
+		if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.DownloadButton", x, y, w, h))
+			warning("GlobalOptions_Cloud_Container.DownloadButton's position is undefined");
+		downloadButtonY = y;
+		if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.StorageDisconnectHint", x, y, w, h))
+			warning("GlobalOptions_Cloud_Container.StorageDisconnectHint's position is undefined");
+		disconnectHintY = y;
+
+		bool showDownloadButton = (shownConnectedInfo && _selectedStorageIndex == CloudMan.getStorageIndex() && _selectedStorageIndex != Cloud::kStorageGoogleDriveId); // cannot download via Google Drive
+		if (_storageDownloadHint) _storageDownloadHint->setVisible(showDownloadButton);
+		if (_storageDownloadButton) _storageDownloadButton->setVisible(showDownloadButton);
+		if (_storageDisconnectHint) _storageDisconnectHint->setVisible(shownConnectedInfo);
+		if (_storageDisconnectButton) _storageDisconnectButton->setVisible(shownConnectedInfo);
+
+		if (showDownloadButton) {
+			if (_storageDownloadHint) _storageDownloadHint->setPos(_storageDownloadHint->getRelX(), downloadHintY);
+			if (_storageDownloadButton) _storageDownloadButton->setPos(_storageDownloadButton->getRelX(), downloadButtonY);
+			if (_storageDisconnectHint) _storageDisconnectHint->setPos(_storageDisconnectHint->getRelX(), disconnectHintY);
+			if (_storageDisconnectButton)_storageDisconnectButton->setPos(_storageDisconnectButton->getRelX(), disconnectHintY + downloadButtonY - downloadHintY);
+		} else {
+			if (_storageDisconnectHint) _storageDisconnectHint->setPos(_storageDisconnectHint->getRelX(), downloadHintY);
+			if (_storageDisconnectButton)_storageDisconnectButton->setPos(_storageDisconnectButton->getRelX(), downloadButtonY);
+		}
+
+		if (!shownConnectedInfo) {
+			bool connecting = _connectingStorage;
+			if (_storageWizardNotConnectedHint) _storageWizardNotConnectedHint->setVisible(shown);
+			if (_storageWizardOpenLinkHint) _storageWizardOpenLinkHint->setVisible(shown);
+			if (_storageWizardLink) {
+				_storageWizardLink->setVisible(shown);
+				_storageWizardLink->setEnabled(g_system->hasFeature(OSystem::kFeatureOpenUrl) && !connecting);
+			}
+			if (_storageWizardCodeHint) _storageWizardCodeHint->setVisible(shown);
+			if (_storageWizardCodeBox) {
+				_storageWizardCodeBox->setVisible(shown);
+				_storageWizardCodeBox->setEnabled(!connecting);
+			}
+			if (_storageWizardPasteButton) {
+				_storageWizardPasteButton->setVisible(shown && g_system->hasFeature(OSystem::kFeatureClipboardSupport));
+				_storageWizardPasteButton->setEnabled(!connecting);
+			}
+			if (_storageWizardConnectButton) {
+				_storageWizardConnectButton->setVisible(shown);
+				_storageWizardConnectButton->setEnabled(!connecting);
+			}
+			if (_storageWizardConnectionStatusHint) {
+				_storageWizardConnectionStatusHint->setVisible(shown && _storageWizardConnectionStatusHint->getLabel() != "...");
+				_storageWizardConnectionStatusHint->setEnabled(!connecting);
+			}
+
+			int16 x2, y2;
+			uint16 w2, h2;
+			int16 shiftUp;
+			if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.StorageUsernameDesc", x2, y2, w2, h2))
+				warning("GlobalOptions_Cloud_Container.StorageUsernameDesc's position is undefined");
+			shiftUp = y2;
+			if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.StorageWizardNotConnectedHint", x2, y2, w2, h2))
+				warning("GlobalOptions_Cloud_Container.StorageWizardNotConnectedHint's position is undefined");
+			shiftUp = y2 - shiftUp;
+
+			shiftWidget(_storageWizardNotConnectedHint, "GlobalOptions_Cloud_Container.StorageWizardNotConnectedHint", 0, -shiftUp);
+			shiftWidget(_storageWizardOpenLinkHint, "GlobalOptions_Cloud_Container.StorageWizardOpenLinkHint", 0, -shiftUp);
+			shiftWidget(_storageWizardLink, "GlobalOptions_Cloud_Container.StorageWizardLink", 0, -shiftUp);
+			shiftWidget(_storageWizardCodeHint, "GlobalOptions_Cloud_Container.StorageWizardCodeHint", 0, -shiftUp);
+			shiftWidget(_storageWizardCodeBox, "GlobalOptions_Cloud_Container.StorageWizardCodeBox", 0, -shiftUp);
+			shiftWidget(_storageWizardPasteButton, "GlobalOptions_Cloud_Container.StorageWizardPasteButton", 0, -shiftUp);
+			shiftWidget(_storageWizardConnectButton, "GlobalOptions_Cloud_Container.StorageWizardConnectButton", 0, -shiftUp);
+			shiftWidget(_storageWizardConnectionStatusHint, "GlobalOptions_Cloud_Container.StorageWizardConnectionStatusHint", 0, -shiftUp);
+		}
+
+		if (!shown)
+			serverLabelPosition = (_storageUsernameDesc ? _storageUsernameDesc->getRelY() : 0);
+		else {
+			if (shownConnectedInfo)
+				serverLabelPosition = (_storageDisconnectButton ? _storageDisconnectButton->getRelY() + _storageDisconnectButton->getHeight() + 16 : 0);
+			else
+				serverLabelPosition = (_storageWizardConnectButton ? _storageWizardConnectButton->getRelY() + _storageWizardConnectButton->getHeight() + 16 : 0);
+		}
 	}
-	if (_storageConnectButton)
-		_storageConnectButton->setVisible(shown);
-	if (_storageRefreshButton)
-		_storageRefreshButton->setVisible(shown && _selectedStorageIndex == CloudMan.getStorageIndex());
-	if (_storageDownloadButton)
-		_storageDownloadButton->setVisible(shown && _selectedStorageIndex == CloudMan.getStorageIndex());
-	if (!shown)
-		serverLabelPosition = (_storageUsernameDesc ? _storageUsernameDesc->getRelY() : 0);
 #else // USE_LIBCURL
 	_selectedStorageIndex = 0;
 
@@ -2387,10 +2584,10 @@ void GlobalOptionsDialog::setupCloudTab() {
 		_storageLastSyncDesc->setVisible(false);
 	if (_storageLastSync)
 		_storageLastSync->setVisible(false);
-	if (_storageConnectButton)
-		_storageConnectButton->setVisible(false);
-	if (_storageRefreshButton)
-		_storageRefreshButton->setVisible(false);
+	if (_storageDisconnectButton)
+		_storageDisconnectButton->setVisible(false);
+	if (_storageSyncSavesButton)
+		_storageSyncSavesButton->setVisible(false);
 	if (_storageDownloadButton)
 		_storageDownloadButton->setVisible(false);
 
@@ -2504,23 +2701,51 @@ void GlobalOptionsDialog::setupCloudTab() {
 	if (_serverPortClearButton)
 		_serverPortClearButton->setVisible(false);
 #endif // USE_SDL_NET
+
+	// temporary hide all local server-related info to see how Cloud looks without it
+	/*
+	if (_runServerButton)
+		_runServerButton->setVisible(false);
+	if (_serverInfoLabel) {
+		_serverInfoLabel->setPos(_serverInfoLabel->getRelX(), serverLabelPosition);
+		_serverInfoLabel->setVisible(false);
+	}
+	if (_rootPathButton)
+		_rootPathButton->setVisible(false);
+	if (_rootPath)
+		_rootPath->setVisible(false);
+	if (_rootPathClearButton)
+		_rootPathClearButton->setVisible(false);
+	if (_serverPortDesc)
+		_serverPortDesc->setVisible(false);
+	if (_serverPort)
+		_serverPort->setVisible(false);
+	if (_serverPortClearButton)
+		_serverPortClearButton->setVisible(false);
+	*/
 }
 
 #ifdef USE_LIBCURL
-void GlobalOptionsDialog::storageInfoCallback(Cloud::Storage::StorageInfoResponse response) {
-	//we could've used response.value.email()
-	//but Storage already notified CloudMan
-	//so we just set the flag to redraw our cloud tab
+void GlobalOptionsDialog::storageConnectionCallback(Networking::ErrorResponse response) {
+	Common::String message = "...";
+	if (!response.failed && !response.interrupted) {
+		// success
+		g_system->displayMessageOnOSD(_("Storage connected."));
+	} else {
+		message = _("Failed to connect storage.");
+		if (response.failed) {
+			message = Common::String(_("Failed to connect storage: ")) + _(response.response.c_str());
+		}
+	}
+
+	if (_storageWizardConnectionStatusHint)
+		_storageWizardConnectionStatusHint->setLabel(message);
+
 	_redrawCloudTab = true;
+	_connectingStorage = false;
 }
 
-void GlobalOptionsDialog::storageListDirectoryCallback(Cloud::Storage::ListDirectoryResponse response) {
-	Common::Array<Cloud::StorageFile> &files = response.value;
-	uint64 totalSize = 0;
-	for (uint32 i = 0; i < files.size(); ++i)
-		if (!files[i].isDirectory())
-			totalSize += files[i].size();
-	CloudMan.setStorageUsedSpace(CloudMan.getStorageIndex(), totalSize);
+void GlobalOptionsDialog::storageSavesSyncedCallback(Cloud::Storage::BoolResponse response) {
 	_redrawCloudTab = true;
 }
 
diff --git a/gui/options.h b/gui/options.h
index ad9cb2a..13983c1 100644
--- a/gui/options.h
+++ b/gui/options.h
@@ -301,11 +301,25 @@ protected:
 	StaticTextWidget *_storageUsername;
 	StaticTextWidget *_storageUsedSpaceDesc;
 	StaticTextWidget *_storageUsedSpace;
+	StaticTextWidget *_storageSyncHint;
 	StaticTextWidget *_storageLastSyncDesc;
 	StaticTextWidget *_storageLastSync;
-	ButtonWidget	 *_storageConnectButton;
-	ButtonWidget	 *_storageRefreshButton;
+	ButtonWidget	 *_storageSyncSavesButton;
+	StaticTextWidget *_storageDownloadHint;
 	ButtonWidget	 *_storageDownloadButton;
+	StaticTextWidget *_storageDisconnectHint;
+	ButtonWidget	 *_storageDisconnectButton;
+
+	bool _connectingStorage;
+	StaticTextWidget *_storageWizardNotConnectedHint;
+	StaticTextWidget *_storageWizardOpenLinkHint;
+	StaticTextWidget *_storageWizardLink;
+	StaticTextWidget *_storageWizardCodeHint;
+	EditTextWidget *_storageWizardCodeBox;
+	ButtonWidget	 *_storageWizardPasteButton;
+	ButtonWidget	 *_storageWizardConnectButton;
+	StaticTextWidget *_storageWizardConnectionStatusHint;
+
 	ButtonWidget	 *_runServerButton;
 	StaticTextWidget *_serverInfoLabel;
 	ButtonWidget	 *_rootPathButton;
@@ -319,11 +333,12 @@ protected:
 	bool _serverWasRunning;
 #endif
 
+	void shiftWidget(Widget *widget, const char *widgetName, int32 xOffset, int32 yOffset);
 	void setupCloudTab();
 
 #ifdef USE_LIBCURL
-	void storageInfoCallback(Cloud::Storage::StorageInfoResponse response);
-	void storageListDirectoryCallback(Cloud::Storage::ListDirectoryResponse response);
+	void storageConnectionCallback(Networking::ErrorResponse response);
+	void storageSavesSyncedCallback(Cloud::Storage::BoolResponse response);
 	void storageErrorCallback(Networking::ErrorResponse response);
 #endif
 #endif // USE_CLOUD
diff --git a/gui/storagewizarddialog.cpp b/gui/storagewizarddialog.cpp
deleted file mode 100644
index b01d442..0000000
--- a/gui/storagewizarddialog.cpp
+++ /dev/null
@@ -1,224 +0,0 @@
-/* ScummVM - Graphic Adventure Engine
- *
- * ScummVM is the legal property of its developers, whose names
- * are too numerous to list here. Please refer to the COPYRIGHT
- * file distributed with this source distribution.
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License
- * as published by the Free Software Foundation; either version 2
- * of the License, or (at your option) any later version.
- *
- * This program 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 General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- *
- */
-
-#include "gui/storagewizarddialog.h"
-#include "gui/gui-manager.h"
-#include "gui/message.h"
-#include "gui/widget.h"
-#include "gui/widgets/edittext.h"
-#include "gui/widgets/scrollcontainer.h"
-#include "backends/cloud/cloudmanager.h"
-#ifdef USE_SDL_NET
-#include "backends/networking/sdl_net/localwebserver.h"
-#endif
-#include "common/translation.h"
-
-namespace GUI {
-
-enum {
-	kConnectCmd = 'Cnnt',
-	kCodeBoxCmd = 'CdBx',
-	kOpenUrlCmd = 'OpUr',
-	kPasteCodeCmd = 'PsCd',
-	kStorageWizardContainerReflowCmd = 'SWCr'
-};
-
-StorageWizardDialog::StorageWizardDialog(uint32 storageId):
-	Dialog("GlobalOptions_Cloud_ConnectionWizard"), _storageId(storageId), _close(false) {
-#ifdef USE_SDL_NET
-	_stopServerOnClose = false;
-#endif
-	_backgroundType = GUI::ThemeEngine::kDialogBackgroundPlain;
-
-	ScrollContainerWidget *container = new ScrollContainerWidget(this, "GlobalOptions_Cloud_ConnectionWizard.Container", kStorageWizardContainerReflowCmd);
-	container->setTarget(this);
-
-	Common::String headline = Common::String::format(_("%s Storage Connection Wizard"), CloudMan.listStorages()[_storageId].c_str());
-	_headlineWidget = new StaticTextWidget(container, "GlobalOptions_Cloud_ConnectionWizard_Container.Headline", headline);
-
-	_navigateLineWidget = new StaticTextWidget(container, "GlobalOptions_Cloud_ConnectionWizard_Container.NavigateLine", _("Navigate to the following URL:"));
-	_urlLineWidget = new StaticTextWidget(container, "GlobalOptions_Cloud_ConnectionWizard_Container.URLLine", getUrl());
-
-	_returnLine1 = new StaticTextWidget(container, "GlobalOptions_Cloud_ConnectionWizard_Container.ReturnLine1", _("Obtain the code from the storage, enter it"));
-	_returnLine2 = new StaticTextWidget(container, "GlobalOptions_Cloud_ConnectionWizard_Container.ReturnLine2", _("in the following field and press 'Connect':"));
-	_codeWidget = new EditTextWidget(container, "GlobalOptions_Cloud_ConnectionWizard_Container.CodeBox1", "", 0, kCodeBoxCmd);
-	_messageWidget = new StaticTextWidget(container, "GlobalOptions_Cloud_ConnectionWizard_Container.MessageLine", "");
-
-	// Buttons
-	_cancelWidget = new ButtonWidget(container, "GlobalOptions_Cloud_ConnectionWizard_Container.CancelButton", _("Cancel"), 0, kCloseCmd);
-	_openUrlWidget = new ButtonWidget(container, "GlobalOptions_Cloud_ConnectionWizard_Container.OpenUrlButton", _("Open URL"), 0, kOpenUrlCmd);
-	_pasteCodeWidget = new ButtonWidget(container, "GlobalOptions_Cloud_ConnectionWizard_Container.PasteCodeButton", _("Paste"), _("Pastes clipboard contents into fields"), kPasteCodeCmd);
-	_connectWidget = new ButtonWidget(container, "GlobalOptions_Cloud_ConnectionWizard_Container.ConnectButton", _("Connect"), 0, kConnectCmd);
-
-	// Initialy the code is empty, so disable the connect button
-	_connectWidget->setEnabled(false);
-
-	_picture = new GraphicsWidget(container, "GlobalOptions_Cloud_ConnectionWizard_Container.Picture");
-#ifndef DISABLE_FANCY_THEMES
-	if (g_gui.theme()->supportsImages()) {
-		_picture->useThemeTransparency(true);
-		switch (_storageId) {
-		case Cloud::kStorageDropboxId:
-			_picture->setGfx(g_gui.theme()->getImageSurface(ThemeEngine::kImageDropboxLogo));
-			break;
-		case Cloud::kStorageOneDriveId:
-			_picture->setGfx(g_gui.theme()->getImageSurface(ThemeEngine::kImageOneDriveLogo));
-			break;
-		case Cloud::kStorageGoogleDriveId:
-			_picture->setGfx(g_gui.theme()->getImageSurface(ThemeEngine::kImageGoogleDriveLogo));
-			break;
-		case Cloud::kStorageBoxId:
-			_picture->setGfx(g_gui.theme()->getImageSurface(ThemeEngine::kImageBoxLogo));
-			break;
-		}
-	}
-#endif
-
-	containerWidgetsReflow();
-}
-
-void StorageWizardDialog::open() {
-	Dialog::open();
-
-	if (CloudMan.isWorking()) {
-		bool doClose = true;
-
-		MessageDialog alert(_("Another Storage is active. Do you want to interrupt it?"), _("Yes"), _("No"));
-		if (alert.runModal() == GUI::kMessageOK) {
-			if (CloudMan.isDownloading())
-				CloudMan.cancelDownload();
-			if (CloudMan.isSyncing())
-				CloudMan.cancelSync();
-
-			// I believe it still would return `true` here, but just in case
-			if (CloudMan.isWorking()) {
-				MessageDialog alert2(_("Wait until current Storage finishes up and try again."));
-				alert2.runModal();
-			} else {
-				doClose = false;
-			}
-		}
-
-		if (doClose) {
-			close();
-		}
-	}
-}
-
-void StorageWizardDialog::close() {
-	Dialog::close();
-}
-
-void StorageWizardDialog::handleCommand(CommandSender *sender, uint32 cmd, uint32 data) {
-	switch (cmd) {
-	case kCodeBoxCmd: {
-		Common::String code = _codeWidget->getEditString();
-		bool ok = (code.size() > 0);
-		_connectWidget->setEnabled(ok);
-		_messageWidget->setLabel("");
-		break;
-	}
-	case kOpenUrlCmd: {
-		if (!g_system->openUrl(getUrl())) {
-			MessageDialog alert(_("Failed to open URL!\nPlease navigate to this page manually."));
-			alert.runModal();
-		}
-		break;
-	}
-	case kPasteCodeCmd: {
-		if (g_system->hasTextInClipboard()) {
-			Common::String message = g_system->getTextFromClipboard();
-			if (!message.empty()) {
-				_codeWidget->setEditString(message);
-			}
-			handleCommand(sender, kCodeBoxCmd, data);
-			g_gui.scheduleTopDialogRedraw();
-		}
-		break;
-	}
-	case kConnectCmd: {
-		Common::String code = _codeWidget->getEditString();
-		if (code.size() == 0)
-			return;
-
-		CloudMan.connectStorage(_storageId, code);
-		setResult(1);
-		close();
-		break;
-	}
-#ifdef USE_SDL_NET
-	case kStorageCodePassedCmd:
-		CloudMan.connectStorage(_storageId, LocalServer.indexPageHandler().code());
-		_close = true;
-		break;
-#endif
-	case kStorageWizardContainerReflowCmd:
-		containerWidgetsReflow();
-		break;
-	default:
-		Dialog::handleCommand(sender, cmd, data);
-	}
-}
-
-void StorageWizardDialog::handleTickle() {
-	if (_close) {
-		setResult(1);
-		close();
-	}
-
-	Dialog::handleTickle();
-}
-
-void StorageWizardDialog::containerWidgetsReflow() {
-	// contents
-	if (_headlineWidget) _headlineWidget->setVisible(true);
-	if (_navigateLineWidget) _navigateLineWidget->setVisible(true);
-	if (_urlLineWidget) _urlLineWidget->setVisible(true);
-	if (_returnLine1) _returnLine1->setVisible(true);
-	if (_returnLine2) _returnLine2->setVisible(true);
-	
-	_codeWidget->setVisible(true);
-	_messageWidget->setVisible(true);
-
-	// left column / first bottom row
-	if (_picture) {
-		_picture->setVisible(g_system->getOverlayWidth() > 320);
-	}
-	if (_openUrlWidget) {
-		bool visible = g_system->hasFeature(OSystem::kFeatureOpenUrl);
-		_openUrlWidget->setVisible(visible);
-	}
-	if (_pasteCodeWidget) {
-		bool visible = g_system->hasFeature(OSystem::kFeatureClipboardSupport);
-		_pasteCodeWidget->setVisible(visible);
-	}
-
-	// bottom row
-	if (_cancelWidget) _cancelWidget->setVisible(true);
-	if (_connectWidget) _connectWidget->setVisible(true);
-}
-
-Common::String StorageWizardDialog::getUrl() const {
-	return "https://cloud.scummvm.org/";
-}
-
-} // End of namespace GUI
diff --git a/gui/storagewizarddialog.h b/gui/storagewizarddialog.h
deleted file mode 100644
index ede3750..0000000
--- a/gui/storagewizarddialog.h
+++ /dev/null
@@ -1,83 +0,0 @@
-/* ScummVM - Graphic Adventure Engine
- *
- * ScummVM is the legal property of its developers, whose names
- * are too numerous to list here. Please refer to the COPYRIGHT
- * file distributed with this source distribution.
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License
- * as published by the Free Software Foundation; either version 2
- * of the License, or (at your option) any later version.
- *
- * This program 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 General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- *
- */
-
-#ifndef GUI_STORAGEWIZARDDIALOG_H
-#define GUI_STORAGEWIZARDDIALOG_H
-
-#include "gui/dialog.h"
-#include "common/str.h"
-
-namespace GUI {
-
-class CommandSender;
-class EditTextWidget;
-class StaticTextWidget;
-class ButtonWidget;
-class GraphicsWidget;
-
-#ifdef USE_SDL_NET
-enum StorageWizardDialogCommands {
-	kStorageCodePassedCmd = 'SWDC'
-};
-#endif
-
-class StorageWizardDialog : public Dialog {
-	uint32 _storageId;
-
-	StaticTextWidget *_headlineWidget;
-	StaticTextWidget *_navigateLineWidget;
-	StaticTextWidget *_urlLineWidget;
-	StaticTextWidget *_returnLine1;
-	StaticTextWidget *_returnLine2;
-	EditTextWidget *_codeWidget;
-	StaticTextWidget *_messageWidget;
-
-	GraphicsWidget *_picture;
-	ButtonWidget *_openUrlWidget;
-	ButtonWidget *_pasteCodeWidget;
-
-	ButtonWidget *_cancelWidget;
-	ButtonWidget *_connectWidget;
-
-	bool _close;
-#ifdef USE_SDL_NET
-	bool _stopServerOnClose;
-#endif
-
-	/** Hides/shows widgets for Container to work with them correctly. */
-	void containerWidgetsReflow();
-
-	/** Return short scummvm.org URL for user to navigate to. */
-	Common::String getUrl() const;
-
-public:
-	StorageWizardDialog(uint32 storageId);
-
-	virtual void open();
-	virtual void close();
-	virtual void handleCommand(CommandSender *sender, uint32 cmd, uint32 data);
-	virtual void handleTickle();
-};
-
-} // End of namespace GUI
-
-#endif
diff --git a/gui/themes/scummclassic.zip b/gui/themes/scummclassic.zip
index 6bd0fc9..78229c3 100644
Binary files a/gui/themes/scummclassic.zip and b/gui/themes/scummclassic.zip differ
diff --git a/gui/themes/scummclassic/classic_layout.stx b/gui/themes/scummclassic/classic_layout.stx
index c92c4d1..3929af9 100644
--- a/gui/themes/scummclassic/classic_layout.stx
+++ b/gui/themes/scummclassic/classic_layout.stx
@@ -58,6 +58,12 @@
 		<widget name = 'SmallLabel'
 				size = '24, Globals.Line.Height'
 		/>
+		<widget name = 'CloudTabLabel'
+				size = '200, Globals.Line.Height'
+		/>
+		<widget name = 'CloudTabLabelValue'
+				size = '200, Globals.Line.Height'
+		/>
 
 		<widget name = 'ShortOptionsLabel'
 				size = '60, Globals.Line.Height'
@@ -66,6 +72,9 @@
 		<widget name = 'Button'
 				size = '108, 24'
 		/>
+		<widget name = 'WideButton'
+				size = '216, 24'
+		/>
 		<widget name = 'Slider'
 				size = '128, 18'
 		/>
@@ -602,49 +611,126 @@
 
 	<dialog name = 'GlobalOptions_Cloud_Container' overlays = 'GlobalOptions_Cloud.Container'>
 		<layout type = 'vertical' padding = '16, 16, 16, 16' spacing = '8'>
+			<layout type = 'horizontal' padding = '-19, 7, 0, 0' spacing = '10'>
+				<layout type = 'vertical' padding = '0, 0, 2, 0' spacing = '2'>
+					<widget name = 'StoragePopupDesc'
+							type = 'OptionsLabel'
+							height = 'Globals.Line.Height'
+					/>
+				</layout>
+				<layout type = 'vertical' padding = '0, 0, 0, 0' spacing = '2'>
+					<widget name = 'StoragePopup'
+							type = 'PopUp'
+					/>
+				</layout>
+			</layout>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'>
-				<widget name = 'StoragePopupDesc'
-						type = 'OptionsLabel'
-				/>
-				<widget name = 'StoragePopup'
-						type = 'PopUp'
-				/>
+				<layout type = 'vertical' padding = '0, 0, 6, 0' spacing = '2'>
+					<widget name = 'StorageUsernameDesc'
+							type = 'CloudTabLabel'
+					/>
+					<widget name = 'StorageUsernameLabel'
+							type = 'CloudTabLabelValue'
+					/>
+				</layout>
+				<layout type = 'vertical' padding = '0, 0, 6, 0' spacing = '2'>
+					<widget name = 'StorageUsedSpaceDesc'
+							type = 'CloudTabLabel'
+					/>
+					<widget name = 'StorageUsedSpaceLabel'
+							type = 'CloudTabLabelValue'
+					/>
+				</layout>
 			</layout>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'>
-				<widget name = 'StorageUsernameDesc'
-						type = 'OptionsLabel'
-				/>
-				<widget name = 'StorageUsernameLabel'
+				<layout type = 'vertical' padding = '0, 0, 6, 0' spacing = '2'>
+					<widget name = 'StorageLastSyncDesc'
+							type = 'CloudTabLabel'
+					/>
+					<widget name = 'StorageLastSyncLabel'
+							type = 'CloudTabLabelValue'
+					/>
+				</layout>
+				<layout type = 'vertical' padding = '0, 0, 7, 0' spacing = '2'>
+					<widget name = 'SyncSavesButton'
+							type = 'Button'
+					/>
+				</layout>
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, -4, 0' spacing = '10' center = 'true'>
+				<widget name = 'StorageSyncHint'
 						height = 'Globals.Line.Height'
 				/>
 			</layout>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'>
-				<widget name = 'StorageUsedSpaceDesc'
-						type = 'OptionsLabel'
-				/>
-				<widget name = 'StorageUsedSpaceLabel'
-						height = 'Globals.Line.Height'
-				/>
+				<layout type = 'vertical' padding = '0, 0, 6, 0' spacing = '4'>
+					<widget name = 'StorageDownloadHint'
+							height = 'Globals.Line.Height'
+					/>
+					<widget name = 'DownloadButton'
+							type = 'WideButton'
+					/>
+				</layout>
 			</layout>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'>
-				<widget name = 'StorageLastSyncDesc'
-						type = 'OptionsLabel'
+				<layout type = 'vertical' padding = '0, 0, 8, 0' spacing = '4'>
+					<widget name = 'StorageDisconnectHint'
+							height = 'Globals.Line.Height'
+					/>
+					<widget name = 'DisconnectButton'
+							type = 'Button'
+					/>
+				</layout>
+			</layout>
+
+			<!-- here goes unconnected Storage layout (connection wizard) -->
+			<layout type = 'vertical' padding = '0, 0, 6, 0' spacing = '2'>
+				<widget name = 'StorageWizardNotConnectedHint'
+						height = 'Globals.Line.Height'
 				/>
-				<widget name = 'StorageLastSyncLabel'
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, -2, 0' spacing = '10' center = 'true'>
+				<layout type = 'vertical' padding = '0, 0, 2, 0' spacing = '4'>
+					<widget name = 'StorageWizardOpenLinkHint'
+							width = '106'
+							height = 'Globals.Line.Height'
+					/>
+				</layout>
+				<layout type = 'vertical' padding = '0, 0, 1, 0' spacing = '4'>
+					<widget name = 'StorageWizardLink'
+							width = '192'
+							height = 'Globals.Line.Height'
+					/>
+				</layout>
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, -2, 0' spacing = '10' center = 'true'>
+				<widget name = 'StorageWizardCodeHint'
 						height = 'Globals.Line.Height'
 				/>
 			</layout>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'>
-				<widget name = 'ConnectButton'
-						type = 'Button'
-				/>
-				<widget name = 'RefreshButton'
+				<layout type = 'vertical' padding = '0, 0, -2, 0' spacing = '2'>
+					<widget name = 'StorageWizardCodeBox'
+						width = '108'
+						height = '24'
+					/>
+				</layout>
+				<layout type = 'vertical' padding = '0, 0, -2, 0' spacing = '2'>
+					<widget name = 'StorageWizardPasteButton'
+							type = 'Button'
+					/>
+				</layout>
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, -2, 0' spacing = '10' center = 'true'>
+				<widget name = 'StorageWizardConnectButton'
 						type = 'Button'
 				/>
-				<widget name = 'DownloadButton'
-						type = 'Button'
+				<widget name = 'StorageWizardConnectionStatusHint'
+						height = 'Globals.Line.Height'
 				/>
 			</layout>
+
+			<!-- here goes Wi-Fi Sharing -->
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'>
 				<widget name = 'RunServerButton'
 						type = 'Button'
@@ -716,99 +802,6 @@
 		</layout>
 	</dialog>
 
-	<dialog name = 'GlobalOptions_Cloud_ConnectionWizard' overlays = 'Dialog.GlobalOptions'>
-		<layout type = 'vertical' padding = '0, 0, 0, 0'>		
-			<widget name = 'Container'/>
-		</layout>
-	</dialog>
-
-	<dialog name = 'GlobalOptions_Cloud_ConnectionWizard_Container' overlays = 'GlobalOptions_Cloud_ConnectionWizard.Container'>
-		<layout type = 'vertical' padding = '16, 16, 16, 16' spacing = '0'>
-			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'>
-				<layout type = 'vertical' padding = '0, 0, 0, 0' spacing = '6'>
-					<widget name = 'Picture'
-							width = '109'
-							height = '109'
-					/>
-					<widget name = 'OpenUrlButton'
-							type = 'Button'
-					/>
-					<widget name = 'PasteCodeButton'
-							type = 'Button'
-					/>
-				</layout>
-				<layout type = 'vertical' padding = '0, 0, 0, 0' spacing = '6'>
-					<widget name = 'Headline'
-							height = 'Globals.Line.Height'
-					/>
-					<space size = '4' />
-					<widget name = 'NavigateLine'
-							height = 'Globals.Line.Height'
-					/>
-					<widget name = 'URLLine'
-							height = 'Globals.Line.Height'
-					/>
-					<space size = '4' />
-					<widget name = 'ReturnLine1'
-							height = 'Globals.Line.Height'
-					/>
-					<widget name = 'ReturnLine2'
-							height = 'Globals.Line.Height'
-					/>
-					<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '4' center = 'true'>
-						<widget name = 'CodeBox1'
-							width = '70'
-							height = 'Globals.Line.Height'
-						/>
-						<widget name = 'CodeBox2'
-							width = '70'
-							height = 'Globals.Line.Height'
-						/>
-						<widget name = 'CodeBox3'
-							width = '70'
-							height = 'Globals.Line.Height'
-						/>
-						<widget name = 'CodeBox4'
-							width = '70'
-							height = 'Globals.Line.Height'
-						/>
-					</layout>
-					<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '4' center = 'true'>
-						<widget name = 'CodeBox5'
-							width = '70'
-							height = 'Globals.Line.Height'
-						/>
-						<widget name = 'CodeBox6'
-							width = '70'
-							height = 'Globals.Line.Height'
-						/>
-						<widget name = 'CodeBox7'
-							width = '70'
-							height = 'Globals.Line.Height'
-						/>
-						<widget name = 'CodeBox8'
-							width = '70'
-							height = 'Globals.Line.Height'
-						/>
-					</layout>
-					<widget name = 'MessageLine'
-							height = 'Globals.Line.Height'
-					/>
-					<space size = '6' />
-				</layout>
-			</layout>
-			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'>
-				<widget name = 'CancelButton'
-						type = 'Button'
-				/>
-				<space />
-				<widget name = 'ConnectButton'
-						type = 'Button'
-				/>
-			</layout>
-		</layout>
-	</dialog>
-
 	<dialog name='KeysDialog' overlays='Dialog.GlobalOptions' shading='dim'>
 		<layout type='vertical' padding='8,8,8,8' center='true'>
 			<widget name='Action'
diff --git a/gui/themes/scummclassic/classic_layout_lowres.stx b/gui/themes/scummclassic/classic_layout_lowres.stx
index 67e82f9..8f39a69 100644
--- a/gui/themes/scummclassic/classic_layout_lowres.stx
+++ b/gui/themes/scummclassic/classic_layout_lowres.stx
@@ -56,6 +56,9 @@
 		<widget name = 'Button'
 				size = '72, 16'
 		/>
+		<widget name = 'WideButton'
+				size = '144, 16'
+		/>
 
 		<widget name = 'Slider'
 				size = '85, 12'
@@ -68,6 +71,12 @@
 		<widget name = 'SmallLabel'
 				size = '18, Globals.Line.Height'
 		/>
+		<widget name = 'CloudTabLabel'
+				size = '180, Globals.Line.Height'
+		/>
+		<widget name = 'CloudTabLabelValue'
+				size = '180, Globals.Line.Height'
+		/>
 		<widget name = 'PopUp'
 				size = '-1, 15'
 		/>
@@ -605,58 +614,128 @@
 	</dialog>
 
 	<dialog name = 'GlobalOptions_Cloud_Container' overlays = 'GlobalOptions_Cloud.Container'>
-		<layout type = 'vertical' padding = '16, 16, 16, 16' spacing = '8'>
+		<layout type = 'vertical' padding = '10, 13, 10, 10' spacing = '8'>
+			<layout type = 'horizontal' padding = '-10, 1, 0, 0' spacing = '6'>
+				<layout type = 'vertical' padding = '0, 0, 1, 0' spacing = '1'>
+					<widget name = 'StoragePopupDesc'
+							width = '100'
+							height = 'Globals.Line.Height'
+							textalign = 'right'
+					/>
+				</layout>
+				<layout type = 'vertical' padding = '0, 0, 0, 0' spacing = '1'>
+					<widget name = 'StoragePopup'
+							type = 'PopUp'
+					/>
+				</layout>
+			</layout>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'>
-				<widget name = 'StoragePopupDesc'
-						width = '80'
-						height = 'Globals.Line.Height'
-						textalign = 'right'
-				/>
-				<widget name = 'StoragePopup'
-						type = 'PopUp'
-				/>
+				<layout type = 'vertical' padding = '0, 0, 3, 0' spacing = '1'>
+					<widget name = 'StorageUsernameDesc'
+							type = 'CloudTabLabel'
+					/>
+					<widget name = 'StorageUsernameLabel'
+							type = 'CloudTabLabelValue'
+					/>
+				</layout>
+				<layout type = 'vertical' padding = '0, 0, 3, 0' spacing = '1'>
+					<widget name = 'StorageUsedSpaceDesc'
+							type = 'CloudTabLabel'
+					/>
+					<widget name = 'StorageUsedSpaceLabel'
+							type = 'CloudTabLabelValue'
+					/>
+				</layout>
 			</layout>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'>
-				<widget name = 'StorageUsernameDesc'
-						width = '80'
-						height = 'Globals.Line.Height'
-						textalign = 'right'
-				/>
-				<widget name = 'StorageUsernameLabel'
+				<layout type = 'vertical' padding = '0, 0, 3, 0' spacing = '1'>
+					<widget name = 'StorageLastSyncDesc'
+							type = 'CloudTabLabel'
+					/>
+					<widget name = 'StorageLastSyncLabel'
+							type = 'CloudTabLabelValue'
+					/>
+				</layout>
+				<layout type = 'vertical' padding = '0, 0, 5, 0' spacing = '1'>
+					<widget name = 'SyncSavesButton'
+							type = 'Button'
+					/>
+				</layout>
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, -3, 0' spacing = '6' center = 'true'>
+				<widget name = 'StorageSyncHint'
 						height = 'Globals.Line.Height'
 				/>
 			</layout>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'>
-				<widget name = 'StorageUsedSpaceDesc'
-						width = '80'
-						height = 'Globals.Line.Height'
-						textalign = 'right'
-				/>
-				<widget name = 'StorageUsedSpaceLabel'
-						height = 'Globals.Line.Height'
-				/>
+				<layout type = 'vertical' padding = '0, 0, 3, 0' spacing = '4'>
+					<widget name = 'StorageDownloadHint'
+							height = 'Globals.Line.Height'
+					/>
+					<widget name = 'DownloadButton'
+							type = 'WideButton'
+					/>
+				</layout>
 			</layout>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'>
-				<widget name = 'StorageLastSyncDesc'
-						width = '80'
+				<layout type = 'vertical' padding = '0, 0, 3, 0' spacing = '4'>
+					<widget name = 'StorageDisconnectHint'
+							height = 'Globals.Line.Height'
+					/>
+					<widget name = 'DisconnectButton'
+							type = 'Button'
+					/>
+				</layout>
+			</layout>
+
+			<!-- here goes unconnected Storage layout (connection wizard) -->
+			<layout type = 'vertical' padding = '0, 0, 3, 0' spacing = '1'>
+				<widget name = 'StorageWizardNotConnectedHint'
 						height = 'Globals.Line.Height'
-						textalign = 'right'
 				/>
-				<widget name = 'StorageLastSyncLabel'
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, -3, 0' spacing = '6' center = 'true'>
+				<layout type = 'vertical' padding = '0, 0, 1, 0' spacing = '2'>
+					<widget name = 'StorageWizardOpenLinkHint'
+							width = '90'
+							height = 'Globals.Line.Height'
+					/>
+				</layout>
+				<layout type = 'vertical' padding = '0, 0, 1, 0' spacing = '4'>
+					<widget name = 'StorageWizardLink'
+							width = '150'
+							height = 'Globals.Line.Height'
+					/>
+				</layout>
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, -2, 0' spacing = '6' center = 'true'>
+				<widget name = 'StorageWizardCodeHint'
 						height = 'Globals.Line.Height'
 				/>
 			</layout>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'>
-				<widget name = 'ConnectButton'
-						type = 'Button'
-				/>
-				<widget name = 'RefreshButton'
+				<layout type = 'vertical' padding = '0, 0, -2, 0' spacing = '2'>
+					<widget name = 'StorageWizardCodeBox'
+						width = '72'
+						height = '16'
+					/>
+				</layout>
+				<layout type = 'vertical' padding = '0, 0, -2, 0' spacing = '2'>
+					<widget name = 'StorageWizardPasteButton'
+							type = 'Button'
+					/>
+				</layout>
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, -2, 0' spacing = '6' center = 'true'>
+				<widget name = 'StorageWizardConnectButton'
 						type = 'Button'
 				/>
-				<widget name = 'DownloadButton'
-						type = 'Button'
+				<widget name = 'StorageWizardConnectionStatusHint'
+						height = 'Globals.Line.Height'
 				/>
 			</layout>
+
+			<!-- here goes Wi-Fi Sharing -->
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'>
 				<widget name = 'RunServerButton'
 						type = 'Button'
@@ -730,94 +809,6 @@
 		</layout>
 	</dialog>
 
-	<dialog name = 'GlobalOptions_Cloud_ConnectionWizard' overlays = 'Dialog.GlobalOptions'>
-		<layout type = 'vertical' padding = '0, 0, 0, 0'>
-			<widget name = 'Container'/>
-		</layout>
-	</dialog>
-
-	<dialog name = 'GlobalOptions_Cloud_ConnectionWizard_Container' overlays = 'GlobalOptions_Cloud_ConnectionWizard.Container'>
-		<layout type = 'vertical' padding = '16, 16, 16, 16' spacing = '8'>
-			<layout type = 'vertical' padding = '0, 0, 0, 0' spacing = '4'>
-				<widget name = 'Headline'
-						height = 'Globals.Line.Height'
-				/>
-				<space size = '2' />
-				<widget name = 'NavigateLine'
-						height = 'Globals.Line.Height'
-				/>
-				<widget name = 'URLLine'
-						height = 'Globals.Line.Height'
-				/>
-				<space size = '2' />
-				<widget name = 'ReturnLine1'
-						height = 'Globals.Line.Height'
-				/>
-				<widget name = 'ReturnLine2'
-						height = 'Globals.Line.Height'
-				/>
-				<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '4' center = 'true'>
-					<widget name = 'CodeBox1'
-						width = '60'
-						height = '16'
-					/>
-					<widget name = 'CodeBox2'
-						width = '60'
-						height = '16'
-					/>
-					<widget name = 'CodeBox3'
-						width = '60'
-						height = '16'
-					/>
-					<widget name = 'CodeBox4'
-						width = '60'
-						height = '16'
-					/>
-				</layout>
-				<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '4' center = 'true'>
-					<widget name = 'CodeBox5'
-						width = '60'
-						height = '16'
-					/>
-					<widget name = 'CodeBox6'
-						width = '60'
-						height = '16'
-					/>
-					<widget name = 'CodeBox7'
-						width = '60'
-						height = '16'
-					/>
-					<widget name = 'CodeBox8'
-						width = '60'
-						height = '16'
-					/>
-				</layout>
-				<widget name = 'MessageLine'
-						height = 'Globals.Line.Height'
-				/>
-				<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '4' center = 'true'>
-					<widget name = 'OpenUrlButton'
-							type = 'Button'
-					/>
-					<widget name = 'PasteCodeButton'
-							type = 'Button'
-					/>
-				</layout>
-				<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '4' center = 'true'>
-					<widget name = 'CancelButton'
-							type = 'Button'
-					/>
-					<space />
-					<widget name = 'ConnectButton'
-							type = 'Button'
-					/>
-				</layout>
-				<space size = '6' />
-				<widget name = 'Picture' width = '1' height = '1' />
-			</layout>
-		</layout>
-	</dialog>
-
 	<dialog name='KeysDialog' overlays='Dialog.GlobalOptions' shading='dim'>
 		<layout type='vertical' padding='8,8,8,8' center='true'>
 			<widget name='Action'
diff --git a/gui/themes/scummmodern.zip b/gui/themes/scummmodern.zip
index 1a5fd5c..d1b4c23 100644
Binary files a/gui/themes/scummmodern.zip and b/gui/themes/scummmodern.zip differ
diff --git a/gui/themes/scummmodern/box.bmp b/gui/themes/scummmodern/box.bmp
deleted file mode 100644
index 21fb650..0000000
Binary files a/gui/themes/scummmodern/box.bmp and /dev/null differ
diff --git a/gui/themes/scummmodern/dropbox.bmp b/gui/themes/scummmodern/dropbox.bmp
deleted file mode 100644
index bfe6207..0000000
Binary files a/gui/themes/scummmodern/dropbox.bmp and /dev/null differ
diff --git a/gui/themes/scummmodern/googledrive.bmp b/gui/themes/scummmodern/googledrive.bmp
deleted file mode 100644
index f79a0e7..0000000
Binary files a/gui/themes/scummmodern/googledrive.bmp and /dev/null differ
diff --git a/gui/themes/scummmodern/onedrive.bmp b/gui/themes/scummmodern/onedrive.bmp
deleted file mode 100644
index cd26d71..0000000
Binary files a/gui/themes/scummmodern/onedrive.bmp and /dev/null differ
diff --git a/gui/themes/scummmodern/scummmodern_gfx.stx b/gui/themes/scummmodern/scummmodern_gfx.stx
index 8b884ab..0e62f36 100644
--- a/gui/themes/scummmodern/scummmodern_gfx.stx
+++ b/gui/themes/scummmodern/scummmodern_gfx.stx
@@ -119,10 +119,6 @@
 		<bitmap filename = 'editbtn_small.bmp'/>
 		<bitmap filename = 'switchbtn_small.bmp'/>
 		<bitmap filename = 'fastreplay_small.bmp'/>
-		<bitmap filename = 'dropbox.bmp'/>
-		<bitmap filename = 'onedrive.bmp'/>
-		<bitmap filename = 'googledrive.bmp'/>
-		<bitmap filename = 'box.bmp'/>
 	</bitmaps>
 
 	<fonts>
diff --git a/gui/themes/scummmodern/scummmodern_layout.stx b/gui/themes/scummmodern/scummmodern_layout.stx
index d6c927c..b1d4e28 100644
--- a/gui/themes/scummmodern/scummmodern_layout.stx
+++ b/gui/themes/scummmodern/scummmodern_layout.stx
@@ -65,10 +65,19 @@
 		<widget name = 'SmallLabel'
 				size = '24, Globals.Line.Height'
 		/>
+		<widget name = 'CloudTabLabel'
+				size = '200, Globals.Line.Height'
+		/>
+		<widget name = 'CloudTabLabelValue'
+				size = '200, Globals.Line.Height'
+		/>
 
 		<widget name = 'Button'
 				size = '108, 24'
 		/>
+		<widget name = 'WideButton'
+				size = '216, 24'
+		/>
 
 		<widget name = 'Slider'
 				size = '128, 18'
@@ -616,49 +625,126 @@
 
 	<dialog name = 'GlobalOptions_Cloud_Container' overlays = 'GlobalOptions_Cloud.Container'>
 		<layout type = 'vertical' padding = '16, 16, 16, 16' spacing = '8'>
+			<layout type = 'horizontal' padding = '-27, 7, 0, 0' spacing = '10'>
+				<layout type = 'vertical' padding = '0, 0, 2, 0' spacing = '2'>
+					<widget name = 'StoragePopupDesc'
+							type = 'OptionsLabel'
+							height = 'Globals.Line.Height'
+					/>
+				</layout>
+				<layout type = 'vertical' padding = '0, 0, 0, 0' spacing = '2'>
+					<widget name = 'StoragePopup'
+							type = 'PopUp'
+					/>
+				</layout>
+			</layout>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'>
-				<widget name = 'StoragePopupDesc'
-						type = 'OptionsLabel'
-				/>
-				<widget name = 'StoragePopup'
-						type = 'PopUp'
-				/>
+				<layout type = 'vertical' padding = '0, 0, 6, 0' spacing = '2'>
+					<widget name = 'StorageUsernameDesc'
+							type = 'CloudTabLabel'
+					/>
+					<widget name = 'StorageUsernameLabel'
+							type = 'CloudTabLabelValue'
+					/>
+				</layout>
+				<layout type = 'vertical' padding = '0, 0, 6, 0' spacing = '2'>
+					<widget name = 'StorageUsedSpaceDesc'
+							type = 'CloudTabLabel'
+					/>
+					<widget name = 'StorageUsedSpaceLabel'
+							type = 'CloudTabLabelValue'
+					/>
+				</layout>
 			</layout>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'>
-				<widget name = 'StorageUsernameDesc'
-						type = 'OptionsLabel'
-				/>
-				<widget name = 'StorageUsernameLabel'
+				<layout type = 'vertical' padding = '0, 0, 6, 0' spacing = '2'>
+					<widget name = 'StorageLastSyncDesc'
+							type = 'CloudTabLabel'
+					/>
+					<widget name = 'StorageLastSyncLabel'
+							type = 'CloudTabLabelValue'
+					/>
+				</layout>
+				<layout type = 'vertical' padding = '0, 0, 7, 0' spacing = '2'>
+					<widget name = 'SyncSavesButton'
+							type = 'Button'
+					/>
+				</layout>
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, -4, 0' spacing = '10' center = 'true'>
+				<widget name = 'StorageSyncHint'
 						height = 'Globals.Line.Height'
 				/>
 			</layout>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'>
-				<widget name = 'StorageUsedSpaceDesc'
-						type = 'OptionsLabel'
-				/>
-				<widget name = 'StorageUsedSpaceLabel'
-						height = 'Globals.Line.Height'
-				/>
+				<layout type = 'vertical' padding = '0, 0, 6, 0' spacing = '4'>
+					<widget name = 'StorageDownloadHint'
+							height = 'Globals.Line.Height'
+					/>
+					<widget name = 'DownloadButton'
+							type = 'WideButton'
+					/>
+				</layout>
 			</layout>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'>
-				<widget name = 'StorageLastSyncDesc'
-						type = 'OptionsLabel'
+				<layout type = 'vertical' padding = '0, 0, 8, 0' spacing = '4'>
+					<widget name = 'StorageDisconnectHint'
+							height = 'Globals.Line.Height'
+					/>
+					<widget name = 'DisconnectButton'
+							type = 'Button'
+					/>
+				</layout>
+			</layout>
+
+			<!-- here goes unconnected Storage layout (connection wizard) -->
+			<layout type = 'vertical' padding = '0, 0, 6, 0' spacing = '2'>
+				<widget name = 'StorageWizardNotConnectedHint'
+						height = 'Globals.Line.Height'
 				/>
-				<widget name = 'StorageLastSyncLabel'
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, -4, 0' spacing = '10' center = 'true'>
+				<layout type = 'vertical' padding = '0, 0, 2, 0' spacing = '4'>
+					<widget name = 'StorageWizardOpenLinkHint'
+							width = '96'
+							height = 'Globals.Line.Height'
+					/>
+				</layout>
+				<layout type = 'vertical' padding = '0, 0, 1, 0' spacing = '4'>
+					<widget name = 'StorageWizardLink'
+							width = '192'
+							height = 'Globals.Line.Height'
+					/>
+				</layout>
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, -2, 0' spacing = '10' center = 'true'>
+				<widget name = 'StorageWizardCodeHint'
 						height = 'Globals.Line.Height'
 				/>
 			</layout>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'>
-				<widget name = 'ConnectButton'
-						type = 'Button'
-				/>
-				<widget name = 'RefreshButton'
+				<layout type = 'vertical' padding = '0, 0, -2, 0' spacing = '2'>
+					<widget name = 'StorageWizardCodeBox'
+						width = '108'
+						height = '24'
+					/>
+				</layout>
+				<layout type = 'vertical' padding = '0, 0, -2, 0' spacing = '2'>
+					<widget name = 'StorageWizardPasteButton'
+							type = 'Button'
+					/>
+				</layout>
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, -2, 0' spacing = '10' center = 'true'>
+				<widget name = 'StorageWizardConnectButton'
 						type = 'Button'
 				/>
-				<widget name = 'DownloadButton'
-						type = 'Button'
+				<widget name = 'StorageWizardConnectionStatusHint'
+						height = 'Globals.Line.Height'
 				/>
 			</layout>
+
+			<!-- here goes Wi-Fi Sharing -->
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'>
 				<widget name = 'RunServerButton'
 						type = 'Button'
@@ -730,99 +816,6 @@
 		</layout>
 	</dialog>
 
-	<dialog name = 'GlobalOptions_Cloud_ConnectionWizard' overlays = 'Dialog.GlobalOptions'>
-		<layout type = 'vertical' padding = '0, 0, 0, 0'>		
-			<widget name = 'Container'/>
-		</layout>
-	</dialog>
-
-	<dialog name = 'GlobalOptions_Cloud_ConnectionWizard_Container' overlays = 'GlobalOptions_Cloud_ConnectionWizard.Container'>
-		<layout type = 'vertical' padding = '16, 16, 16, 16' spacing = '0'>
-			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'>
-				<layout type = 'vertical' padding = '0, 0, 0, 0' spacing = '6'>
-					<widget name = 'Picture'
-							width = '109'
-							height = '109'
-					/>
-					<widget name = 'OpenUrlButton'
-							type = 'Button'
-					/>
-					<widget name = 'PasteCodeButton'
-							type = 'Button'
-					/>
-				</layout>
-				<layout type = 'vertical' padding = '0, 0, 0, 0' spacing = '6'>
-					<widget name = 'Headline'
-							height = 'Globals.Line.Height'
-					/>
-					<space size = '4' />
-					<widget name = 'NavigateLine'
-							height = 'Globals.Line.Height'
-					/>
-					<widget name = 'URLLine'
-							height = 'Globals.Line.Height'
-					/>
-					<space size = '4' />
-					<widget name = 'ReturnLine1'
-							height = 'Globals.Line.Height'
-					/>
-					<widget name = 'ReturnLine2'
-							height = 'Globals.Line.Height'
-					/>
-					<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '4' center = 'true'>
-						<widget name = 'CodeBox1'
-							width = '70'
-							height = 'Globals.Line.Height'
-						/>
-						<widget name = 'CodeBox2'
-							width = '70'
-							height = 'Globals.Line.Height'
-						/>
-						<widget name = 'CodeBox3'
-							width = '70'
-							height = 'Globals.Line.Height'
-						/>
-						<widget name = 'CodeBox4'
-							width = '70'
-							height = 'Globals.Line.Height'
-						/>
-					</layout>
-					<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '4' center = 'true'>
-						<widget name = 'CodeBox5'
-							width = '70'
-							height = 'Globals.Line.Height'
-						/>
-						<widget name = 'CodeBox6'
-							width = '70'
-							height = 'Globals.Line.Height'
-						/>
-						<widget name = 'CodeBox7'
-							width = '70'
-							height = 'Globals.Line.Height'
-						/>
-						<widget name = 'CodeBox8'
-							width = '70'
-							height = 'Globals.Line.Height'
-						/>
-					</layout>
-					<widget name = 'MessageLine'
-							height = 'Globals.Line.Height'
-					/>
-					<space size = '6' />
-				</layout>
-			</layout>
-			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'>
-				<widget name = 'CancelButton'
-						type = 'Button'
-				/>
-				<space />
-				<widget name = 'ConnectButton'
-						type = 'Button'
-				/>
-			</layout>
-		</layout>
-	</dialog>
-
 	<dialog name='KeysDialog' overlays='Dialog.GlobalOptions' shading='dim'>
 		<layout type='vertical' padding='8,8,8,8' center='true'>
 			<widget name='Action'
diff --git a/gui/themes/scummmodern/scummmodern_layout_lowres.stx b/gui/themes/scummmodern/scummmodern_layout_lowres.stx
index 464450c..fd9990d 100644
--- a/gui/themes/scummmodern/scummmodern_layout_lowres.stx
+++ b/gui/themes/scummmodern/scummmodern_layout_lowres.stx
@@ -46,6 +46,9 @@
 		<widget name = 'Button'
 				size = '72, 16'
 		/>
+		<widget name = 'WideButton'
+				size = '144, 16'
+		/>
 
 		<widget name = 'Slider'
 				size = '85, 12'
@@ -66,6 +69,12 @@
 		<widget name = 'SmallLabel'
 				size = '18, Globals.Line.Height'
 		/>
+		<widget name = 'CloudTabLabel'
+				size = '170, Globals.Line.Height'
+		/>
+		<widget name = 'CloudTabLabelValue'
+				size = '170, Globals.Line.Height'
+		/>
 		<widget name = 'PopUp'
 				size = '-1, 15'
 		/>
@@ -603,58 +612,128 @@
 	</dialog>
 
 	<dialog name = 'GlobalOptions_Cloud_Container' overlays = 'GlobalOptions_Cloud.Container'>
-		<layout type = 'vertical' padding = '16, 16, 16, 16' spacing = '8'>
+		<layout type = 'vertical' padding = '10, 13, 10, 10' spacing = '8'>
+			<layout type = 'horizontal' padding = '-7, 1, 0, 0' spacing = '6'>
+				<layout type = 'vertical' padding = '0, 0, 1, 0' spacing = '1'>
+					<widget name = 'StoragePopupDesc'
+							width = '80'
+							height = 'Globals.Line.Height'
+							textalign = 'right'
+					/>
+				</layout>
+				<layout type = 'vertical' padding = '0, 0, 0, 0' spacing = '1'>
+					<widget name = 'StoragePopup'
+							type = 'PopUp'
+					/>
+				</layout>
+			</layout>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'>
-				<widget name = 'StoragePopupDesc'
-						width = '80'
-						height = 'Globals.Line.Height'
-						textalign = 'right'
-				/>
-				<widget name = 'StoragePopup'
-						type = 'PopUp'
-				/>
+				<layout type = 'vertical' padding = '0, 0, 3, 0' spacing = '1'>
+					<widget name = 'StorageUsernameDesc'
+							type = 'CloudTabLabel'
+					/>
+					<widget name = 'StorageUsernameLabel'
+							type = 'CloudTabLabelValue'
+					/>
+				</layout>
+				<layout type = 'vertical' padding = '0, 0, 3, 0' spacing = '1'>
+					<widget name = 'StorageUsedSpaceDesc'
+							type = 'CloudTabLabel'
+					/>
+					<widget name = 'StorageUsedSpaceLabel'
+							type = 'CloudTabLabelValue'
+					/>
+				</layout>
 			</layout>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'>
-				<widget name = 'StorageUsernameDesc'
-						width = '80'
-						height = 'Globals.Line.Height'
-						textalign = 'right'
-				/>
-				<widget name = 'StorageUsernameLabel'
+				<layout type = 'vertical' padding = '0, 0, 3, 0' spacing = '1'>
+					<widget name = 'StorageLastSyncDesc'
+							type = 'CloudTabLabel'
+					/>
+					<widget name = 'StorageLastSyncLabel'
+							type = 'CloudTabLabelValue'
+					/>
+				</layout>
+				<layout type = 'vertical' padding = '0, 0, 5, 0' spacing = '1'>
+					<widget name = 'SyncSavesButton'
+							type = 'Button'
+					/>
+				</layout>
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, -3, 0' spacing = '6' center = 'true'>
+				<widget name = 'StorageSyncHint'
 						height = 'Globals.Line.Height'
 				/>
 			</layout>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'>
-				<widget name = 'StorageUsedSpaceDesc'
-						width = '80'
-						height = 'Globals.Line.Height'
-						textalign = 'right'
-				/>
-				<widget name = 'StorageUsedSpaceLabel'
-						height = 'Globals.Line.Height'
-				/>
+				<layout type = 'vertical' padding = '0, 0, 3, 0' spacing = '4'>
+					<widget name = 'StorageDownloadHint'
+							height = 'Globals.Line.Height'
+					/>
+					<widget name = 'DownloadButton'
+							type = 'WideButton'
+					/>
+				</layout>
 			</layout>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'>
-				<widget name = 'StorageLastSyncDesc'
-						width = '80'
+				<layout type = 'vertical' padding = '0, 0, 3, 0' spacing = '4'>
+					<widget name = 'StorageDisconnectHint'
+							height = 'Globals.Line.Height'
+					/>
+					<widget name = 'DisconnectButton'
+							type = 'Button'
+					/>
+				</layout>
+			</layout>
+
+			<!-- here goes unconnected Storage layout (connection wizard) -->
+			<layout type = 'vertical' padding = '0, 0, 3, 0' spacing = '1'>
+				<widget name = 'StorageWizardNotConnectedHint'
 						height = 'Globals.Line.Height'
-						textalign = 'right'
 				/>
-				<widget name = 'StorageLastSyncLabel'
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, -3, 0' spacing = '6' center = 'true'>
+				<layout type = 'vertical' padding = '0, 0, 1, 0' spacing = '2'>
+					<widget name = 'StorageWizardOpenLinkHint'
+							width = '90'
+							height = 'Globals.Line.Height'
+					/>
+				</layout>
+				<layout type = 'vertical' padding = '0, 0, 1, 0' spacing = '4'>
+					<widget name = 'StorageWizardLink'
+							width = '150'
+							height = 'Globals.Line.Height'
+					/>
+				</layout>
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, -2, 0' spacing = '6' center = 'true'>
+				<widget name = 'StorageWizardCodeHint'
 						height = 'Globals.Line.Height'
 				/>
 			</layout>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'>
-				<widget name = 'ConnectButton'
-						type = 'Button'
-				/>
-				<widget name = 'RefreshButton'
+				<layout type = 'vertical' padding = '0, 0, -2, 0' spacing = '2'>
+					<widget name = 'StorageWizardCodeBox'
+						width = '72'
+						height = '16'
+					/>
+				</layout>
+				<layout type = 'vertical' padding = '0, 0, -2, 0' spacing = '2'>
+					<widget name = 'StorageWizardPasteButton'
+							type = 'Button'
+					/>
+				</layout>
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, -2, 0' spacing = '6' center = 'true'>
+				<widget name = 'StorageWizardConnectButton'
 						type = 'Button'
 				/>
-				<widget name = 'DownloadButton'
-						type = 'Button'
+				<widget name = 'StorageWizardConnectionStatusHint'
+						height = 'Globals.Line.Height'
 				/>
 			</layout>
+
+			<!-- here goes Wi-Fi Sharing -->
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'>
 				<widget name = 'RunServerButton'
 						type = 'Button'
@@ -728,94 +807,6 @@
 		</layout>
 	</dialog>
 
-	<dialog name = 'GlobalOptions_Cloud_ConnectionWizard' overlays = 'Dialog.GlobalOptions'>
-		<layout type = 'vertical' padding = '0, 0, 0, 0'>
-			<widget name = 'Container'/>
-		</layout>
-	</dialog>
-
-	<dialog name = 'GlobalOptions_Cloud_ConnectionWizard_Container' overlays = 'GlobalOptions_Cloud_ConnectionWizard.Container'>
-		<layout type = 'vertical' padding = '16, 16, 16, 16' spacing = '8'>
-			<layout type = 'vertical' padding = '0, 0, 0, 0' spacing = '4'>
-				<widget name = 'Headline'
-						height = 'Globals.Line.Height'
-				/>
-				<space size = '2' />
-				<widget name = 'NavigateLine'
-						height = 'Globals.Line.Height'
-				/>
-				<widget name = 'URLLine'
-						height = 'Globals.Line.Height'
-				/>
-				<space size = '2' />
-				<widget name = 'ReturnLine1'
-						height = 'Globals.Line.Height'
-				/>
-				<widget name = 'ReturnLine2'
-						height = 'Globals.Line.Height'
-				/>
-				<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '4' center = 'true'>
-					<widget name = 'CodeBox1'
-						width = '60'
-						height = '16'
-					/>
-					<widget name = 'CodeBox2'
-						width = '60'
-						height = '16'
-					/>
-					<widget name = 'CodeBox3'
-						width = '60'
-						height = '16'
-					/>
-					<widget name = 'CodeBox4'
-						width = '60'
-						height = '16'
-					/>
-				</layout>
-				<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '4' center = 'true'>
-					<widget name = 'CodeBox5'
-						width = '60'
-						height = '16'
-					/>
-					<widget name = 'CodeBox6'
-						width = '60'
-						height = '16'
-					/>
-					<widget name = 'CodeBox7'
-						width = '60'
-						height = '16'
-					/>
-					<widget name = 'CodeBox8'
-						width = '60'
-						height = '16'
-					/>
-				</layout>
-				<widget name = 'MessageLine'
-						height = 'Globals.Line.Height'
-				/>
-				<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '4' center = 'true'>
-					<widget name = 'OpenUrlButton'
-							type = 'Button'
-					/>
-					<widget name = 'PasteCodeButton'
-							type = 'Button'
-					/>
-				</layout>
-				<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '4' center = 'true'>
-					<widget name = 'CancelButton'
-							type = 'Button'
-					/>
-					<space />
-					<widget name = 'ConnectButton'
-							type = 'Button'
-					/>
-				</layout>
-				<space size = '6' />
-				<widget name = 'Picture' width = '1' height = '1' />
-			</layout>
-		</layout>
-	</dialog>
-
 	<dialog name='KeysDialog' overlays='Dialog.GlobalOptions' shading='dim'>
 		<layout type='vertical' padding='8,8,8,8' center='true'>
 			<widget name='Action'
diff --git a/gui/themes/scummremastered.zip b/gui/themes/scummremastered.zip
index 6d235b4..5b5f82d 100644
Binary files a/gui/themes/scummremastered.zip and b/gui/themes/scummremastered.zip differ
diff --git a/gui/themes/scummremastered/box.bmp b/gui/themes/scummremastered/box.bmp
deleted file mode 100644
index 7da33fc..0000000
Binary files a/gui/themes/scummremastered/box.bmp and /dev/null differ
diff --git a/gui/themes/scummremastered/dropbox.bmp b/gui/themes/scummremastered/dropbox.bmp
deleted file mode 100644
index bfe6207..0000000
Binary files a/gui/themes/scummremastered/dropbox.bmp and /dev/null differ
diff --git a/gui/themes/scummremastered/googledrive.bmp b/gui/themes/scummremastered/googledrive.bmp
deleted file mode 100644
index f79a0e7..0000000
Binary files a/gui/themes/scummremastered/googledrive.bmp and /dev/null differ
diff --git a/gui/themes/scummremastered/onedrive.bmp b/gui/themes/scummremastered/onedrive.bmp
deleted file mode 100644
index 16f67cb..0000000
Binary files a/gui/themes/scummremastered/onedrive.bmp and /dev/null differ
diff --git a/gui/themes/scummremastered/remastered_gfx.stx b/gui/themes/scummremastered/remastered_gfx.stx
index f514432..76b722c 100644
--- a/gui/themes/scummremastered/remastered_gfx.stx
+++ b/gui/themes/scummremastered/remastered_gfx.stx
@@ -120,10 +120,6 @@
 		<bitmap filename = 'editbtn_small.bmp'/>
 		<bitmap filename = 'switchbtn_small.bmp'/>
 		<bitmap filename = 'fastreplay_small.bmp'/>
-		<bitmap filename = 'dropbox.bmp'/>
-		<bitmap filename = 'onedrive.bmp'/>
-		<bitmap filename = 'googledrive.bmp'/>
-		<bitmap filename = 'box.bmp'/>
 	</bitmaps>
 
 	<fonts>
diff --git a/gui/themes/scummremastered/remastered_layout.stx b/gui/themes/scummremastered/remastered_layout.stx
index d6c927c..b1d4e28 100644
--- a/gui/themes/scummremastered/remastered_layout.stx
+++ b/gui/themes/scummremastered/remastered_layout.stx
@@ -65,10 +65,19 @@
 		<widget name = 'SmallLabel'
 				size = '24, Globals.Line.Height'
 		/>
+		<widget name = 'CloudTabLabel'
+				size = '200, Globals.Line.Height'
+		/>
+		<widget name = 'CloudTabLabelValue'
+				size = '200, Globals.Line.Height'
+		/>
 
 		<widget name = 'Button'
 				size = '108, 24'
 		/>
+		<widget name = 'WideButton'
+				size = '216, 24'
+		/>
 
 		<widget name = 'Slider'
 				size = '128, 18'
@@ -616,49 +625,126 @@
 
 	<dialog name = 'GlobalOptions_Cloud_Container' overlays = 'GlobalOptions_Cloud.Container'>
 		<layout type = 'vertical' padding = '16, 16, 16, 16' spacing = '8'>
+			<layout type = 'horizontal' padding = '-27, 7, 0, 0' spacing = '10'>
+				<layout type = 'vertical' padding = '0, 0, 2, 0' spacing = '2'>
+					<widget name = 'StoragePopupDesc'
+							type = 'OptionsLabel'
+							height = 'Globals.Line.Height'
+					/>
+				</layout>
+				<layout type = 'vertical' padding = '0, 0, 0, 0' spacing = '2'>
+					<widget name = 'StoragePopup'
+							type = 'PopUp'
+					/>
+				</layout>
+			</layout>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'>
-				<widget name = 'StoragePopupDesc'
-						type = 'OptionsLabel'
-				/>
-				<widget name = 'StoragePopup'
-						type = 'PopUp'
-				/>
+				<layout type = 'vertical' padding = '0, 0, 6, 0' spacing = '2'>
+					<widget name = 'StorageUsernameDesc'
+							type = 'CloudTabLabel'
+					/>
+					<widget name = 'StorageUsernameLabel'
+							type = 'CloudTabLabelValue'
+					/>
+				</layout>
+				<layout type = 'vertical' padding = '0, 0, 6, 0' spacing = '2'>
+					<widget name = 'StorageUsedSpaceDesc'
+							type = 'CloudTabLabel'
+					/>
+					<widget name = 'StorageUsedSpaceLabel'
+							type = 'CloudTabLabelValue'
+					/>
+				</layout>
 			</layout>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'>
-				<widget name = 'StorageUsernameDesc'
-						type = 'OptionsLabel'
-				/>
-				<widget name = 'StorageUsernameLabel'
+				<layout type = 'vertical' padding = '0, 0, 6, 0' spacing = '2'>
+					<widget name = 'StorageLastSyncDesc'
+							type = 'CloudTabLabel'
+					/>
+					<widget name = 'StorageLastSyncLabel'
+							type = 'CloudTabLabelValue'
+					/>
+				</layout>
+				<layout type = 'vertical' padding = '0, 0, 7, 0' spacing = '2'>
+					<widget name = 'SyncSavesButton'
+							type = 'Button'
+					/>
+				</layout>
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, -4, 0' spacing = '10' center = 'true'>
+				<widget name = 'StorageSyncHint'
 						height = 'Globals.Line.Height'
 				/>
 			</layout>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'>
-				<widget name = 'StorageUsedSpaceDesc'
-						type = 'OptionsLabel'
-				/>
-				<widget name = 'StorageUsedSpaceLabel'
-						height = 'Globals.Line.Height'
-				/>
+				<layout type = 'vertical' padding = '0, 0, 6, 0' spacing = '4'>
+					<widget name = 'StorageDownloadHint'
+							height = 'Globals.Line.Height'
+					/>
+					<widget name = 'DownloadButton'
+							type = 'WideButton'
+					/>
+				</layout>
 			</layout>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'>
-				<widget name = 'StorageLastSyncDesc'
-						type = 'OptionsLabel'
+				<layout type = 'vertical' padding = '0, 0, 8, 0' spacing = '4'>
+					<widget name = 'StorageDisconnectHint'
+							height = 'Globals.Line.Height'
+					/>
+					<widget name = 'DisconnectButton'
+							type = 'Button'
+					/>
+				</layout>
+			</layout>
+
+			<!-- here goes unconnected Storage layout (connection wizard) -->
+			<layout type = 'vertical' padding = '0, 0, 6, 0' spacing = '2'>
+				<widget name = 'StorageWizardNotConnectedHint'
+						height = 'Globals.Line.Height'
 				/>
-				<widget name = 'StorageLastSyncLabel'
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, -4, 0' spacing = '10' center = 'true'>
+				<layout type = 'vertical' padding = '0, 0, 2, 0' spacing = '4'>
+					<widget name = 'StorageWizardOpenLinkHint'
+							width = '96'
+							height = 'Globals.Line.Height'
+					/>
+				</layout>
+				<layout type = 'vertical' padding = '0, 0, 1, 0' spacing = '4'>
+					<widget name = 'StorageWizardLink'
+							width = '192'
+							height = 'Globals.Line.Height'
+					/>
+				</layout>
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, -2, 0' spacing = '10' center = 'true'>
+				<widget name = 'StorageWizardCodeHint'
 						height = 'Globals.Line.Height'
 				/>
 			</layout>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'>
-				<widget name = 'ConnectButton'
-						type = 'Button'
-				/>
-				<widget name = 'RefreshButton'
+				<layout type = 'vertical' padding = '0, 0, -2, 0' spacing = '2'>
+					<widget name = 'StorageWizardCodeBox'
+						width = '108'
+						height = '24'
+					/>
+				</layout>
+				<layout type = 'vertical' padding = '0, 0, -2, 0' spacing = '2'>
+					<widget name = 'StorageWizardPasteButton'
+							type = 'Button'
+					/>
+				</layout>
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, -2, 0' spacing = '10' center = 'true'>
+				<widget name = 'StorageWizardConnectButton'
 						type = 'Button'
 				/>
-				<widget name = 'DownloadButton'
-						type = 'Button'
+				<widget name = 'StorageWizardConnectionStatusHint'
+						height = 'Globals.Line.Height'
 				/>
 			</layout>
+
+			<!-- here goes Wi-Fi Sharing -->
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'>
 				<widget name = 'RunServerButton'
 						type = 'Button'
@@ -730,99 +816,6 @@
 		</layout>
 	</dialog>
 
-	<dialog name = 'GlobalOptions_Cloud_ConnectionWizard' overlays = 'Dialog.GlobalOptions'>
-		<layout type = 'vertical' padding = '0, 0, 0, 0'>		
-			<widget name = 'Container'/>
-		</layout>
-	</dialog>
-
-	<dialog name = 'GlobalOptions_Cloud_ConnectionWizard_Container' overlays = 'GlobalOptions_Cloud_ConnectionWizard.Container'>
-		<layout type = 'vertical' padding = '16, 16, 16, 16' spacing = '0'>
-			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'>
-				<layout type = 'vertical' padding = '0, 0, 0, 0' spacing = '6'>
-					<widget name = 'Picture'
-							width = '109'
-							height = '109'
-					/>
-					<widget name = 'OpenUrlButton'
-							type = 'Button'
-					/>
-					<widget name = 'PasteCodeButton'
-							type = 'Button'
-					/>
-				</layout>
-				<layout type = 'vertical' padding = '0, 0, 0, 0' spacing = '6'>
-					<widget name = 'Headline'
-							height = 'Globals.Line.Height'
-					/>
-					<space size = '4' />
-					<widget name = 'NavigateLine'
-							height = 'Globals.Line.Height'
-					/>
-					<widget name = 'URLLine'
-							height = 'Globals.Line.Height'
-					/>
-					<space size = '4' />
-					<widget name = 'ReturnLine1'
-							height = 'Globals.Line.Height'
-					/>
-					<widget name = 'ReturnLine2'
-							height = 'Globals.Line.Height'
-					/>
-					<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '4' center = 'true'>
-						<widget name = 'CodeBox1'
-							width = '70'
-							height = 'Globals.Line.Height'
-						/>
-						<widget name = 'CodeBox2'
-							width = '70'
-							height = 'Globals.Line.Height'
-						/>
-						<widget name = 'CodeBox3'
-							width = '70'
-							height = 'Globals.Line.Height'
-						/>
-						<widget name = 'CodeBox4'
-							width = '70'
-							height = 'Globals.Line.Height'
-						/>
-					</layout>
-					<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '4' center = 'true'>
-						<widget name = 'CodeBox5'
-							width = '70'
-							height = 'Globals.Line.Height'
-						/>
-						<widget name = 'CodeBox6'
-							width = '70'
-							height = 'Globals.Line.Height'
-						/>
-						<widget name = 'CodeBox7'
-							width = '70'
-							height = 'Globals.Line.Height'
-						/>
-						<widget name = 'CodeBox8'
-							width = '70'
-							height = 'Globals.Line.Height'
-						/>
-					</layout>
-					<widget name = 'MessageLine'
-							height = 'Globals.Line.Height'
-					/>
-					<space size = '6' />
-				</layout>
-			</layout>
-			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'>
-				<widget name = 'CancelButton'
-						type = 'Button'
-				/>
-				<space />
-				<widget name = 'ConnectButton'
-						type = 'Button'
-				/>
-			</layout>
-		</layout>
-	</dialog>
-
 	<dialog name='KeysDialog' overlays='Dialog.GlobalOptions' shading='dim'>
 		<layout type='vertical' padding='8,8,8,8' center='true'>
 			<widget name='Action'
diff --git a/gui/themes/scummremastered/remastered_layout_lowres.stx b/gui/themes/scummremastered/remastered_layout_lowres.stx
index 464450c..fd9990d 100644
--- a/gui/themes/scummremastered/remastered_layout_lowres.stx
+++ b/gui/themes/scummremastered/remastered_layout_lowres.stx
@@ -46,6 +46,9 @@
 		<widget name = 'Button'
 				size = '72, 16'
 		/>
+		<widget name = 'WideButton'
+				size = '144, 16'
+		/>
 
 		<widget name = 'Slider'
 				size = '85, 12'
@@ -66,6 +69,12 @@
 		<widget name = 'SmallLabel'
 				size = '18, Globals.Line.Height'
 		/>
+		<widget name = 'CloudTabLabel'
+				size = '170, Globals.Line.Height'
+		/>
+		<widget name = 'CloudTabLabelValue'
+				size = '170, Globals.Line.Height'
+		/>
 		<widget name = 'PopUp'
 				size = '-1, 15'
 		/>
@@ -603,58 +612,128 @@
 	</dialog>
 
 	<dialog name = 'GlobalOptions_Cloud_Container' overlays = 'GlobalOptions_Cloud.Container'>
-		<layout type = 'vertical' padding = '16, 16, 16, 16' spacing = '8'>
+		<layout type = 'vertical' padding = '10, 13, 10, 10' spacing = '8'>
+			<layout type = 'horizontal' padding = '-7, 1, 0, 0' spacing = '6'>
+				<layout type = 'vertical' padding = '0, 0, 1, 0' spacing = '1'>
+					<widget name = 'StoragePopupDesc'
+							width = '80'
+							height = 'Globals.Line.Height'
+							textalign = 'right'
+					/>
+				</layout>
+				<layout type = 'vertical' padding = '0, 0, 0, 0' spacing = '1'>
+					<widget name = 'StoragePopup'
+							type = 'PopUp'
+					/>
+				</layout>
+			</layout>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'>
-				<widget name = 'StoragePopupDesc'
-						width = '80'
-						height = 'Globals.Line.Height'
-						textalign = 'right'
-				/>
-				<widget name = 'StoragePopup'
-						type = 'PopUp'
-				/>
+				<layout type = 'vertical' padding = '0, 0, 3, 0' spacing = '1'>
+					<widget name = 'StorageUsernameDesc'
+							type = 'CloudTabLabel'
+					/>
+					<widget name = 'StorageUsernameLabel'
+							type = 'CloudTabLabelValue'
+					/>
+				</layout>
+				<layout type = 'vertical' padding = '0, 0, 3, 0' spacing = '1'>
+					<widget name = 'StorageUsedSpaceDesc'
+							type = 'CloudTabLabel'
+					/>
+					<widget name = 'StorageUsedSpaceLabel'
+							type = 'CloudTabLabelValue'
+					/>
+				</layout>
 			</layout>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'>
-				<widget name = 'StorageUsernameDesc'
-						width = '80'
-						height = 'Globals.Line.Height'
-						textalign = 'right'
-				/>
-				<widget name = 'StorageUsernameLabel'
+				<layout type = 'vertical' padding = '0, 0, 3, 0' spacing = '1'>
+					<widget name = 'StorageLastSyncDesc'
+							type = 'CloudTabLabel'
+					/>
+					<widget name = 'StorageLastSyncLabel'
+							type = 'CloudTabLabelValue'
+					/>
+				</layout>
+				<layout type = 'vertical' padding = '0, 0, 5, 0' spacing = '1'>
+					<widget name = 'SyncSavesButton'
+							type = 'Button'
+					/>
+				</layout>
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, -3, 0' spacing = '6' center = 'true'>
+				<widget name = 'StorageSyncHint'
 						height = 'Globals.Line.Height'
 				/>
 			</layout>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'>
-				<widget name = 'StorageUsedSpaceDesc'
-						width = '80'
-						height = 'Globals.Line.Height'
-						textalign = 'right'
-				/>
-				<widget name = 'StorageUsedSpaceLabel'
-						height = 'Globals.Line.Height'
-				/>
+				<layout type = 'vertical' padding = '0, 0, 3, 0' spacing = '4'>
+					<widget name = 'StorageDownloadHint'
+							height = 'Globals.Line.Height'
+					/>
+					<widget name = 'DownloadButton'
+							type = 'WideButton'
+					/>
+				</layout>
 			</layout>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'>
-				<widget name = 'StorageLastSyncDesc'
-						width = '80'
+				<layout type = 'vertical' padding = '0, 0, 3, 0' spacing = '4'>
+					<widget name = 'StorageDisconnectHint'
+							height = 'Globals.Line.Height'
+					/>
+					<widget name = 'DisconnectButton'
+							type = 'Button'
+					/>
+				</layout>
+			</layout>
+
+			<!-- here goes unconnected Storage layout (connection wizard) -->
+			<layout type = 'vertical' padding = '0, 0, 3, 0' spacing = '1'>
+				<widget name = 'StorageWizardNotConnectedHint'
 						height = 'Globals.Line.Height'
-						textalign = 'right'
 				/>
-				<widget name = 'StorageLastSyncLabel'
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, -3, 0' spacing = '6' center = 'true'>
+				<layout type = 'vertical' padding = '0, 0, 1, 0' spacing = '2'>
+					<widget name = 'StorageWizardOpenLinkHint'
+							width = '90'
+							height = 'Globals.Line.Height'
+					/>
+				</layout>
+				<layout type = 'vertical' padding = '0, 0, 1, 0' spacing = '4'>
+					<widget name = 'StorageWizardLink'
+							width = '150'
+							height = 'Globals.Line.Height'
+					/>
+				</layout>
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, -2, 0' spacing = '6' center = 'true'>
+				<widget name = 'StorageWizardCodeHint'
 						height = 'Globals.Line.Height'
 				/>
 			</layout>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'>
-				<widget name = 'ConnectButton'
-						type = 'Button'
-				/>
-				<widget name = 'RefreshButton'
+				<layout type = 'vertical' padding = '0, 0, -2, 0' spacing = '2'>
+					<widget name = 'StorageWizardCodeBox'
+						width = '72'
+						height = '16'
+					/>
+				</layout>
+				<layout type = 'vertical' padding = '0, 0, -2, 0' spacing = '2'>
+					<widget name = 'StorageWizardPasteButton'
+							type = 'Button'
+					/>
+				</layout>
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, -2, 0' spacing = '6' center = 'true'>
+				<widget name = 'StorageWizardConnectButton'
 						type = 'Button'
 				/>
-				<widget name = 'DownloadButton'
-						type = 'Button'
+				<widget name = 'StorageWizardConnectionStatusHint'
+						height = 'Globals.Line.Height'
 				/>
 			</layout>
+
+			<!-- here goes Wi-Fi Sharing -->
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'>
 				<widget name = 'RunServerButton'
 						type = 'Button'
@@ -728,94 +807,6 @@
 		</layout>
 	</dialog>
 
-	<dialog name = 'GlobalOptions_Cloud_ConnectionWizard' overlays = 'Dialog.GlobalOptions'>
-		<layout type = 'vertical' padding = '0, 0, 0, 0'>
-			<widget name = 'Container'/>
-		</layout>
-	</dialog>
-
-	<dialog name = 'GlobalOptions_Cloud_ConnectionWizard_Container' overlays = 'GlobalOptions_Cloud_ConnectionWizard.Container'>
-		<layout type = 'vertical' padding = '16, 16, 16, 16' spacing = '8'>
-			<layout type = 'vertical' padding = '0, 0, 0, 0' spacing = '4'>
-				<widget name = 'Headline'
-						height = 'Globals.Line.Height'
-				/>
-				<space size = '2' />
-				<widget name = 'NavigateLine'
-						height = 'Globals.Line.Height'
-				/>
-				<widget name = 'URLLine'
-						height = 'Globals.Line.Height'
-				/>
-				<space size = '2' />
-				<widget name = 'ReturnLine1'
-						height = 'Globals.Line.Height'
-				/>
-				<widget name = 'ReturnLine2'
-						height = 'Globals.Line.Height'
-				/>
-				<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '4' center = 'true'>
-					<widget name = 'CodeBox1'
-						width = '60'
-						height = '16'
-					/>
-					<widget name = 'CodeBox2'
-						width = '60'
-						height = '16'
-					/>
-					<widget name = 'CodeBox3'
-						width = '60'
-						height = '16'
-					/>
-					<widget name = 'CodeBox4'
-						width = '60'
-						height = '16'
-					/>
-				</layout>
-				<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '4' center = 'true'>
-					<widget name = 'CodeBox5'
-						width = '60'
-						height = '16'
-					/>
-					<widget name = 'CodeBox6'
-						width = '60'
-						height = '16'
-					/>
-					<widget name = 'CodeBox7'
-						width = '60'
-						height = '16'
-					/>
-					<widget name = 'CodeBox8'
-						width = '60'
-						height = '16'
-					/>
-				</layout>
-				<widget name = 'MessageLine'
-						height = 'Globals.Line.Height'
-				/>
-				<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '4' center = 'true'>
-					<widget name = 'OpenUrlButton'
-							type = 'Button'
-					/>
-					<widget name = 'PasteCodeButton'
-							type = 'Button'
-					/>
-				</layout>
-				<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '4' center = 'true'>
-					<widget name = 'CancelButton'
-							type = 'Button'
-					/>
-					<space />
-					<widget name = 'ConnectButton'
-							type = 'Button'
-					/>
-				</layout>
-				<space size = '6' />
-				<widget name = 'Picture' width = '1' height = '1' />
-			</layout>
-		</layout>
-	</dialog>
-
 	<dialog name='KeysDialog' overlays='Dialog.GlobalOptions' shading='dim'>
 		<layout type='vertical' padding='8,8,8,8' center='true'>
 			<widget name='Action'
diff --git a/gui/widgets/edittext.cpp b/gui/widgets/edittext.cpp
index ba9ef61..b73cb99 100644
--- a/gui/widgets/edittext.cpp
+++ b/gui/widgets/edittext.cpp
@@ -28,26 +28,26 @@
 
 namespace GUI {
 
-EditTextWidget::EditTextWidget(GuiObject *boss, int x, int y, int w, int h, const String &text, const char *tooltip, uint32 cmd, uint32 finishCmd)
+EditTextWidget::EditTextWidget(GuiObject *boss, int x, int y, int w, int h, const String &text, const char *tooltip, uint32 cmd, uint32 finishCmd, ThemeEngine::FontStyle font)
 	: EditableWidget(boss, x, y - 1, w, h + 2, tooltip, cmd) {
 	setFlags(WIDGET_ENABLED | WIDGET_CLEARBG | WIDGET_RETAIN_FOCUS | WIDGET_WANT_TICKLE);
 	_type = kEditTextWidget;
 	_finishCmd = finishCmd;
 
 	setEditString(text);
-	setFontStyle(ThemeEngine::kFontStyleNormal);
+	setFontStyle(font);
 
 	_leftPadding = _rightPadding = 0;
 }
 
-EditTextWidget::EditTextWidget(GuiObject *boss, const String &name, const String &text, const char *tooltip, uint32 cmd, uint32 finishCmd)
+EditTextWidget::EditTextWidget(GuiObject *boss, const String &name, const String &text, const char *tooltip, uint32 cmd, uint32 finishCmd, ThemeEngine::FontStyle font)
 	: EditableWidget(boss, name, tooltip, cmd) {
 	setFlags(WIDGET_ENABLED | WIDGET_CLEARBG | WIDGET_RETAIN_FOCUS | WIDGET_WANT_TICKLE);
 	_type = kEditTextWidget;
 	_finishCmd = finishCmd;
 
 	setEditString(text);
-	setFontStyle(ThemeEngine::kFontStyleNormal);
+	setFontStyle(font);
 
 	_leftPadding = _rightPadding = 0;
 }
diff --git a/gui/widgets/edittext.h b/gui/widgets/edittext.h
index 9a1b698..a20d40e 100644
--- a/gui/widgets/edittext.h
+++ b/gui/widgets/edittext.h
@@ -40,8 +40,8 @@ protected:
 	int				_rightPadding;
 
 public:
-	EditTextWidget(GuiObject *boss, int x, int y, int w, int h, const String &text, const char *tooltip = 0, uint32 cmd = 0, uint32 finishCmd = 0);
-	EditTextWidget(GuiObject *boss, const String &name, const String &text, const char *tooltp = 0, uint32 cmd = 0, uint32 finishCmd = 0);
+	EditTextWidget(GuiObject *boss, int x, int y, int w, int h, const String &text, const char *tooltip = 0, uint32 cmd = 0, uint32 finishCmd = 0, ThemeEngine::FontStyle font = ThemeEngine::kFontStyleNormal);
+	EditTextWidget(GuiObject *boss, const String &name, const String &text, const char *tooltp = 0, uint32 cmd = 0, uint32 finishCmd = 0, ThemeEngine::FontStyle font = ThemeEngine::kFontStyleNormal);
 
 	void setEditString(const String &str);
 
diff --git a/gui/widgets/scrollcontainer.cpp b/gui/widgets/scrollcontainer.cpp
index 7e3f50d..5a0e408 100644
--- a/gui/widgets/scrollcontainer.cpp
+++ b/gui/widgets/scrollcontainer.cpp
@@ -74,6 +74,7 @@ void ScrollContainerWidget::recalc() {
 	h = max - min;
 
 	if (h <= _limitH) _scrolledY = 0;
+	if (_scrolledY > h - _limitH) _scrolledY = 0;
 
 	_verticalScroll->_numEntries = h;
 	_verticalScroll->_currentPos = _scrolledY;


Commit: 3df126853aa661f760792d478f80c2e2427b298c
    https://github.com/scummvm/scummvm/commit/3df126853aa661f760792d478f80c2e2427b298c
Author: Alexander Tkachev (alexander at tkachov.ru)
Date: 2019-07-30T14:51:41-04:00

Commit Message:
GUI: Separate Cloud tab in two

All local webserver-related settings are now shown in a separate, "Wi-Fi
Sharing" tab (shown if built with USE_SDL_NET). Cloud tab is only shown
if actual cloud storages are built (USE_LIBCURL).

Changed paths:
    gui/options.cpp
    gui/options.h
    gui/themes/scummclassic.zip
    gui/themes/scummclassic/classic_layout.stx
    gui/themes/scummclassic/classic_layout_lowres.stx
    gui/themes/scummmodern.zip
    gui/themes/scummmodern/scummmodern_layout.stx
    gui/themes/scummmodern/scummmodern_layout_lowres.stx
    gui/themes/scummremastered.zip
    gui/themes/scummremastered/remastered_layout.stx
    gui/themes/scummremastered/remastered_layout_lowres.stx


diff --git a/gui/options.cpp b/gui/options.cpp
index 38bfc3b..d69cd29 100644
--- a/gui/options.cpp
+++ b/gui/options.cpp
@@ -1479,9 +1479,6 @@ GlobalOptionsDialog::GlobalOptionsDialog(LauncherDialog *launcher)
 #ifdef USE_CLOUD
 #ifdef USE_LIBCURL
 	_selectedStorageIndex = CloudMan.getStorageIndex();
-#else
-	_selectedStorageIndex = 0;
-#endif
 	_storagePopUpDesc = 0;
 	_storagePopUp = 0;
 	_storageUsernameDesc = 0;
@@ -1506,7 +1503,9 @@ GlobalOptionsDialog::GlobalOptionsDialog(LauncherDialog *launcher)
 	_storageWizardPasteButton = 0;
 	_storageWizardConnectButton = 0;
 	_storageWizardConnectionStatusHint = 0;
-
+	_redrawCloudTab = false;
+#endif
+#ifdef USE_SDL_NET
 	_runServerButton = 0;
 	_serverInfoLabel = 0;
 	_rootPathButton = 0;
@@ -1515,8 +1514,6 @@ GlobalOptionsDialog::GlobalOptionsDialog(LauncherDialog *launcher)
 	_serverPortDesc = 0;
 	_serverPort = 0;
 	_serverPortClearButton = 0;
-	_redrawCloudTab = false;
-#ifdef USE_SDL_NET
 	_serverWasRunning = false;
 #endif
 #endif
@@ -1748,76 +1745,26 @@ void GlobalOptionsDialog::build() {
 #endif
 
 #ifdef USE_CLOUD
+	const char *context = (g_system->getOverlayWidth() > 320 ? nullptr : "lowres");
+#ifdef USE_LIBCURL
 	//
-	// 7) The cloud tab
+	// 7) The Cloud tab (remote storages)
 	//
-	if (g_system->getOverlayWidth() > 320)
-		tab->addTab(_("Cloud"));
-	else
-		tab->addTab(_c("Cloud", "lowres"));
+	tab->addTab(_c("Cloud", context));
 
 	ScrollContainerWidget *container = new ScrollContainerWidget(tab, "GlobalOptions_Cloud.Container", kCloudTabContainerReflowCmd);
 	container->setTarget(this);
 	container->setBackgroundType(ThemeEngine::kDialogBackgroundNone);
 
-	_storagePopUpDesc = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StoragePopupDesc", _("Active storage:"), _("Active cloud storage"));
-	_storagePopUp = new PopUpWidget(container, "GlobalOptions_Cloud_Container.StoragePopup");
-#ifdef USE_LIBCURL
-	Common::StringArray list = CloudMan.listStorages();
-	for (uint32 i = 0; i < list.size(); ++i)
-		_storagePopUp->appendEntry(list[i], i);
-#else
-	_storagePopUp->appendEntry(_("<none>"), 0);
-#endif
-	_storagePopUp->setSelected(_selectedStorageIndex);
-
-	const char* context = (g_system->getOverlayWidth() > 320 ? nullptr : "lowres");
-
-	_storageUsernameDesc = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StorageUsernameDesc", _("Username:"), _("Username used by this storage"));
-	_storageUsername = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StorageUsernameLabel", "<none>", "", ThemeEngine::kFontStyleNormal);
-
-	_storageUsedSpaceDesc = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StorageUsedSpaceDesc", _("Used space:"), _("Space used by ScummVM's saved games on this storage"));
-	_storageUsedSpace = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StorageUsedSpaceLabel", "0 bytes", "", ThemeEngine::kFontStyleNormal);
-	
-	_storageLastSyncDesc = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StorageLastSyncDesc", _("Last sync:"), _("When was the last time saved games were synced with this storage"));
-	_storageLastSync = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StorageLastSyncLabel", "<never>", "", ThemeEngine::kFontStyleNormal);
-	_storageSyncHint = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StorageSyncHint", _c("Saves sync automatically on launch, after saving and on loading.", context), "", ThemeEngine::kFontStyleNormal);
-	_storageSyncSavesButton = new ButtonWidget(container, "GlobalOptions_Cloud_Container.SyncSavesButton", _("Sync now"), _("Start saves sync"), kSyncSavesStorageCmd);
-
-	_storageDownloadHint = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StorageDownloadHint", _c("You can download game files from your cloud ScummVM folder:", context));
-	_storageDownloadButton = new ButtonWidget(container, "GlobalOptions_Cloud_Container.DownloadButton", _("Download game files"), _("Open downloads manager dialog"), kDownloadStorageCmd);
-
-	_storageDisconnectHint = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StorageDisconnectHint", _c("To change account for this storage, disconnect and connect again:", context));
-	_storageDisconnectButton = new ButtonWidget(container, "GlobalOptions_Cloud_Container.DisconnectButton", _("Disconnect"), _("Stop using this storage on this device"), kDisconnectStorageCmd);
-
-	_storageWizardNotConnectedHint = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StorageWizardNotConnectedHint", _c("This storage is not connected yet! To connect,", context));
-	_storageWizardOpenLinkHint = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StorageWizardOpenLinkHint", "1. Open this link:");
-	_storageWizardLink = new ButtonWidget(container, "GlobalOptions_Cloud_Container.StorageWizardLink", "https://cloud.scummvm.org/", _("Open URL"), kOpenUrlStorageCmd);
-	_storageWizardCodeHint = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StorageWizardCodeHint", _c("2. Get the code and enter it here:", context));
-	_storageWizardCodeBox = new EditTextWidget(container, "GlobalOptions_Cloud_Container.StorageWizardCodeBox", "", 0, 0, 0, ThemeEngine::kFontStyleConsole);
-	_storageWizardPasteButton = new ButtonWidget(container, "GlobalOptions_Cloud_Container.StorageWizardPasteButton", _("Paste"), _("Paste code from clipboard"), kPasteCodeStorageCmd);
-	_storageWizardConnectButton = new ButtonWidget(container, "GlobalOptions_Cloud_Container.StorageWizardConnectButton", _("3. Connect"), _("Connect your cloud storage account"), kConnectStorageCmd);
-	_storageWizardConnectionStatusHint = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StorageWizardConnectionStatusHint", "...");
-
-	_runServerButton = new ButtonWidget(container, "GlobalOptions_Cloud_Container.RunServerButton", _("Run server"), _("Run local webserver"), kRunServerCmd);
-	_serverInfoLabel = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.ServerInfoLabel", _("Not running"));
-
-	// Root path
-	_rootPathButton = new ButtonWidget(container, "GlobalOptions_Cloud_Container.RootPathButton", _c("/root/ Path:", context), _("Specifies which directory the Files Manager can access"), kChooseRootDirCmd);
-	_rootPath = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.RootPath", "/foo/bar", _("Specifies which directory the Files Manager can access"));
-
-	_rootPathClearButton = addClearButton(container, "GlobalOptions_Cloud_Container.RootPathClearButton", kRootPathClearCmd);
-
+	addCloudControls(container, "GlobalOptions_Cloud_Container.", context);
+#endif USE_LIBCURL
 #ifdef USE_SDL_NET
-	uint32 port = Networking::LocalWebserver::getPort();
-#else
-	uint32 port = 0; // the following widgets are hidden anyway
-#endif
-	_serverPortDesc = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.ServerPortDesc", _("Server's port:"), _("Which port is used by the server\nAuth with server is not available with non-default port"));
-	_serverPort = new EditTextWidget(container, "GlobalOptions_Cloud_Container.ServerPortEditText", Common::String::format("%u", port), 0);
-	_serverPortClearButton = addClearButton(container, "GlobalOptions_Cloud_Container.ServerPortClearButton", kServerPortClearCmd);
-
-	setupCloudTab();
+	//
+	// 8) The Wi-Fi Sharing tab (local "cloud" webserver)
+	//
+	tab->addTab(_c("Wi-Fi Sharing", context));
+	addWiFiSharingControls(tab, "GlobalOptions_WiFiSharing.", context);
+#endif // USE_SDL_NET
 #endif // USE_CLOUD
 
 	// Activate the first tab
@@ -1887,6 +1834,7 @@ void GlobalOptionsDialog::build() {
 	_rendererPopUp->setSelectedTag(mode);
 
 #ifdef USE_CLOUD
+#ifdef USE_SDL_NET
 	Common::String rootPath(ConfMan.get("rootpath", "cloud"));
 	if (rootPath.empty() || !ConfMan.hasKey("rootpath", "cloud")) {
 		_rootPath->setLabel(_c("None", "path"));
@@ -1894,6 +1842,7 @@ void GlobalOptionsDialog::build() {
 		_rootPath->setLabel(rootPath);
 	}
 #endif
+#endif
 }
 
 void GlobalOptionsDialog::clean() {
@@ -1910,16 +1859,67 @@ void GlobalOptionsDialog::clean() {
 	OptionsDialog::clean();
 }
 
-void GlobalOptionsDialog::shiftWidget(Widget *widget, const char *widgetName, int32 xOffset, int32 yOffset) {
-	if (!widget) return;
+#ifdef USE_CLOUD
+#ifdef USE_LIBCURL
+void GlobalOptionsDialog::addCloudControls(GuiObject *boss, const Common::String &prefix, const char *context) {
+	_storagePopUpDesc = new StaticTextWidget(boss, prefix + "StoragePopupDesc", _("Active storage:"), _("Active cloud storage"));
+	_storagePopUp = new PopUpWidget(boss, prefix + "StoragePopup");
+	Common::StringArray list = CloudMan.listStorages();
+	for (uint32 i = 0; i < list.size(); ++i)
+		_storagePopUp->appendEntry(list[i], i);
+	_storagePopUp->setSelected(_selectedStorageIndex);
 
-	int16 x, y;
-	uint16 w, h;
-	if (!g_gui.xmlEval()->getWidgetData(widgetName, x, y, w, h))
-		warning("%s's position is undefined", widgetName);
+	_storageUsernameDesc = new StaticTextWidget(boss, prefix + "StorageUsernameDesc", _("Username:"), _("Username used by this storage"));
+	_storageUsername = new StaticTextWidget(boss, prefix + "StorageUsernameLabel", "<none>", "", ThemeEngine::kFontStyleNormal);
+
+	_storageUsedSpaceDesc = new StaticTextWidget(boss, prefix + "StorageUsedSpaceDesc", _("Used space:"), _("Space used by ScummVM's saved games on this storage"));
+	_storageUsedSpace = new StaticTextWidget(boss, prefix + "StorageUsedSpaceLabel", "0 bytes", "", ThemeEngine::kFontStyleNormal);
+
+	_storageLastSyncDesc = new StaticTextWidget(boss, prefix + "StorageLastSyncDesc", _("Last sync:"), _("When was the last time saved games were synced with this storage"));
+	_storageLastSync = new StaticTextWidget(boss, prefix + "StorageLastSyncLabel", "<never>", "", ThemeEngine::kFontStyleNormal);
+	_storageSyncHint = new StaticTextWidget(boss, prefix + "StorageSyncHint", _c("Saves sync automatically on launch, after saving and on loading.", context), "", ThemeEngine::kFontStyleNormal);
+	_storageSyncSavesButton = new ButtonWidget(boss, prefix + "SyncSavesButton", _("Sync now"), _("Start saves sync"), kSyncSavesStorageCmd);
+
+	_storageDownloadHint = new StaticTextWidget(boss, prefix + "StorageDownloadHint", _c("You can download game files from your cloud ScummVM folder:", context));
+	_storageDownloadButton = new ButtonWidget(boss, prefix + "DownloadButton", _("Download game files"), _("Open downloads manager dialog"), kDownloadStorageCmd);
+
+	_storageDisconnectHint = new StaticTextWidget(boss, prefix + "StorageDisconnectHint", _c("To change account for this storage, disconnect and connect again:", context));
+	_storageDisconnectButton = new ButtonWidget(boss, prefix + "DisconnectButton", _("Disconnect"), _("Stop using this storage on this device"), kDisconnectStorageCmd);
+
+	_storageWizardNotConnectedHint = new StaticTextWidget(boss, prefix + "StorageWizardNotConnectedHint", _c("This storage is not connected yet! To connect,", context));
+	_storageWizardOpenLinkHint = new StaticTextWidget(boss, prefix + "StorageWizardOpenLinkHint", "1. Open this link:");
+	_storageWizardLink = new ButtonWidget(boss, prefix + "StorageWizardLink", "https://cloud.scummvm.org/", _("Open URL"), kOpenUrlStorageCmd);
+	_storageWizardCodeHint = new StaticTextWidget(boss, prefix + "StorageWizardCodeHint", _c("2. Get the code and enter it here:", context));
+	_storageWizardCodeBox = new EditTextWidget(boss, prefix + "StorageWizardCodeBox", "", 0, 0, 0, ThemeEngine::kFontStyleConsole);
+	_storageWizardPasteButton = new ButtonWidget(boss, prefix + "StorageWizardPasteButton", _("Paste"), _("Paste code from clipboard"), kPasteCodeStorageCmd);
+	_storageWizardConnectButton = new ButtonWidget(boss, prefix + "StorageWizardConnectButton", _("3. Connect"), _("Connect your cloud storage account"), kConnectStorageCmd);
+	_storageWizardConnectionStatusHint = new StaticTextWidget(boss, prefix + "StorageWizardConnectionStatusHint", "...");
+
+	setupCloudTab();
+}
+#endif // USE_LIBCURL
+
+#ifdef USE_SDL_NET
+void GlobalOptionsDialog::addWiFiSharingControls(GuiObject *boss, const Common::String &prefix, const char *context) {
+	_runServerButton = new ButtonWidget(boss, prefix + "RunServerButton", _("Run server"), _("Run local webserver"), kRunServerCmd);
+	_serverInfoLabel = new StaticTextWidget(boss, prefix + "ServerInfoLabel", _("Not running"));
+
+	// Root path
+	_rootPathButton = new ButtonWidget(boss, prefix + "RootPathButton", _c("/root/ Path:", context), _("Select which directory will be shown as /root/ in the Files Manager"), kChooseRootDirCmd);
+	_rootPath = new StaticTextWidget(boss, prefix + "RootPath", "/foo/bar", _("Select which directory will be shown as /root/ in the Files Manager"));
+	_rootPathClearButton = addClearButton(boss, prefix + "RootPathClearButton", kRootPathClearCmd);
+
+	uint32 port = Networking::LocalWebserver::getPort();
+
+	_serverPortDesc = new StaticTextWidget(boss, prefix + "ServerPortDesc", _("Server's port:"), _("Port for server to use"));
+	_serverPort = new EditTextWidget(boss, prefix + "ServerPortEditText", Common::String::format("%u", port), 0);
+	_serverPortClearButton = addClearButton(boss, prefix + "ServerPortClearButton", kServerPortClearCmd);
 	
-	widget->setPos(x + xOffset, y + yOffset);
+	reflowWiFiSharingTabLayout();
+
 }
+#endif // USE_SDL_NET
+#endif // USE_CLOUD
 
 void GlobalOptionsDialog::apply() {
 	OptionsDialog::apply();
@@ -1953,12 +1953,14 @@ void GlobalOptionsDialog::apply() {
 #endif
 
 #ifdef USE_CLOUD
+#ifdef USE_SDL_NET
 	Common::String rootPath(_rootPath->getLabel());
 	if (!rootPath.empty() && (rootPath != _c("None", "path")))
 		ConfMan.set("rootpath", rootPath, "cloud");
 	else
 		ConfMan.removeKey("rootpath", "cloud");
 #endif
+#endif
 
 	ConfMan.setInt("autosave_period", _autosavePeriodPopUp->getSelectedTag(), _domain);
 
@@ -2141,6 +2143,7 @@ void GlobalOptionsDialog::handleCommand(CommandSender *sender, uint32 cmd, uint3
 	}
 #endif
 #ifdef USE_CLOUD
+#ifdef USE_SDL_NET
 	case kChooseRootDirCmd: {
 		BrowserDialog browser(_("Select directory for Files Manager /root/"), true);
 		if (browser.runModal() > 0) {
@@ -2155,6 +2158,7 @@ void GlobalOptionsDialog::handleCommand(CommandSender *sender, uint32 cmd, uint3
 		break;
 	}
 #endif
+#endif
 	case kThemePathClearCmd:
 		_themePath->setLabel(_c("None", "path"));
 		break;
@@ -2165,10 +2169,12 @@ void GlobalOptionsDialog::handleCommand(CommandSender *sender, uint32 cmd, uint3
 		_savePath->setLabel(_("Default"));
 		break;
 #ifdef USE_CLOUD
+#ifdef USE_SDL_NET
 	case kRootPathClearCmd:
 		_rootPath->setLabel(_c("None", "path"));
 		break;
 #endif
+#endif
 	case kChooseSoundFontCmd: {
 		BrowserDialog browser(_("Select SoundFont"), false);
 		if (browser.runModal() > 0) {
@@ -2196,12 +2202,12 @@ void GlobalOptionsDialog::handleCommand(CommandSender *sender, uint32 cmd, uint3
 		break;
 	}
 #ifdef USE_CLOUD
-	case kCloudTabContainerReflowCmd:
+#ifdef USE_LIBCURL
+	case kCloudTabContainerReflowCmd: {
 		setupCloudTab();
 		break;
-#ifdef USE_LIBCURL
-	case kPopUpItemSelectedCmd:
-	{
+	}
+	case kPopUpItemSelectedCmd: {
 		// update container's scrollbar
 		reflowLayout();
 		break;
@@ -2322,28 +2328,26 @@ void GlobalOptionsDialog::handleCommand(CommandSender *sender, uint32 cmd, uint3
 	}
 #endif // USE_LIBCURL
 #ifdef USE_SDL_NET
-	case kRunServerCmd:
-		{
+	case kRunServerCmd: {
 #ifdef NETWORKING_LOCALWEBSERVER_ENABLE_PORT_OVERRIDE
-			// save server's port
-			uint32 port = Networking::LocalWebserver::getPort();
-			if (_serverPort) {
-				uint64 contents = _serverPort->getEditString().asUint64();
-				if (contents != 0)
-					port = contents;
-			}
-			ConfMan.setInt("local_server_port", port);
-			ConfMan.flushToDisk();
+		// save server's port
+		uint32 port = Networking::LocalWebserver::getPort();
+		if (_serverPort) {
+			uint64 contents = _serverPort->getEditString().asUint64();
+			if (contents != 0)
+				port = contents;
+		}
+		ConfMan.setInt("local_server_port", port);
+		ConfMan.flushToDisk();
 #endif // NETWORKING_LOCALWEBSERVER_ENABLE_PORT_OVERRIDE
 
-			if (LocalServer.isRunning())
-				LocalServer.stopOnIdle();
-			else
-				LocalServer.start();
-
-			break;
-		}
+		if (LocalServer.isRunning())
+			LocalServer.stopOnIdle();
+		else
+			LocalServer.start();
 
+		break;
+	}
 	case kServerPortClearCmd: {
 		if (_serverPort) {
 			_serverPort->setEditString(Common::String::format("%u", Networking::LocalWebserver::DEFAULT_SERVER_PORT));
@@ -2377,18 +2381,20 @@ void GlobalOptionsDialog::handleCommand(CommandSender *sender, uint32 cmd, uint3
 void GlobalOptionsDialog::handleTickle() {
 	OptionsDialog::handleTickle();
 #ifdef USE_CLOUD
-#ifdef USE_SDL_NET
-	if (LocalServer.isRunning() != _serverWasRunning) {
-		_serverWasRunning = !_serverWasRunning;
-		_redrawCloudTab = true;
-	}
-#endif
+#ifdef USE_LIBCURL
 	if (_redrawCloudTab) {
 		setupCloudTab();
 		g_gui.scheduleTopDialogRedraw();
 		_redrawCloudTab = false;
 	}
-#endif
+#endif // USE_LIBCURL
+#ifdef USE_SDL_NET
+	if (LocalServer.isRunning() != _serverWasRunning) {
+		_serverWasRunning = !_serverWasRunning;
+		reflowWiFiSharingTabLayout();
+	}
+#endif // USE_SDL_NET
+#endif // USE_CLOUD
 }
 
 void GlobalOptionsDialog::reflowLayout() {
@@ -2428,15 +2434,19 @@ void GlobalOptionsDialog::reflowLayout() {
 
 	OptionsDialog::reflowLayout();
 #ifdef USE_CLOUD
+#ifdef USE_LIBCURL
 	setupCloudTab();
-#endif
+#endif // USE_LIBCURL
+#ifdef USE_SDL_NET
+	reflowWiFiSharingTabLayout();
+#endif // USE_SDL_NET
+#endif // USE_CLOUD
 }
 
 #ifdef USE_CLOUD
-void GlobalOptionsDialog::setupCloudTab() {
-	int serverLabelPosition = -1; //no override
 #ifdef USE_LIBCURL
-	_selectedStorageIndex = (_storagePopUp ? _storagePopUp->getSelectedTag() : (uint32) Cloud::kStorageNoneId);
+void GlobalOptionsDialog::setupCloudTab() {
+	_selectedStorageIndex = (_storagePopUp ? _storagePopUp->getSelectedTag() : (uint32)Cloud::kStorageNoneId);
 
 	if (_storagePopUpDesc) _storagePopUpDesc->setVisible(true);
 	if (_storagePopUp) _storagePopUp->setVisible(true);
@@ -2445,7 +2455,9 @@ void GlobalOptionsDialog::setupCloudTab() {
 	bool storageConnected = (username != "");
 	bool shown = (_selectedStorageIndex != Cloud::kStorageNoneId);
 	bool shownConnectedInfo = (shown && storageConnected);
-	
+
+	// there goes layout for connected Storage
+
 	if (_storageUsernameDesc) _storageUsernameDesc->setVisible(shownConnectedInfo);
 	if (_storageUsername) {
 		_storageUsername->setLabel(username);
@@ -2478,199 +2490,124 @@ void GlobalOptionsDialog::setupCloudTab() {
 	if (_storageSyncSavesButton)
 		_storageSyncSavesButton->setVisible(shownConnectedInfo && _selectedStorageIndex == CloudMan.getStorageIndex());
 
-	{
-		int16 x, y;
-		uint16 w, h;
-		int16 downloadHintY, downloadButtonY, disconnectHintY;
-		if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.StorageDownloadHint", x, y, w, h))
-			warning("GlobalOptions_Cloud_Container.StorageDownloadHint's position is undefined");
-		downloadHintY = y;
-		if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.DownloadButton", x, y, w, h))
-			warning("GlobalOptions_Cloud_Container.DownloadButton's position is undefined");
-		downloadButtonY = y;
-		if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.StorageDisconnectHint", x, y, w, h))
-			warning("GlobalOptions_Cloud_Container.StorageDisconnectHint's position is undefined");
-		disconnectHintY = y;
-
-		bool showDownloadButton = (shownConnectedInfo && _selectedStorageIndex == CloudMan.getStorageIndex() && _selectedStorageIndex != Cloud::kStorageGoogleDriveId); // cannot download via Google Drive
-		if (_storageDownloadHint) _storageDownloadHint->setVisible(showDownloadButton);
-		if (_storageDownloadButton) _storageDownloadButton->setVisible(showDownloadButton);
-		if (_storageDisconnectHint) _storageDisconnectHint->setVisible(shownConnectedInfo);
-		if (_storageDisconnectButton) _storageDisconnectButton->setVisible(shownConnectedInfo);
-
-		if (showDownloadButton) {
-			if (_storageDownloadHint) _storageDownloadHint->setPos(_storageDownloadHint->getRelX(), downloadHintY);
-			if (_storageDownloadButton) _storageDownloadButton->setPos(_storageDownloadButton->getRelX(), downloadButtonY);
-			if (_storageDisconnectHint) _storageDisconnectHint->setPos(_storageDisconnectHint->getRelX(), disconnectHintY);
-			if (_storageDisconnectButton)_storageDisconnectButton->setPos(_storageDisconnectButton->getRelX(), disconnectHintY + downloadButtonY - downloadHintY);
-		} else {
-			if (_storageDisconnectHint) _storageDisconnectHint->setPos(_storageDisconnectHint->getRelX(), downloadHintY);
-			if (_storageDisconnectButton)_storageDisconnectButton->setPos(_storageDisconnectButton->getRelX(), downloadButtonY);
-		}
+	int16 x, y;
+	uint16 w, h;
+	int16 downloadHintY, downloadButtonY, disconnectHintY;
+	if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.StorageDownloadHint", x, y, w, h))
+		warning("GlobalOptions_Cloud_Container.StorageDownloadHint's position is undefined");
+	downloadHintY = y;
+	if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.DownloadButton", x, y, w, h))
+		warning("GlobalOptions_Cloud_Container.DownloadButton's position is undefined");
+	downloadButtonY = y;
+	if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.StorageDisconnectHint", x, y, w, h))
+		warning("GlobalOptions_Cloud_Container.StorageDisconnectHint's position is undefined");
+	disconnectHintY = y;
+
+	bool showDownloadButton = (shownConnectedInfo && _selectedStorageIndex == CloudMan.getStorageIndex() && _selectedStorageIndex != Cloud::kStorageGoogleDriveId); // cannot download via Google Drive
+	if (_storageDownloadHint) _storageDownloadHint->setVisible(showDownloadButton);
+	if (_storageDownloadButton) _storageDownloadButton->setVisible(showDownloadButton);
+	if (_storageDisconnectHint) _storageDisconnectHint->setVisible(shownConnectedInfo);
+	if (_storageDisconnectButton) _storageDisconnectButton->setVisible(shownConnectedInfo);
+
+	if (showDownloadButton) {
+		if (_storageDownloadHint) _storageDownloadHint->setPos(_storageDownloadHint->getRelX(), downloadHintY);
+		if (_storageDownloadButton) _storageDownloadButton->setPos(_storageDownloadButton->getRelX(), downloadButtonY);
+		if (_storageDisconnectHint) _storageDisconnectHint->setPos(_storageDisconnectHint->getRelX(), disconnectHintY);
+		if (_storageDisconnectButton)_storageDisconnectButton->setPos(_storageDisconnectButton->getRelX(), disconnectHintY + downloadButtonY - downloadHintY);
+	} else {
+		if (_storageDisconnectHint) _storageDisconnectHint->setPos(_storageDisconnectHint->getRelX(), downloadHintY);
+		if (_storageDisconnectButton)_storageDisconnectButton->setPos(_storageDisconnectButton->getRelX(), downloadButtonY);
+	}
 
-		if (!shownConnectedInfo) {
-			bool connecting = _connectingStorage;
-			if (_storageWizardNotConnectedHint) _storageWizardNotConnectedHint->setVisible(shown);
-			if (_storageWizardOpenLinkHint) _storageWizardOpenLinkHint->setVisible(shown);
-			if (_storageWizardLink) {
-				_storageWizardLink->setVisible(shown);
-				_storageWizardLink->setEnabled(g_system->hasFeature(OSystem::kFeatureOpenUrl) && !connecting);
-			}
-			if (_storageWizardCodeHint) _storageWizardCodeHint->setVisible(shown);
-			if (_storageWizardCodeBox) {
-				_storageWizardCodeBox->setVisible(shown);
-				_storageWizardCodeBox->setEnabled(!connecting);
-			}
-			if (_storageWizardPasteButton) {
-				_storageWizardPasteButton->setVisible(shown && g_system->hasFeature(OSystem::kFeatureClipboardSupport));
-				_storageWizardPasteButton->setEnabled(!connecting);
-			}
-			if (_storageWizardConnectButton) {
-				_storageWizardConnectButton->setVisible(shown);
-				_storageWizardConnectButton->setEnabled(!connecting);
-			}
-			if (_storageWizardConnectionStatusHint) {
-				_storageWizardConnectionStatusHint->setVisible(shown && _storageWizardConnectionStatusHint->getLabel() != "...");
-				_storageWizardConnectionStatusHint->setEnabled(!connecting);
-			}
+	// there goes layout for non-connected Storage (connection wizard)
 
-			int16 x2, y2;
-			uint16 w2, h2;
-			int16 shiftUp;
-			if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.StorageUsernameDesc", x2, y2, w2, h2))
-				warning("GlobalOptions_Cloud_Container.StorageUsernameDesc's position is undefined");
-			shiftUp = y2;
-			if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.StorageWizardNotConnectedHint", x2, y2, w2, h2))
-				warning("GlobalOptions_Cloud_Container.StorageWizardNotConnectedHint's position is undefined");
-			shiftUp = y2 - shiftUp;
-
-			shiftWidget(_storageWizardNotConnectedHint, "GlobalOptions_Cloud_Container.StorageWizardNotConnectedHint", 0, -shiftUp);
-			shiftWidget(_storageWizardOpenLinkHint, "GlobalOptions_Cloud_Container.StorageWizardOpenLinkHint", 0, -shiftUp);
-			shiftWidget(_storageWizardLink, "GlobalOptions_Cloud_Container.StorageWizardLink", 0, -shiftUp);
-			shiftWidget(_storageWizardCodeHint, "GlobalOptions_Cloud_Container.StorageWizardCodeHint", 0, -shiftUp);
-			shiftWidget(_storageWizardCodeBox, "GlobalOptions_Cloud_Container.StorageWizardCodeBox", 0, -shiftUp);
-			shiftWidget(_storageWizardPasteButton, "GlobalOptions_Cloud_Container.StorageWizardPasteButton", 0, -shiftUp);
-			shiftWidget(_storageWizardConnectButton, "GlobalOptions_Cloud_Container.StorageWizardConnectButton", 0, -shiftUp);
-			shiftWidget(_storageWizardConnectionStatusHint, "GlobalOptions_Cloud_Container.StorageWizardConnectionStatusHint", 0, -shiftUp);
+	if (!shownConnectedInfo) {
+		bool connecting = _connectingStorage;
+		if (_storageWizardNotConnectedHint) _storageWizardNotConnectedHint->setVisible(shown);
+		if (_storageWizardOpenLinkHint) _storageWizardOpenLinkHint->setVisible(shown);
+		if (_storageWizardLink) {
+			_storageWizardLink->setVisible(shown);
+			_storageWizardLink->setEnabled(g_system->hasFeature(OSystem::kFeatureOpenUrl) && !connecting);
 		}
-
-		if (!shown)
-			serverLabelPosition = (_storageUsernameDesc ? _storageUsernameDesc->getRelY() : 0);
-		else {
-			if (shownConnectedInfo)
-				serverLabelPosition = (_storageDisconnectButton ? _storageDisconnectButton->getRelY() + _storageDisconnectButton->getHeight() + 16 : 0);
-			else
-				serverLabelPosition = (_storageWizardConnectButton ? _storageWizardConnectButton->getRelY() + _storageWizardConnectButton->getHeight() + 16 : 0);
+		if (_storageWizardCodeHint) _storageWizardCodeHint->setVisible(shown);
+		if (_storageWizardCodeBox) {
+			_storageWizardCodeBox->setVisible(shown);
+			_storageWizardCodeBox->setEnabled(!connecting);
+		}
+		if (_storageWizardPasteButton) {
+			_storageWizardPasteButton->setVisible(shown && g_system->hasFeature(OSystem::kFeatureClipboardSupport));
+			_storageWizardPasteButton->setEnabled(!connecting);
+		}
+		if (_storageWizardConnectButton) {
+			_storageWizardConnectButton->setVisible(shown);
+			_storageWizardConnectButton->setEnabled(!connecting);
+		}
+		if (_storageWizardConnectionStatusHint) {
+			_storageWizardConnectionStatusHint->setVisible(shown && _storageWizardConnectionStatusHint->getLabel() != "...");
+			_storageWizardConnectionStatusHint->setEnabled(!connecting);
 		}
+
+		int16 shiftUp;
+		if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.StorageUsernameDesc", x, y, w, h))
+			warning("GlobalOptions_Cloud_Container.StorageUsernameDesc's position is undefined");
+		shiftUp = y;
+		if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.StorageWizardNotConnectedHint", x, y, w, h))
+			warning("GlobalOptions_Cloud_Container.StorageWizardNotConnectedHint's position is undefined");
+		shiftUp = y - shiftUp;
+
+		shiftWidget(_storageWizardNotConnectedHint, "GlobalOptions_Cloud_Container.StorageWizardNotConnectedHint", 0, -shiftUp);
+		shiftWidget(_storageWizardOpenLinkHint, "GlobalOptions_Cloud_Container.StorageWizardOpenLinkHint", 0, -shiftUp);
+		shiftWidget(_storageWizardLink, "GlobalOptions_Cloud_Container.StorageWizardLink", 0, -shiftUp);
+		shiftWidget(_storageWizardCodeHint, "GlobalOptions_Cloud_Container.StorageWizardCodeHint", 0, -shiftUp);
+		shiftWidget(_storageWizardCodeBox, "GlobalOptions_Cloud_Container.StorageWizardCodeBox", 0, -shiftUp);
+		shiftWidget(_storageWizardPasteButton, "GlobalOptions_Cloud_Container.StorageWizardPasteButton", 0, -shiftUp);
+		shiftWidget(_storageWizardConnectButton, "GlobalOptions_Cloud_Container.StorageWizardConnectButton", 0, -shiftUp);
+		shiftWidget(_storageWizardConnectionStatusHint, "GlobalOptions_Cloud_Container.StorageWizardConnectionStatusHint", 0, -shiftUp);
 	}
-#else // USE_LIBCURL
-	_selectedStorageIndex = 0;
-
-	if (_storagePopUpDesc)
-		_storagePopUpDesc->setVisible(false);
-	if (_storagePopUp)
-		_storagePopUp->setVisible(false);
-	if (_storageUsernameDesc)
-		_storageUsernameDesc->setVisible(false);
-	if (_storageUsernameDesc)
-		_storageUsernameDesc->setVisible(false);
-	if (_storageUsername)
-		_storageUsername->setVisible(false);
-	if (_storageUsedSpaceDesc)
-		_storageUsedSpaceDesc->setVisible(false);
-	if (_storageUsedSpace)
-		_storageUsedSpace->setVisible(false);
-	if (_storageLastSyncDesc)
-		_storageLastSyncDesc->setVisible(false);
-	if (_storageLastSync)
-		_storageLastSync->setVisible(false);
-	if (_storageDisconnectButton)
-		_storageDisconnectButton->setVisible(false);
-	if (_storageSyncSavesButton)
-		_storageSyncSavesButton->setVisible(false);
-	if (_storageDownloadButton)
-		_storageDownloadButton->setVisible(false);
+}
+
+void GlobalOptionsDialog::shiftWidget(Widget *widget, const char *widgetName, int32 xOffset, int32 yOffset) {
+	if (!widget) return;
 
-	serverLabelPosition = (_storagePopUpDesc ? _storagePopUpDesc->getRelY() : 0);
-#endif // USE_LIBCURL
-#ifdef USE_SDL_NET
-	//determine original widget's positions
 	int16 x, y;
 	uint16 w, h;
-	int serverButtonY, serverInfoY;
-	int serverRootButtonY, serverRootY, serverRootClearButtonY;
-	int serverPortDescY, serverPortY, serverPortClearButtonY;
-	if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.RunServerButton", x, y, w, h))
-		warning("GlobalOptions_Cloud_Container.RunServerButton's position is undefined");
-	serverButtonY = y;
-	if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.ServerInfoLabel", x, y, w, h))
-		warning("GlobalOptions_Cloud_Container.ServerInfoLabel's position is undefined");
-	serverInfoY = y;
-
-	if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.RootPathButton", x, y, w, h))
-		warning("GlobalOptions_Cloud_Container.RootPathButton's position is undefined");
-	serverRootButtonY = y;
-	if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.RootPath", x, y, w, h))
-		warning("GlobalOptions_Cloud_Container.RootPath's position is undefined");
-	serverRootY = y;
-	if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.RootPathClearButton", x, y, w, h))
-		warning("GlobalOptions_Cloud_Container.RootPathClearButton's position is undefined");
-	serverRootClearButtonY = y;
-
-	if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.ServerPortDesc", x, y, w, h))
-		warning("GlobalOptions_Cloud_Container.ServerPortDesc's position is undefined");
-	serverPortDescY = y;
-	if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.ServerPortEditText", x, y, w, h))
-		warning("GlobalOptions_Cloud_Container.ServerPortEditText's position is undefined");
-	serverPortY = y;
-	if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.ServerPortClearButton", x, y, w, h))
-		warning("GlobalOptions_Cloud_Container.ServerPortClearButton's position is undefined");
-	serverPortClearButtonY = y;
+	if (!g_gui.xmlEval()->getWidgetData(widgetName, x, y, w, h))
+		warning("%s's position is undefined", widgetName);
 
+	widget->setPos(x + xOffset, y + yOffset);
+}
+#endif // USE_LIBCURL
+
+#ifdef USE_SDL_NET
+void GlobalOptionsDialog::reflowWiFiSharingTabLayout() {
 	bool serverIsRunning = LocalServer.isRunning();
 
-	if (serverLabelPosition < 0)
-		serverLabelPosition = serverInfoY;
 	if (_runServerButton) {
 		_runServerButton->setVisible(true);
-		_runServerButton->setPos(_runServerButton->getRelX(), serverLabelPosition + serverButtonY - serverInfoY);
 		_runServerButton->setLabel(_(serverIsRunning ? "Stop server" : "Run server"));
 		_runServerButton->setTooltip(_(serverIsRunning ? "Stop local webserver" : "Run local webserver"));
 	}
 	if (_serverInfoLabel) {
 		_serverInfoLabel->setVisible(true);
-		_serverInfoLabel->setPos(_serverInfoLabel->getRelX(), serverLabelPosition);
 		if (serverIsRunning)
 			_serverInfoLabel->setLabel(LocalServer.getAddress());
 		else
 			_serverInfoLabel->setLabel(_("Not running"));
 	}
-	if (_rootPathButton) {
-		_rootPathButton->setVisible(true);
-		_rootPathButton->setPos(_rootPathButton->getRelX(), serverLabelPosition + serverRootButtonY - serverInfoY);
-	}
-	if (_rootPath) {
-		_rootPath->setVisible(true);
-		_rootPath->setPos(_rootPath->getRelX(), serverLabelPosition + serverRootY - serverInfoY);
-	}
-	if (_rootPathClearButton) {
-		_rootPathClearButton->setVisible(true);
-		_rootPathClearButton->setPos(_rootPathClearButton->getRelX(), serverLabelPosition + serverRootClearButtonY - serverInfoY);
-	}
+	if (_rootPathButton) _rootPathButton->setVisible(true);
+	if (_rootPath) _rootPath->setVisible(true);	
+	if (_rootPathClearButton) _rootPathClearButton->setVisible(true);
 #ifdef NETWORKING_LOCALWEBSERVER_ENABLE_PORT_OVERRIDE
 	if (_serverPortDesc) {
 		_serverPortDesc->setVisible(true);
-		_serverPortDesc->setPos(_serverPortDesc->getRelX(), serverLabelPosition + serverPortDescY - serverInfoY);
 		_serverPortDesc->setEnabled(!serverIsRunning);
 	}
 	if (_serverPort) {
 		_serverPort->setVisible(true);
-		_serverPort->setPos(_serverPort->getRelX(), serverLabelPosition + serverPortY - serverInfoY);
 		_serverPort->setEnabled(!serverIsRunning);
 	}
 	if (_serverPortClearButton) {
 		_serverPortClearButton->setVisible(true);
-		_serverPortClearButton->setPos(_serverPortClearButton->getRelX(), serverLabelPosition + serverPortClearButtonY - serverInfoY);
 		_serverPortClearButton->setEnabled(!serverIsRunning);
 	}
 #else // NETWORKING_LOCALWEBSERVER_ENABLE_PORT_OVERRIDE
@@ -2681,49 +2618,8 @@ void GlobalOptionsDialog::setupCloudTab() {
 	if (_serverPortClearButton)
 		_serverPortClearButton->setVisible(false);
 #endif // NETWORKING_LOCALWEBSERVER_ENABLE_PORT_OVERRIDE
-#else // USE_SDL_NET
-	if (_runServerButton)
-		_runServerButton->setVisible(false);
-	if (_serverInfoLabel) {
-		_serverInfoLabel->setPos(_serverInfoLabel->getRelX(), serverLabelPosition); // Prevent compiler warning from serverLabelPosition being unused.
-		_serverInfoLabel->setVisible(false);
-	}
-	if (_rootPathButton)
-		_rootPathButton->setVisible(false);
-	if (_rootPath)
-		_rootPath->setVisible(false);
-	if (_rootPathClearButton)
-		_rootPathClearButton->setVisible(false);
-	if (_serverPortDesc)
-		_serverPortDesc->setVisible(false);
-	if (_serverPort)
-		_serverPort->setVisible(false);
-	if (_serverPortClearButton)
-		_serverPortClearButton->setVisible(false);
-#endif // USE_SDL_NET
-
-	// temporary hide all local server-related info to see how Cloud looks without it
-	/*
-	if (_runServerButton)
-		_runServerButton->setVisible(false);
-	if (_serverInfoLabel) {
-		_serverInfoLabel->setPos(_serverInfoLabel->getRelX(), serverLabelPosition);
-		_serverInfoLabel->setVisible(false);
-	}
-	if (_rootPathButton)
-		_rootPathButton->setVisible(false);
-	if (_rootPath)
-		_rootPath->setVisible(false);
-	if (_rootPathClearButton)
-		_rootPathClearButton->setVisible(false);
-	if (_serverPortDesc)
-		_serverPortDesc->setVisible(false);
-	if (_serverPort)
-		_serverPort->setVisible(false);
-	if (_serverPortClearButton)
-		_serverPortClearButton->setVisible(false);
-	*/
 }
+#endif // USE_SDL_NET
 
 #ifdef USE_LIBCURL
 void GlobalOptionsDialog::storageConnectionCallback(Networking::ErrorResponse response) {
diff --git a/gui/options.h b/gui/options.h
index 13983c1..c10dcca 100644
--- a/gui/options.h
+++ b/gui/options.h
@@ -291,12 +291,13 @@ protected:
 #endif
 
 #ifdef USE_CLOUD
+#ifdef USE_LIBCURL
 	//
 	// Cloud controls
 	//
 	uint32 _selectedStorageIndex;
 	StaticTextWidget *_storagePopUpDesc;
-	PopUpWidget *_storagePopUp;
+	PopUpWidget      *_storagePopUp;
 	StaticTextWidget *_storageUsernameDesc;
 	StaticTextWidget *_storageUsername;
 	StaticTextWidget *_storageUsedSpaceDesc;
@@ -315,32 +316,39 @@ protected:
 	StaticTextWidget *_storageWizardOpenLinkHint;
 	StaticTextWidget *_storageWizardLink;
 	StaticTextWidget *_storageWizardCodeHint;
-	EditTextWidget *_storageWizardCodeBox;
+	EditTextWidget   *_storageWizardCodeBox;
 	ButtonWidget	 *_storageWizardPasteButton;
 	ButtonWidget	 *_storageWizardConnectButton;
 	StaticTextWidget *_storageWizardConnectionStatusHint;
+	bool _redrawCloudTab;
+
+	void addCloudControls(GuiObject *boss, const Common::String &prefix, const char *context = nullptr);
+	void setupCloudTab();
+	void shiftWidget(Widget *widget, const char *widgetName, int32 xOffset, int32 yOffset);
 
+	void storageConnectionCallback(Networking::ErrorResponse response);
+	void storageSavesSyncedCallback(Cloud::Storage::BoolResponse response);
+	void storageErrorCallback(Networking::ErrorResponse response);
+#endif // USE_LIBCURL
+
+#ifdef USE_SDL_NET
+	//
+	// Wi-Fi Sharing controls
+	//
 	ButtonWidget	 *_runServerButton;
 	StaticTextWidget *_serverInfoLabel;
 	ButtonWidget	 *_rootPathButton;
 	StaticTextWidget *_rootPath;
 	ButtonWidget	 *_rootPathClearButton;
 	StaticTextWidget *_serverPortDesc;
-	EditTextWidget *_serverPort;
+	EditTextWidget   *_serverPort;
 	ButtonWidget	 *_serverPortClearButton;
-	bool _redrawCloudTab;
-#ifdef USE_SDL_NET
 	bool _serverWasRunning;
-#endif
 
-	void shiftWidget(Widget *widget, const char *widgetName, int32 xOffset, int32 yOffset);
-	void setupCloudTab();
+	void addWiFiSharingControls(GuiObject *boss, const Common::String &prefix, const char *context = nullptr);
+	void reflowWiFiSharingTabLayout();
+#endif // USE_SDL_NET
 
-#ifdef USE_LIBCURL
-	void storageConnectionCallback(Networking::ErrorResponse response);
-	void storageSavesSyncedCallback(Cloud::Storage::BoolResponse response);
-	void storageErrorCallback(Networking::ErrorResponse response);
-#endif
 #endif // USE_CLOUD
 };
 
diff --git a/gui/themes/scummclassic.zip b/gui/themes/scummclassic.zip
index 78229c3..1e8f3ad 100644
Binary files a/gui/themes/scummclassic.zip and b/gui/themes/scummclassic.zip differ
diff --git a/gui/themes/scummclassic/classic_layout.stx b/gui/themes/scummclassic/classic_layout.stx
index 3929af9..f5fb08e 100644
--- a/gui/themes/scummclassic/classic_layout.stx
+++ b/gui/themes/scummclassic/classic_layout.stx
@@ -729,8 +729,11 @@
 						height = 'Globals.Line.Height'
 				/>
 			</layout>
+		</layout>
+	</dialog>
 
-			<!-- here goes Wi-Fi Sharing -->
+	<dialog name = 'GlobalOptions_WiFiSharing' overlays = 'Dialog.GlobalOptions.TabWidget'>
+		<layout type = 'vertical' padding = '16, 16, 16, 16' spacing = '8'>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'>
 				<widget name = 'RunServerButton'
 						type = 'Button'
diff --git a/gui/themes/scummclassic/classic_layout_lowres.stx b/gui/themes/scummclassic/classic_layout_lowres.stx
index 8f39a69..bbeead9 100644
--- a/gui/themes/scummclassic/classic_layout_lowres.stx
+++ b/gui/themes/scummclassic/classic_layout_lowres.stx
@@ -734,8 +734,11 @@
 						height = 'Globals.Line.Height'
 				/>
 			</layout>
+		</layout>
+	</dialog>
 
-			<!-- here goes Wi-Fi Sharing -->
+	<dialog name = 'GlobalOptions_WiFiSharing' overlays = 'Dialog.GlobalOptions.TabWidget'>
+		<layout type = 'vertical' padding = '16, 16, 16, 16' spacing = '8'>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'>
 				<widget name = 'RunServerButton'
 						type = 'Button'
diff --git a/gui/themes/scummmodern.zip b/gui/themes/scummmodern.zip
index d1b4c23..d6ce4c3 100644
Binary files a/gui/themes/scummmodern.zip and b/gui/themes/scummmodern.zip differ
diff --git a/gui/themes/scummmodern/scummmodern_layout.stx b/gui/themes/scummmodern/scummmodern_layout.stx
index b1d4e28..967e5e5 100644
--- a/gui/themes/scummmodern/scummmodern_layout.stx
+++ b/gui/themes/scummmodern/scummmodern_layout.stx
@@ -743,8 +743,11 @@
 						height = 'Globals.Line.Height'
 				/>
 			</layout>
+		</layout>
+	</dialog>
 
-			<!-- here goes Wi-Fi Sharing -->
+	<dialog name = 'GlobalOptions_WiFiSharing' overlays = 'Dialog.GlobalOptions.TabWidget'>
+		<layout type = 'vertical' padding = '16, 16, 16, 16' spacing = '8'>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'>
 				<widget name = 'RunServerButton'
 						type = 'Button'
diff --git a/gui/themes/scummmodern/scummmodern_layout_lowres.stx b/gui/themes/scummmodern/scummmodern_layout_lowres.stx
index fd9990d..8ff830a 100644
--- a/gui/themes/scummmodern/scummmodern_layout_lowres.stx
+++ b/gui/themes/scummmodern/scummmodern_layout_lowres.stx
@@ -732,8 +732,11 @@
 						height = 'Globals.Line.Height'
 				/>
 			</layout>
+		</layout>
+	</dialog>
 
-			<!-- here goes Wi-Fi Sharing -->
+	<dialog name = 'GlobalOptions_WiFiSharing' overlays = 'Dialog.GlobalOptions.TabWidget'>
+		<layout type = 'vertical' padding = '16, 16, 16, 16' spacing = '8'>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'>
 				<widget name = 'RunServerButton'
 						type = 'Button'
diff --git a/gui/themes/scummremastered.zip b/gui/themes/scummremastered.zip
index 5b5f82d..763fc46 100644
Binary files a/gui/themes/scummremastered.zip and b/gui/themes/scummremastered.zip differ
diff --git a/gui/themes/scummremastered/remastered_layout.stx b/gui/themes/scummremastered/remastered_layout.stx
index b1d4e28..967e5e5 100644
--- a/gui/themes/scummremastered/remastered_layout.stx
+++ b/gui/themes/scummremastered/remastered_layout.stx
@@ -743,8 +743,11 @@
 						height = 'Globals.Line.Height'
 				/>
 			</layout>
+		</layout>
+	</dialog>
 
-			<!-- here goes Wi-Fi Sharing -->
+	<dialog name = 'GlobalOptions_WiFiSharing' overlays = 'Dialog.GlobalOptions.TabWidget'>
+		<layout type = 'vertical' padding = '16, 16, 16, 16' spacing = '8'>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'>
 				<widget name = 'RunServerButton'
 						type = 'Button'
diff --git a/gui/themes/scummremastered/remastered_layout_lowres.stx b/gui/themes/scummremastered/remastered_layout_lowres.stx
index fd9990d..f16b905 100644
--- a/gui/themes/scummremastered/remastered_layout_lowres.stx
+++ b/gui/themes/scummremastered/remastered_layout_lowres.stx
@@ -609,7 +609,7 @@
 		<layout type = 'vertical' padding = '0, 0, 0, 0'>
 			<widget name = 'Container'/>
 		</layout>
-	</dialog>
+	</dialog>	
 
 	<dialog name = 'GlobalOptions_Cloud_Container' overlays = 'GlobalOptions_Cloud.Container'>
 		<layout type = 'vertical' padding = '10, 13, 10, 10' spacing = '8'>
@@ -732,8 +732,11 @@
 						height = 'Globals.Line.Height'
 				/>
 			</layout>
+		</layout>
+	</dialog>
 
-			<!-- here goes Wi-Fi Sharing -->
+	<dialog name = 'GlobalOptions_WiFiSharing' overlays = 'Dialog.GlobalOptions.TabWidget'>
+		<layout type = 'vertical' padding = '16, 16, 16, 16' spacing = '8'>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'>
 				<widget name = 'RunServerButton'
 						type = 'Button'


Commit: 28c93ed027130e10a8bd2ea9f57aa3cb5e2d147a
    https://github.com/scummvm/scummvm/commit/28c93ed027130e10a8bd2ea9f57aa3cb5e2d147a
Author: Alexander Tkachev (alexander at tkachov.ru)
Date: 2019-07-30T14:51:41-04:00

Commit Message:
GUI: Fix Cloud tab wizard elements displaying after connecting storage

Elements were displayed until scrollbar was used, now they are hidden
right after storage is connected.

Changed paths:
    gui/options.cpp


diff --git a/gui/options.cpp b/gui/options.cpp
index d69cd29..b555727 100644
--- a/gui/options.cpp
+++ b/gui/options.cpp
@@ -2521,32 +2521,33 @@ void GlobalOptionsDialog::setupCloudTab() {
 
 	// there goes layout for non-connected Storage (connection wizard)
 
-	if (!shownConnectedInfo) {
-		bool connecting = _connectingStorage;
-		if (_storageWizardNotConnectedHint) _storageWizardNotConnectedHint->setVisible(shown);
-		if (_storageWizardOpenLinkHint) _storageWizardOpenLinkHint->setVisible(shown);
-		if (_storageWizardLink) {
-			_storageWizardLink->setVisible(shown);
-			_storageWizardLink->setEnabled(g_system->hasFeature(OSystem::kFeatureOpenUrl) && !connecting);
-		}
-		if (_storageWizardCodeHint) _storageWizardCodeHint->setVisible(shown);
-		if (_storageWizardCodeBox) {
-			_storageWizardCodeBox->setVisible(shown);
-			_storageWizardCodeBox->setEnabled(!connecting);
-		}
-		if (_storageWizardPasteButton) {
-			_storageWizardPasteButton->setVisible(shown && g_system->hasFeature(OSystem::kFeatureClipboardSupport));
-			_storageWizardPasteButton->setEnabled(!connecting);
-		}
-		if (_storageWizardConnectButton) {
-			_storageWizardConnectButton->setVisible(shown);
-			_storageWizardConnectButton->setEnabled(!connecting);
-		}
-		if (_storageWizardConnectionStatusHint) {
-			_storageWizardConnectionStatusHint->setVisible(shown && _storageWizardConnectionStatusHint->getLabel() != "...");
-			_storageWizardConnectionStatusHint->setEnabled(!connecting);
-		}
+	shown = (!shownConnectedInfo && shown);
+	bool wizardEnabled = !_connectingStorage;
+	if (_storageWizardNotConnectedHint) _storageWizardNotConnectedHint->setVisible(shown);
+	if (_storageWizardOpenLinkHint) _storageWizardOpenLinkHint->setVisible(shown);
+	if (_storageWizardLink) {
+		_storageWizardLink->setVisible(shown);
+		_storageWizardLink->setEnabled(g_system->hasFeature(OSystem::kFeatureOpenUrl) && wizardEnabled);
+	}
+	if (_storageWizardCodeHint) _storageWizardCodeHint->setVisible(shown);
+	if (_storageWizardCodeBox) {
+		_storageWizardCodeBox->setVisible(shown);
+		_storageWizardCodeBox->setEnabled(wizardEnabled);
+	}
+	if (_storageWizardPasteButton) {
+		_storageWizardPasteButton->setVisible(shown && g_system->hasFeature(OSystem::kFeatureClipboardSupport));
+		_storageWizardPasteButton->setEnabled(wizardEnabled);
+	}
+	if (_storageWizardConnectButton) {
+		_storageWizardConnectButton->setVisible(shown);
+		_storageWizardConnectButton->setEnabled(wizardEnabled);
+	}
+	if (_storageWizardConnectionStatusHint) {
+		_storageWizardConnectionStatusHint->setVisible(shown && _storageWizardConnectionStatusHint->getLabel() != "...");
+		_storageWizardConnectionStatusHint->setEnabled(wizardEnabled);
+	}
 
+	if (!shownConnectedInfo) {
 		int16 shiftUp;
 		if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.StorageUsernameDesc", x, y, w, h))
 			warning("GlobalOptions_Cloud_Container.StorageUsernameDesc's position is undefined");


Commit: a1b847a1ef2ef88ba1ea8c1001cc650dfebcf5a2
    https://github.com/scummvm/scummvm/commit/a1b847a1ef2ef88ba1ea8c1001cc650dfebcf5a2
Author: Alexander Tkachev (alexander at tkachov.ru)
Date: 2019-07-30T14:51:41-04:00

Commit Message:
GUI: Make Cloud tab wizard input box cleanup value

Now input box does not remember old code you've put in it if you changed
selected storage or disconnected one.

Changed paths:
    gui/options.cpp


diff --git a/gui/options.cpp b/gui/options.cpp
index b555727..7caef1f 100644
--- a/gui/options.cpp
+++ b/gui/options.cpp
@@ -2208,6 +2208,8 @@ void GlobalOptionsDialog::handleCommand(CommandSender *sender, uint32 cmd, uint3
 		break;
 	}
 	case kPopUpItemSelectedCmd: {
+		if (_storageWizardCodeBox)
+			_storageWizardCodeBox->setEditString("");
 		// update container's scrollbar
 		reflowLayout();
 		break;
@@ -2298,6 +2300,9 @@ void GlobalOptionsDialog::handleCommand(CommandSender *sender, uint32 cmd, uint3
 		break;
 	}
 	case kDisconnectStorageCmd: {
+		if (_storageWizardCodeBox)
+			_storageWizardCodeBox->setEditString("");
+
 		if (_selectedStorageIndex == CloudMan.getStorageIndex() && CloudMan.isWorking()) {
 			bool cancel = true;
 


Commit: a9d47a163ea625782517125f8aa31735039985a0
    https://github.com/scummvm/scummvm/commit/a9d47a163ea625782517125f8aa31735039985a0
Author: Alexander Tkachev (alexander at tkachov.ru)
Date: 2019-07-30T14:51:41-04:00

Commit Message:
GUI: Fix Cloud tab scrolling problem

GlobalOptionsDialog is now also a CommandSender, so it could send
command "scroll to the top" when storage is disconnected in Cloud tab
(layout updates, that's why scrolling is needed).

Changed paths:
    gui/options.cpp
    gui/options.h


diff --git a/gui/options.cpp b/gui/options.cpp
index 7caef1f..f7c9f60 100644
--- a/gui/options.cpp
+++ b/gui/options.cpp
@@ -1447,7 +1447,7 @@ void OptionsDialog::setupGraphicsTab() {
 
 
 GlobalOptionsDialog::GlobalOptionsDialog(LauncherDialog *launcher)
-	: OptionsDialog(Common::ConfigManager::kApplicationDomain, "GlobalOptions"), _launcher(launcher) {
+	: OptionsDialog(Common::ConfigManager::kApplicationDomain, "GlobalOptions"), CommandSender(nullptr), _launcher(launcher) {
 #ifdef GUI_ENABLE_KEYSDIALOG
 	_keysDialog = 0;
 #endif
@@ -1755,6 +1755,7 @@ void GlobalOptionsDialog::build() {
 	ScrollContainerWidget *container = new ScrollContainerWidget(tab, "GlobalOptions_Cloud.Container", kCloudTabContainerReflowCmd);
 	container->setTarget(this);
 	container->setBackgroundType(ThemeEngine::kDialogBackgroundNone);
+	setTarget(container);
 
 	addCloudControls(container, "GlobalOptions_Cloud_Container.", context);
 #endif USE_LIBCURL
@@ -2329,6 +2330,7 @@ void GlobalOptionsDialog::handleCommand(CommandSender *sender, uint32 cmd, uint3
 
 		CloudMan.disconnectStorage(_selectedStorageIndex);
 		_redrawCloudTab = true;
+		sendCommand(kSetPositionCmd, 0);
 		break;
 	}
 #endif // USE_LIBCURL
diff --git a/gui/options.h b/gui/options.h
index c10dcca..fe9c93c 100644
--- a/gui/options.h
+++ b/gui/options.h
@@ -237,7 +237,7 @@ protected:
 };
 
 
-class GlobalOptionsDialog : public OptionsDialog {
+class GlobalOptionsDialog : public OptionsDialog, public CommandSender {
 public:
 	GlobalOptionsDialog(LauncherDialog *launcher);
 	~GlobalOptionsDialog();


Commit: e4da63823ad363cfd24b15bc671aa6cb8f5b43f1
    https://github.com/scummvm/scummvm/commit/e4da63823ad363cfd24b15bc671aa6cb8f5b43f1
Author: Alexander Tkachev (alexander at tkachov.ru)
Date: 2019-07-30T14:51:41-04:00

Commit Message:
GUI: Add feature description into Wi-Fi Sharing tab

Changed paths:
    gui/options.cpp
    gui/options.h
    gui/themes/scummclassic.zip
    gui/themes/scummclassic/classic_layout.stx
    gui/themes/scummclassic/classic_layout_lowres.stx
    gui/themes/scummmodern.zip
    gui/themes/scummmodern/scummmodern_layout.stx
    gui/themes/scummmodern/scummmodern_layout_lowres.stx
    gui/themes/scummremastered.zip
    gui/themes/scummremastered/remastered_layout.stx
    gui/themes/scummremastered/remastered_layout_lowres.stx


diff --git a/gui/options.cpp b/gui/options.cpp
index f7c9f60..245b985 100644
--- a/gui/options.cpp
+++ b/gui/options.cpp
@@ -1514,6 +1514,8 @@ GlobalOptionsDialog::GlobalOptionsDialog(LauncherDialog *launcher)
 	_serverPortDesc = 0;
 	_serverPort = 0;
 	_serverPortClearButton = 0;
+	_featureDescriptionLine1 = 0;
+	_featureDescriptionLine2 = 0;
 	_serverWasRunning = false;
 #endif
 #endif
@@ -1915,6 +1917,9 @@ void GlobalOptionsDialog::addWiFiSharingControls(GuiObject *boss, const Common::
 	_serverPortDesc = new StaticTextWidget(boss, prefix + "ServerPortDesc", _("Server's port:"), _("Port for server to use"));
 	_serverPort = new EditTextWidget(boss, prefix + "ServerPortEditText", Common::String::format("%u", port), 0);
 	_serverPortClearButton = addClearButton(boss, prefix + "ServerPortClearButton", kServerPortClearCmd);
+
+	_featureDescriptionLine1 = new StaticTextWidget(boss, prefix + "FeatureDescriptionLine1", _c("Run server to manage files with browser (in the same Wi-Fi network).", context), "", ThemeEngine::kFontStyleNormal);
+	_featureDescriptionLine2 = new StaticTextWidget(boss, prefix + "FeatureDescriptionLine2", _c("Closing options dialog will stop the server.", context), "", ThemeEngine::kFontStyleNormal);
 	
 	reflowWiFiSharingTabLayout();
 
@@ -2626,6 +2631,17 @@ void GlobalOptionsDialog::reflowWiFiSharingTabLayout() {
 	if (_serverPortClearButton)
 		_serverPortClearButton->setVisible(false);
 #endif // NETWORKING_LOCALWEBSERVER_ENABLE_PORT_OVERRIDE
+
+	// if port override isn't supported, there will be a gap between these lines and options -- it's OK
+
+	if (_featureDescriptionLine1) {
+		_featureDescriptionLine1->setVisible(true);
+		_featureDescriptionLine1->setEnabled(false);
+	}
+	if (_featureDescriptionLine2) {
+		_featureDescriptionLine2->setVisible(true);
+		_featureDescriptionLine2->setEnabled(false);
+	}
 }
 #endif // USE_SDL_NET
 
diff --git a/gui/options.h b/gui/options.h
index fe9c93c..10e192d 100644
--- a/gui/options.h
+++ b/gui/options.h
@@ -343,6 +343,8 @@ protected:
 	StaticTextWidget *_serverPortDesc;
 	EditTextWidget   *_serverPort;
 	ButtonWidget	 *_serverPortClearButton;
+	StaticTextWidget *_featureDescriptionLine1;
+	StaticTextWidget *_featureDescriptionLine2;
 	bool _serverWasRunning;
 
 	void addWiFiSharingControls(GuiObject *boss, const Common::String &prefix, const char *context = nullptr);
diff --git a/gui/themes/scummclassic.zip b/gui/themes/scummclassic.zip
index 1e8f3ad..c06cdae 100644
Binary files a/gui/themes/scummclassic.zip and b/gui/themes/scummclassic.zip differ
diff --git a/gui/themes/scummclassic/classic_layout.stx b/gui/themes/scummclassic/classic_layout.stx
index f5fb08e..dd0ff14 100644
--- a/gui/themes/scummclassic/classic_layout.stx
+++ b/gui/themes/scummclassic/classic_layout.stx
@@ -767,6 +767,14 @@
 						width = 'Globals.Line.Height'
 				/>
 			</layout>
+			<layout type = 'vertical' padding = '0, 0, 0, 0' spacing = '4' center = 'true'>
+				<widget name = 'FeatureDescriptionLine1'
+						height = 'Globals.Line.Height'
+				/>
+				<widget name = 'FeatureDescriptionLine2'
+						height = 'Globals.Line.Height'
+				/>
+			</layout>
 		</layout>
 	</dialog>
 
diff --git a/gui/themes/scummclassic/classic_layout_lowres.stx b/gui/themes/scummclassic/classic_layout_lowres.stx
index bbeead9..8d014d9 100644
--- a/gui/themes/scummclassic/classic_layout_lowres.stx
+++ b/gui/themes/scummclassic/classic_layout_lowres.stx
@@ -774,6 +774,14 @@
 						width = 'Globals.Line.Height'
 				/>
 			</layout>
+			<layout type = 'vertical' padding = '0, 0, 0, 0' spacing = '2' center = 'true'>
+				<widget name = 'FeatureDescriptionLine1'
+						height = 'Globals.Line.Height'
+				/>
+				<widget name = 'FeatureDescriptionLine2'
+						height = 'Globals.Line.Height'
+				/>
+			</layout>
 		</layout>
 	</dialog>
 
diff --git a/gui/themes/scummmodern.zip b/gui/themes/scummmodern.zip
index d6ce4c3..794b0d7 100644
Binary files a/gui/themes/scummmodern.zip and b/gui/themes/scummmodern.zip differ
diff --git a/gui/themes/scummmodern/scummmodern_layout.stx b/gui/themes/scummmodern/scummmodern_layout.stx
index 967e5e5..d7c60b6 100644
--- a/gui/themes/scummmodern/scummmodern_layout.stx
+++ b/gui/themes/scummmodern/scummmodern_layout.stx
@@ -781,6 +781,14 @@
 						width = 'Globals.Line.Height'
 				/>
 			</layout>
+			<layout type = 'vertical' padding = '0, 0, 0, 0' spacing = '4' center = 'true'>
+				<widget name = 'FeatureDescriptionLine1'
+						height = 'Globals.Line.Height'
+				/>
+				<widget name = 'FeatureDescriptionLine2'
+						height = 'Globals.Line.Height'
+				/>
+			</layout>
 		</layout>
 	</dialog>
 
diff --git a/gui/themes/scummmodern/scummmodern_layout_lowres.stx b/gui/themes/scummmodern/scummmodern_layout_lowres.stx
index 8ff830a..558d5dc 100644
--- a/gui/themes/scummmodern/scummmodern_layout_lowres.stx
+++ b/gui/themes/scummmodern/scummmodern_layout_lowres.stx
@@ -772,6 +772,14 @@
 						width = 'Globals.Line.Height'
 				/>
 			</layout>
+			<layout type = 'vertical' padding = '0, 0, 0, 0' spacing = '2' center = 'true'>
+				<widget name = 'FeatureDescriptionLine1'
+						height = 'Globals.Line.Height'
+				/>
+				<widget name = 'FeatureDescriptionLine2'
+						height = 'Globals.Line.Height'
+				/>
+			</layout>
 		</layout>
 	</dialog>
 
diff --git a/gui/themes/scummremastered.zip b/gui/themes/scummremastered.zip
index 763fc46..5bd3719 100644
Binary files a/gui/themes/scummremastered.zip and b/gui/themes/scummremastered.zip differ
diff --git a/gui/themes/scummremastered/remastered_layout.stx b/gui/themes/scummremastered/remastered_layout.stx
index 967e5e5..d7c60b6 100644
--- a/gui/themes/scummremastered/remastered_layout.stx
+++ b/gui/themes/scummremastered/remastered_layout.stx
@@ -781,6 +781,14 @@
 						width = 'Globals.Line.Height'
 				/>
 			</layout>
+			<layout type = 'vertical' padding = '0, 0, 0, 0' spacing = '4' center = 'true'>
+				<widget name = 'FeatureDescriptionLine1'
+						height = 'Globals.Line.Height'
+				/>
+				<widget name = 'FeatureDescriptionLine2'
+						height = 'Globals.Line.Height'
+				/>
+			</layout>
 		</layout>
 	</dialog>
 
diff --git a/gui/themes/scummremastered/remastered_layout_lowres.stx b/gui/themes/scummremastered/remastered_layout_lowres.stx
index f16b905..53a1acc 100644
--- a/gui/themes/scummremastered/remastered_layout_lowres.stx
+++ b/gui/themes/scummremastered/remastered_layout_lowres.stx
@@ -772,6 +772,14 @@
 						width = 'Globals.Line.Height'
 				/>
 			</layout>
+			<layout type = 'vertical' padding = '0, 0, 0, 0' spacing = '2' center = 'true'>
+				<widget name = 'FeatureDescriptionLine1'
+						height = 'Globals.Line.Height'
+				/>
+				<widget name = 'FeatureDescriptionLine2'
+						height = 'Globals.Line.Height'
+				/>
+			</layout>
 		</layout>
 	</dialog>
 


Commit: f7902583bc7a3aac702d10488e47805742753d0a
    https://github.com/scummvm/scummvm/commit/f7902583bc7a3aac702d10488e47805742753d0a
Author: Alexander Tkachev (alexander at tkachov.ru)
Date: 2019-07-30T14:51:41-04:00

Commit Message:
GUI: Change 'Wi-Fi Sharing' tab name to 'LAN'

Changed paths:
    gui/options.cpp
    gui/options.h
    gui/themes/scummclassic.zip
    gui/themes/scummclassic/classic_layout.stx
    gui/themes/scummclassic/classic_layout_lowres.stx
    gui/themes/scummmodern.zip
    gui/themes/scummmodern/scummmodern_layout.stx
    gui/themes/scummmodern/scummmodern_layout_lowres.stx
    gui/themes/scummremastered.zip
    gui/themes/scummremastered/remastered_layout.stx
    gui/themes/scummremastered/remastered_layout_lowres.stx


diff --git a/gui/options.cpp b/gui/options.cpp
index 245b985..6be08d4 100644
--- a/gui/options.cpp
+++ b/gui/options.cpp
@@ -1763,10 +1763,10 @@ void GlobalOptionsDialog::build() {
 #endif USE_LIBCURL
 #ifdef USE_SDL_NET
 	//
-	// 8) The Wi-Fi Sharing tab (local "cloud" webserver)
+	// 8) The LAN tab (local "cloud" webserver)
 	//
-	tab->addTab(_c("Wi-Fi Sharing", context));
-	addWiFiSharingControls(tab, "GlobalOptions_WiFiSharing.", context);
+	tab->addTab(_c("LAN", context));
+	addNetworkControls(tab, "GlobalOptions_Network.", context);
 #endif // USE_SDL_NET
 #endif // USE_CLOUD
 
@@ -1903,7 +1903,7 @@ void GlobalOptionsDialog::addCloudControls(GuiObject *boss, const Common::String
 #endif // USE_LIBCURL
 
 #ifdef USE_SDL_NET
-void GlobalOptionsDialog::addWiFiSharingControls(GuiObject *boss, const Common::String &prefix, const char *context) {
+void GlobalOptionsDialog::addNetworkControls(GuiObject *boss, const Common::String &prefix, const char *context) {
 	_runServerButton = new ButtonWidget(boss, prefix + "RunServerButton", _("Run server"), _("Run local webserver"), kRunServerCmd);
 	_serverInfoLabel = new StaticTextWidget(boss, prefix + "ServerInfoLabel", _("Not running"));
 
@@ -1918,10 +1918,10 @@ void GlobalOptionsDialog::addWiFiSharingControls(GuiObject *boss, const Common::
 	_serverPort = new EditTextWidget(boss, prefix + "ServerPortEditText", Common::String::format("%u", port), 0);
 	_serverPortClearButton = addClearButton(boss, prefix + "ServerPortClearButton", kServerPortClearCmd);
 
-	_featureDescriptionLine1 = new StaticTextWidget(boss, prefix + "FeatureDescriptionLine1", _c("Run server to manage files with browser (in the same Wi-Fi network).", context), "", ThemeEngine::kFontStyleNormal);
+	_featureDescriptionLine1 = new StaticTextWidget(boss, prefix + "FeatureDescriptionLine1", _c("Run server to manage files with browser (in the same network).", context), "", ThemeEngine::kFontStyleNormal);
 	_featureDescriptionLine2 = new StaticTextWidget(boss, prefix + "FeatureDescriptionLine2", _c("Closing options dialog will stop the server.", context), "", ThemeEngine::kFontStyleNormal);
 	
-	reflowWiFiSharingTabLayout();
+	reflowNetworkTabLayout();
 
 }
 #endif // USE_SDL_NET
@@ -2403,7 +2403,7 @@ void GlobalOptionsDialog::handleTickle() {
 #ifdef USE_SDL_NET
 	if (LocalServer.isRunning() != _serverWasRunning) {
 		_serverWasRunning = !_serverWasRunning;
-		reflowWiFiSharingTabLayout();
+		reflowNetworkTabLayout();
 	}
 #endif // USE_SDL_NET
 #endif // USE_CLOUD
@@ -2450,7 +2450,7 @@ void GlobalOptionsDialog::reflowLayout() {
 	setupCloudTab();
 #endif // USE_LIBCURL
 #ifdef USE_SDL_NET
-	reflowWiFiSharingTabLayout();
+	reflowNetworkTabLayout();
 #endif // USE_SDL_NET
 #endif // USE_CLOUD
 }
@@ -2592,7 +2592,7 @@ void GlobalOptionsDialog::shiftWidget(Widget *widget, const char *widgetName, in
 #endif // USE_LIBCURL
 
 #ifdef USE_SDL_NET
-void GlobalOptionsDialog::reflowWiFiSharingTabLayout() {
+void GlobalOptionsDialog::reflowNetworkTabLayout() {
 	bool serverIsRunning = LocalServer.isRunning();
 
 	if (_runServerButton) {
diff --git a/gui/options.h b/gui/options.h
index 10e192d..a1cdcec 100644
--- a/gui/options.h
+++ b/gui/options.h
@@ -333,7 +333,7 @@ protected:
 
 #ifdef USE_SDL_NET
 	//
-	// Wi-Fi Sharing controls
+	// LAN controls
 	//
 	ButtonWidget	 *_runServerButton;
 	StaticTextWidget *_serverInfoLabel;
@@ -347,8 +347,8 @@ protected:
 	StaticTextWidget *_featureDescriptionLine2;
 	bool _serverWasRunning;
 
-	void addWiFiSharingControls(GuiObject *boss, const Common::String &prefix, const char *context = nullptr);
-	void reflowWiFiSharingTabLayout();
+	void addNetworkControls(GuiObject *boss, const Common::String &prefix, const char *context = nullptr);
+	void reflowNetworkTabLayout();
 #endif // USE_SDL_NET
 
 #endif // USE_CLOUD
diff --git a/gui/themes/scummclassic.zip b/gui/themes/scummclassic.zip
index c06cdae..15dd50a 100644
Binary files a/gui/themes/scummclassic.zip and b/gui/themes/scummclassic.zip differ
diff --git a/gui/themes/scummclassic/classic_layout.stx b/gui/themes/scummclassic/classic_layout.stx
index dd0ff14..5dbd4ca 100644
--- a/gui/themes/scummclassic/classic_layout.stx
+++ b/gui/themes/scummclassic/classic_layout.stx
@@ -732,7 +732,7 @@
 		</layout>
 	</dialog>
 
-	<dialog name = 'GlobalOptions_WiFiSharing' overlays = 'Dialog.GlobalOptions.TabWidget'>
+	<dialog name = 'GlobalOptions_Network' overlays = 'Dialog.GlobalOptions.TabWidget'>
 		<layout type = 'vertical' padding = '16, 16, 16, 16' spacing = '8'>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'>
 				<widget name = 'RunServerButton'
diff --git a/gui/themes/scummclassic/classic_layout_lowres.stx b/gui/themes/scummclassic/classic_layout_lowres.stx
index 8d014d9..d849513 100644
--- a/gui/themes/scummclassic/classic_layout_lowres.stx
+++ b/gui/themes/scummclassic/classic_layout_lowres.stx
@@ -737,7 +737,7 @@
 		</layout>
 	</dialog>
 
-	<dialog name = 'GlobalOptions_WiFiSharing' overlays = 'Dialog.GlobalOptions.TabWidget'>
+	<dialog name = 'GlobalOptions_Network' overlays = 'Dialog.GlobalOptions.TabWidget'>
 		<layout type = 'vertical' padding = '16, 16, 16, 16' spacing = '8'>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'>
 				<widget name = 'RunServerButton'
diff --git a/gui/themes/scummmodern.zip b/gui/themes/scummmodern.zip
index 794b0d7..b998e35 100644
Binary files a/gui/themes/scummmodern.zip and b/gui/themes/scummmodern.zip differ
diff --git a/gui/themes/scummmodern/scummmodern_layout.stx b/gui/themes/scummmodern/scummmodern_layout.stx
index d7c60b6..f6d5d61 100644
--- a/gui/themes/scummmodern/scummmodern_layout.stx
+++ b/gui/themes/scummmodern/scummmodern_layout.stx
@@ -746,7 +746,7 @@
 		</layout>
 	</dialog>
 
-	<dialog name = 'GlobalOptions_WiFiSharing' overlays = 'Dialog.GlobalOptions.TabWidget'>
+	<dialog name = 'GlobalOptions_Network' overlays = 'Dialog.GlobalOptions.TabWidget'>
 		<layout type = 'vertical' padding = '16, 16, 16, 16' spacing = '8'>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'>
 				<widget name = 'RunServerButton'
diff --git a/gui/themes/scummmodern/scummmodern_layout_lowres.stx b/gui/themes/scummmodern/scummmodern_layout_lowres.stx
index 558d5dc..e44aaa9 100644
--- a/gui/themes/scummmodern/scummmodern_layout_lowres.stx
+++ b/gui/themes/scummmodern/scummmodern_layout_lowres.stx
@@ -735,7 +735,7 @@
 		</layout>
 	</dialog>
 
-	<dialog name = 'GlobalOptions_WiFiSharing' overlays = 'Dialog.GlobalOptions.TabWidget'>
+	<dialog name = 'GlobalOptions_Network' overlays = 'Dialog.GlobalOptions.TabWidget'>
 		<layout type = 'vertical' padding = '16, 16, 16, 16' spacing = '8'>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'>
 				<widget name = 'RunServerButton'
diff --git a/gui/themes/scummremastered.zip b/gui/themes/scummremastered.zip
index 5bd3719..57775ab 100644
Binary files a/gui/themes/scummremastered.zip and b/gui/themes/scummremastered.zip differ
diff --git a/gui/themes/scummremastered/remastered_layout.stx b/gui/themes/scummremastered/remastered_layout.stx
index d7c60b6..f6d5d61 100644
--- a/gui/themes/scummremastered/remastered_layout.stx
+++ b/gui/themes/scummremastered/remastered_layout.stx
@@ -746,7 +746,7 @@
 		</layout>
 	</dialog>
 
-	<dialog name = 'GlobalOptions_WiFiSharing' overlays = 'Dialog.GlobalOptions.TabWidget'>
+	<dialog name = 'GlobalOptions_Network' overlays = 'Dialog.GlobalOptions.TabWidget'>
 		<layout type = 'vertical' padding = '16, 16, 16, 16' spacing = '8'>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'>
 				<widget name = 'RunServerButton'
diff --git a/gui/themes/scummremastered/remastered_layout_lowres.stx b/gui/themes/scummremastered/remastered_layout_lowres.stx
index 53a1acc..32191aa 100644
--- a/gui/themes/scummremastered/remastered_layout_lowres.stx
+++ b/gui/themes/scummremastered/remastered_layout_lowres.stx
@@ -735,7 +735,7 @@
 		</layout>
 	</dialog>
 
-	<dialog name = 'GlobalOptions_WiFiSharing' overlays = 'Dialog.GlobalOptions.TabWidget'>
+	<dialog name = 'GlobalOptions_Network' overlays = 'Dialog.GlobalOptions.TabWidget'>
 		<layout type = 'vertical' padding = '16, 16, 16, 16' spacing = '8'>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'>
 				<widget name = 'RunServerButton'


Commit: 16d97b6948326eb07e49b57a567b469493fa1916
    https://github.com/scummvm/scummvm/commit/16d97b6948326eb07e49b57a567b469493fa1916
Author: Alexander Tkachev (alexander at tkachov.ru)
Date: 2019-07-30T14:51:41-04:00

Commit Message:
CLOUD: Minor fixes for the PR#1754

- added missing 'd' in "%d" in SavesSyncRequest;
- removed trailing ',' in enum in gui/options.h;
- fixed #endif to have // before USE_LIBCURL in gui/options.h.

Changed paths:
    backends/cloud/savessyncrequest.cpp
    gui/options.cpp


diff --git a/backends/cloud/savessyncrequest.cpp b/backends/cloud/savessyncrequest.cpp
index 8d34c21..ab455c6 100644
--- a/backends/cloud/savessyncrequest.cpp
+++ b/backends/cloud/savessyncrequest.cpp
@@ -121,7 +121,7 @@ void SavesSyncRequest::directoryListedCallback(Storage::ListDirectoryResponse re
 			else if (_localFilesTimestamps[name] > file.timestamp())
 				debug(9, "- uploading file %s, because it is %d seconds newer than remote\n\tlocal = %d; \tremote = %d", name.c_str(), _localFilesTimestamps[name] - file.timestamp(), _localFilesTimestamps[name], file.timestamp());
 			else
-				debug(9, "- downloading file %s, because it is %d seconds older than remote\n\tlocal = %d; \tremote = %", name.c_str(), file.timestamp() - _localFilesTimestamps[name], _localFilesTimestamps[name], file.timestamp());
+				debug(9, "- downloading file %s, because it is %d seconds older than remote\n\tlocal = %d; \tremote = %d", name.c_str(), file.timestamp() - _localFilesTimestamps[name], _localFilesTimestamps[name], file.timestamp());
 		}
 	}
 
diff --git a/gui/options.cpp b/gui/options.cpp
index 6be08d4..7d9558a 100644
--- a/gui/options.cpp
+++ b/gui/options.cpp
@@ -114,7 +114,7 @@ enum {
 	kConnectStorageCmd = 'Cnnt',
 	kOpenUrlStorageCmd = 'OpUr',
 	kPasteCodeStorageCmd = 'PsCd',
-	kDisconnectStorageCmd = 'DcSt',
+	kDisconnectStorageCmd = 'DcSt'
 };
 #endif
 
@@ -1760,7 +1760,7 @@ void GlobalOptionsDialog::build() {
 	setTarget(container);
 
 	addCloudControls(container, "GlobalOptions_Cloud_Container.", context);
-#endif USE_LIBCURL
+#endif // USE_LIBCURL
 #ifdef USE_SDL_NET
 	//
 	// 8) The LAN tab (local "cloud" webserver)


Commit: 7fc6477ce2d1001ab5111a16d2a6b408951a0b59
    https://github.com/scummvm/scummvm/commit/7fc6477ce2d1001ab5111a16d2a6b408951a0b59
Author: Alexander Tkachev (alexander at tkachov.ru)
Date: 2019-07-30T14:51:41-04:00

Commit Message:
COMMON: Update getHumanReadableBytes() in util.h

Function now casts bytes (as <1024) to unsigned long int to correspond
"%lu" format string. For consistency, KB are now printed as floating
number. Finally, it looks like double is pretty precise to be used in
comparisons, so I made the function a little bit shorter.

Changed paths:
    common/util.cpp


diff --git a/common/util.cpp b/common/util.cpp
index 7c309ce..4b7537f 100644
--- a/common/util.cpp
+++ b/common/util.cpp
@@ -168,38 +168,31 @@ bool isGraph(int c) {
 
 
 Common::String getHumanReadableBytes(uint64 bytes, Common::String &unitsOut) {
-	Common::String result = Common::String::format("%lu", bytes);
-	unitsOut = "B";
-
-	if (bytes >= 1024) {
-		bytes /= 1024;
-		result = Common::String::format("%lu", bytes);
-		unitsOut = "KB";
+	if (bytes < 1024) {
+		unitsOut = "B";
+		return Common::String::format("%lu", (unsigned long int)bytes);
 	}
 
-	double floating = bytes;
+	double floating = bytes / 1024.0;
+	unitsOut = "KB";
 
-	if (bytes >= 1024) {
-		bytes /= 1024;
+	if (floating >= 1024) {
 		floating /= 1024.0;
 		unitsOut = "MB";
 	}
 
-	if (bytes >= 1024) {
-		bytes /= 1024;
+	if (floating >= 1024) {
 		floating /= 1024.0;
 		unitsOut = "GB";
 	}
 
-	if (bytes >= 1024) { // woah
-		bytes /= 1024;
+	if (floating >= 1024) { // woah
 		floating /= 1024.0;
 		unitsOut = "TB";
 	}
 
 	// print one digit after floating point
-	result = Common::String::format("%.1f", floating);
-	return result;
+	return Common::String::format("%.1f", floating);
 }
 
 } // End of namespace Common


Commit: e7ca2b8db02aee7f4893964f13f2f708d0a3b695
    https://github.com/scummvm/scummvm/commit/e7ca2b8db02aee7f4893964f13f2f708d0a3b695
Author: Alexander Tkachev (alexander at tkachov.ru)
Date: 2019-07-30T14:51:41-04:00

Commit Message:
CLOUD: Ignore hidden files in sync/download

In PR#1754 we've discussed and decided to ignore hidden (having a name
starting with '.') files while syncing saves or downloading game files.
This commit adds a CloudManager method to test whether file should be
ignored, and this method could be extended later if we need to ignore
some other specific file names.

Changed paths:
    backends/cloud/cloudmanager.cpp
    backends/cloud/cloudmanager.h
    backends/cloud/folderdownloadrequest.cpp
    backends/cloud/savessyncrequest.cpp


diff --git a/backends/cloud/cloudmanager.cpp b/backends/cloud/cloudmanager.cpp
index 432a63b..a3e9856 100644
--- a/backends/cloud/cloudmanager.cpp
+++ b/backends/cloud/cloudmanager.cpp
@@ -358,6 +358,13 @@ Common::String CloudManager::savesDirectoryPath() {
 	return "";
 }
 
+bool CloudManager::canSyncFilename(const Common::String &filename) const {
+	if (filename == "" || filename[0] == '.')
+		return false;
+
+	return true;
+}
+
 SavesSyncRequest *CloudManager::syncSaves(Storage::BoolCallback callback, Networking::ErrorCallback errorCallback) {
 	Storage *storage = getCurrentStorage();
 	if (storage) {
diff --git a/backends/cloud/cloudmanager.h b/backends/cloud/cloudmanager.h
index 131af9b..ebcc6ea 100644
--- a/backends/cloud/cloudmanager.h
+++ b/backends/cloud/cloudmanager.h
@@ -227,6 +227,9 @@ public:
 	/** Returns storage's saves directory path with the trailing slash. */
 	Common::String savesDirectoryPath();
 
+	/** Returns whether given filename could be uploaded to or downloaded from storage. */
+	bool canSyncFilename(const Common::String &filename) const;
+
 	/**
 	 * Starts saves syncing process in currently active storage if there is any.
 	 */
diff --git a/backends/cloud/folderdownloadrequest.cpp b/backends/cloud/folderdownloadrequest.cpp
index 3d31beb..c3b9eec 100644
--- a/backends/cloud/folderdownloadrequest.cpp
+++ b/backends/cloud/folderdownloadrequest.cpp
@@ -26,6 +26,7 @@
 #include "common/debug.h"
 #include "gui/downloaddialog.h"
 #include <backends/networking/curl/connectionmanager.h>
+#include "cloudmanager.h"
 
 namespace Cloud {
 
@@ -73,8 +74,9 @@ void FolderDownloadRequest::directoryListedCallback(Storage::ListDirectoryRespon
 
 	// remove all directories
 	// non-empty directories would be created by DumpFile, and empty ones are just ignored
+	// also skip all hidden files (with names starting with '.') or with other names that are forbidden to sync in CloudManager
 	for (Common::Array<StorageFile>::iterator i = _pendingFiles.begin(); i != _pendingFiles.end();)
-		if (i->isDirectory())
+		if (i->isDirectory() || !CloudMan.canSyncFilename(i->name()))
 			_pendingFiles.erase(i);
 		else {
 			_totalBytes += i->size();
diff --git a/backends/cloud/savessyncrequest.cpp b/backends/cloud/savessyncrequest.cpp
index ab455c6..3a58100 100644
--- a/backends/cloud/savessyncrequest.cpp
+++ b/backends/cloud/savessyncrequest.cpp
@@ -96,7 +96,7 @@ void SavesSyncRequest::directoryListedCallback(Storage::ListDirectoryResponse re
 		if (file.isDirectory())
 			continue;
 		totalSize += file.size();
-		if (file.name() == DefaultSaveFileManager::TIMESTAMPS_FILENAME)
+		if (file.name() == DefaultSaveFileManager::TIMESTAMPS_FILENAME || !CloudMan.canSyncFilename(file.name()))
 			continue;
 
 		Common::String name = file.name();
@@ -129,7 +129,7 @@ void SavesSyncRequest::directoryListedCallback(Storage::ListDirectoryResponse re
 
 	//upload files which are unavailable in cloud
 	for (Common::HashMap<Common::String, bool>::iterator i = localFileNotAvailableInCloud.begin(); i != localFileNotAvailableInCloud.end(); ++i) {
-		if (i->_key == DefaultSaveFileManager::TIMESTAMPS_FILENAME)
+		if (i->_key == DefaultSaveFileManager::TIMESTAMPS_FILENAME || !CloudMan.canSyncFilename(i->_key))
 			continue;
 		if (i->_value) {
 			_filesToUpload.push_back(i->_key);


Commit: 85431c39bf191725910703f699fe95da595f6adf
    https://github.com/scummvm/scummvm/commit/85431c39bf191725910703f699fe95da595f6adf
Author: Alexander Tkachev (alexander at tkachov.ru)
Date: 2019-07-30T14:51:41-04:00

Commit Message:
CLOUD: Change interaction with /refresh endpoint

Refresh token is now passed as custom HTTP header, not in GET parameter,
to prevent them being written into server logs.

Changed paths:
    backends/cloud/basestorage.cpp


diff --git a/backends/cloud/basestorage.cpp b/backends/cloud/basestorage.cpp
index bb19812..ea54a97 100644
--- a/backends/cloud/basestorage.cpp
+++ b/backends/cloud/basestorage.cpp
@@ -139,8 +139,9 @@ void BaseStorage::refreshAccessToken(BoolCallback callback, Networking::ErrorCal
 	if (errorCallback == nullptr)
 		errorCallback = getErrorPrintingCallback();
 
-	Common::String url = Common::String::format("https://cloud.scummvm.org/%s/refresh?code=%s", cloudProvider().c_str(), _refreshToken.c_str());
+	Common::String url = Common::String::format("https://cloud.scummvm.org/%s/refresh", cloudProvider().c_str());
 	Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(innerCallback, errorCallback, url);
+	request->addHeader("X-ScummVM-Refresh-Token: " + _refreshToken);
 	addRequest(request);
 }
 


Commit: f6a17e679f4fac2fda21dc7f64565a0dc4a10ca1
    https://github.com/scummvm/scummvm/commit/f6a17e679f4fac2fda21dc7f64565a0dc4a10ca1
Author: Alexander Tkachev (alexander at tkachov.ru)
Date: 2019-07-30T14:51:41-04:00

Commit Message:
CLOUD: Ask user to manually enable Storage

For more security, newly connected Storage only gets username/used space
information and is disabled until user manually presses the button.

Changed paths:
    backends/cloud/basestorage.cpp
    backends/cloud/basestorage.h
    backends/cloud/box/boxstorage.cpp
    backends/cloud/box/boxstorage.h
    backends/cloud/cloudmanager.cpp
    backends/cloud/cloudmanager.h
    backends/cloud/dropbox/dropboxstorage.cpp
    backends/cloud/dropbox/dropboxstorage.h
    backends/cloud/googledrive/googledrivestorage.cpp
    backends/cloud/googledrive/googledrivestorage.h
    backends/cloud/id/idstorage.cpp
    backends/cloud/id/idstorage.h
    backends/cloud/onedrive/onedrivestorage.cpp
    backends/cloud/onedrive/onedrivestorage.h
    backends/cloud/storage.cpp
    backends/cloud/storage.h
    gui/options.cpp
    gui/options.h
    gui/themes/scummclassic.zip
    gui/themes/scummclassic/classic_layout.stx
    gui/themes/scummclassic/classic_layout_lowres.stx
    gui/themes/scummmodern.zip
    gui/themes/scummmodern/scummmodern_layout.stx
    gui/themes/scummmodern/scummmodern_layout_lowres.stx
    gui/themes/scummremastered.zip
    gui/themes/scummremastered/remastered_layout.stx
    gui/themes/scummremastered/remastered_layout_lowres.stx


diff --git a/backends/cloud/basestorage.cpp b/backends/cloud/basestorage.cpp
index ea54a97..074c859 100644
--- a/backends/cloud/basestorage.cpp
+++ b/backends/cloud/basestorage.cpp
@@ -32,8 +32,10 @@ namespace Cloud {
 
 BaseStorage::BaseStorage() {}
 
-BaseStorage::BaseStorage(Common::String token, Common::String refreshToken):
-	_token(token), _refreshToken(refreshToken) {}
+BaseStorage::BaseStorage(Common::String token, Common::String refreshToken, bool enabled):
+	_token(token), _refreshToken(refreshToken) {
+	_isEnabled = enabled; 
+}
 
 BaseStorage::~BaseStorage() {}
 
@@ -212,4 +214,20 @@ void BaseStorage::tokenRefreshed(BoolCallback callback, Networking::JsonResponse
 	delete callback;
 }
 
+void BaseStorage::saveIsEnabledFlag(const Common::String &keyPrefix) const {
+	ConfMan.set(keyPrefix + "enabled", _isEnabled ? "true" : "false", ConfMan.kCloudDomain);
+}
+
+bool BaseStorage::loadIsEnabledFlag(const Common::String &keyPrefix) {
+	if (!ConfMan.hasKey(keyPrefix + "enabled", ConfMan.kCloudDomain))
+		return false;
+
+	Common::String enabled = ConfMan.get(keyPrefix + "enabled", ConfMan.kCloudDomain);
+	return (enabled == "true");
+}
+
+void BaseStorage::removeIsEnabledFlag(const Common::String &keyPrefix) {
+	ConfMan.removeKey(keyPrefix + "enabled", ConfMan.kCloudDomain);
+}
+
 } // End of namespace Cloud
diff --git a/backends/cloud/basestorage.h b/backends/cloud/basestorage.h
index aae1a6e..de287fc 100644
--- a/backends/cloud/basestorage.h
+++ b/backends/cloud/basestorage.h
@@ -77,9 +77,19 @@ protected:
 private:
 	void tokenRefreshed(BoolCallback callback, Networking::JsonResponse response);
 
+protected:
+	/** Helper function to save Storage::_isEnabled into config. */
+	void saveIsEnabledFlag(const Common::String &keyPrefix) const;
+
+	/** Helper function to load Storage::_isEnabled value from config. */
+	static bool loadIsEnabledFlag(const Common::String &keyPrefix);
+
+	/** Helper function to remove Storage::_isEnabled from config. */
+	static void removeIsEnabledFlag(const Common::String &keyPrefix);
+
 public:
 	BaseStorage();
-	BaseStorage(Common::String token, Common::String refreshToken);
+	BaseStorage(Common::String token, Common::String refreshToken, bool enabled = false);
 	virtual ~BaseStorage();
 
 	/**
diff --git a/backends/cloud/box/boxstorage.cpp b/backends/cloud/box/boxstorage.cpp
index 13046a0..46af2e5 100644
--- a/backends/cloud/box/boxstorage.cpp
+++ b/backends/cloud/box/boxstorage.cpp
@@ -42,8 +42,8 @@ namespace Box {
 #define BOX_API_FILES_CONTENT "https://api.box.com/2.0/files/%s/content"
 #define BOX_API_USERS_ME "https://api.box.com/2.0/users/me"
 
-BoxStorage::BoxStorage(Common::String token, Common::String refreshToken):
-	IdStorage(token, refreshToken) {}
+BoxStorage::BoxStorage(Common::String token, Common::String refreshToken, bool enabled):
+	IdStorage(token, refreshToken, enabled) {}
 
 BoxStorage::BoxStorage(Common::String code, Networking::ErrorCallback cb) {
 	getAccessToken(code, cb);
@@ -62,6 +62,7 @@ bool BoxStorage::canReuseRefreshToken() { return false; }
 void BoxStorage::saveConfig(Common::String keyPrefix) {
 	ConfMan.set(keyPrefix + "access_token", _token, ConfMan.kCloudDomain);
 	ConfMan.set(keyPrefix + "refresh_token", _refreshToken, ConfMan.kCloudDomain);
+	saveIsEnabledFlag(keyPrefix);
 }
 
 Common::String BoxStorage::name() const {
@@ -224,12 +225,13 @@ BoxStorage *BoxStorage::loadFromConfig(Common::String keyPrefix) {
 
 	Common::String accessToken = ConfMan.get(keyPrefix + "access_token", ConfMan.kCloudDomain);
 	Common::String refreshToken = ConfMan.get(keyPrefix + "refresh_token", ConfMan.kCloudDomain);
-	return new BoxStorage(accessToken, refreshToken);
+	return new BoxStorage(accessToken, refreshToken, loadIsEnabledFlag(keyPrefix));
 }
 
 void BoxStorage::removeFromConfig(Common::String keyPrefix) {
 	ConfMan.removeKey(keyPrefix + "access_token", ConfMan.kCloudDomain);
 	ConfMan.removeKey(keyPrefix + "refresh_token", ConfMan.kCloudDomain);
+	removeIsEnabledFlag(keyPrefix);
 }
 
 Common::String BoxStorage::getRootDirectoryId() {
diff --git a/backends/cloud/box/boxstorage.h b/backends/cloud/box/boxstorage.h
index ce77192..1916c88 100644
--- a/backends/cloud/box/boxstorage.h
+++ b/backends/cloud/box/boxstorage.h
@@ -31,7 +31,7 @@ namespace Box {
 
 class BoxStorage: public Id::IdStorage {
 	/** This private constructor is called from loadFromConfig(). */
-	BoxStorage(Common::String token, Common::String refreshToken);
+	BoxStorage(Common::String token, Common::String refreshToken, bool enabled);
 
 	/** Constructs StorageInfo based on JSON response from cloud. */
 	void infoInnerCallback(StorageInfoCallback outerCallback, Networking::JsonResponse json);
diff --git a/backends/cloud/cloudmanager.cpp b/backends/cloud/cloudmanager.cpp
index a3e9856..38089db 100644
--- a/backends/cloud/cloudmanager.cpp
+++ b/backends/cloud/cloudmanager.cpp
@@ -159,7 +159,6 @@ void CloudManager::replaceStorage(Storage *storage, uint32 index) {
 	//do what should be done on first Storage connect
 	if (_activeStorage) {
 		_activeStorage->info(nullptr, nullptr); //automatically calls setStorageUsername()
-		_activeStorage->syncSaves(nullptr, nullptr);
 	}
 }
 
@@ -365,6 +364,21 @@ bool CloudManager::canSyncFilename(const Common::String &filename) const {
 	return true;
 }
 
+bool CloudManager::isStorageEnabled() const {
+	Storage *storage = getCurrentStorage();
+	if (storage)
+		return storage->isEnabled();
+	return false;
+}
+
+void CloudManager::enableStorage() {
+	Storage *storage = getCurrentStorage();
+	if (storage) {
+		storage->enable();
+		save();
+	}
+}
+
 SavesSyncRequest *CloudManager::syncSaves(Storage::BoolCallback callback, Networking::ErrorCallback errorCallback) {
 	Storage *storage = getCurrentStorage();
 	if (storage) {
diff --git a/backends/cloud/cloudmanager.h b/backends/cloud/cloudmanager.h
index ebcc6ea..2d6c0ba 100644
--- a/backends/cloud/cloudmanager.h
+++ b/backends/cloud/cloudmanager.h
@@ -230,6 +230,12 @@ public:
 	/** Returns whether given filename could be uploaded to or downloaded from storage. */
 	bool canSyncFilename(const Common::String &filename) const;
 
+	/** Returns whether current Storage is manually enabled by user (or false, if there is no active Storage). */
+	bool isStorageEnabled() const;
+
+	/** Sets Storage::_isEnabled to true and updates the config. */
+	void enableStorage();
+
 	/**
 	 * Starts saves syncing process in currently active storage if there is any.
 	 */
diff --git a/backends/cloud/dropbox/dropboxstorage.cpp b/backends/cloud/dropbox/dropboxstorage.cpp
index 5d8b9e0..7edc609 100644
--- a/backends/cloud/dropbox/dropboxstorage.cpp
+++ b/backends/cloud/dropbox/dropboxstorage.cpp
@@ -40,7 +40,7 @@ namespace Dropbox {
 
 #define DROPBOX_API_FILES_DOWNLOAD "https://content.dropboxapi.com/2/files/download"
 
-DropboxStorage::DropboxStorage(Common::String accessToken, bool unused): BaseStorage(accessToken, "") {}
+DropboxStorage::DropboxStorage(Common::String accessToken, bool enabled): BaseStorage(accessToken, "", enabled) {}
 
 DropboxStorage::DropboxStorage(Common::String code, Networking::ErrorCallback cb): BaseStorage() {
 	getAccessToken(code, cb);
@@ -58,6 +58,7 @@ bool DropboxStorage::canReuseRefreshToken() { return false; }
 
 void DropboxStorage::saveConfig(Common::String keyPrefix) {
 	ConfMan.set(keyPrefix + "access_token", _token, ConfMan.kCloudDomain);
+	saveIsEnabledFlag(keyPrefix);
 }
 
 Common::String DropboxStorage::name() const {
@@ -108,12 +109,13 @@ DropboxStorage *DropboxStorage::loadFromConfig(Common::String keyPrefix) {
 		return nullptr;
 	}
 
-	Common::String accessToken = ConfMan.get(keyPrefix + "access_token", ConfMan.kCloudDomain);
-	return new DropboxStorage(accessToken, true);
+	Common::String accessToken = ConfMan.get(keyPrefix + "access_token", ConfMan.kCloudDomain);	
+	return new DropboxStorage(accessToken, loadIsEnabledFlag(keyPrefix));
 }
 
 void DropboxStorage::removeFromConfig(Common::String keyPrefix) {
 	ConfMan.removeKey(keyPrefix + "access_token", ConfMan.kCloudDomain);
+	removeIsEnabledFlag(keyPrefix);
 }
 
 } // End of namespace Dropbox
diff --git a/backends/cloud/dropbox/dropboxstorage.h b/backends/cloud/dropbox/dropboxstorage.h
index 0b76bb5..c6a1374 100644
--- a/backends/cloud/dropbox/dropboxstorage.h
+++ b/backends/cloud/dropbox/dropboxstorage.h
@@ -32,7 +32,7 @@ namespace Dropbox {
 
 class DropboxStorage: public Cloud::BaseStorage {
 	/** This private constructor is called from loadFromConfig(). */
-	DropboxStorage(Common::String token, bool unused);
+	DropboxStorage(Common::String token, bool enabled);
 
 protected:
 	/**
diff --git a/backends/cloud/googledrive/googledrivestorage.cpp b/backends/cloud/googledrive/googledrivestorage.cpp
index a6e17e6..2049d80 100644
--- a/backends/cloud/googledrive/googledrivestorage.cpp
+++ b/backends/cloud/googledrive/googledrivestorage.cpp
@@ -43,8 +43,8 @@ namespace GoogleDrive {
 #define GOOGLEDRIVE_API_FILES "https://www.googleapis.com/drive/v3/files"
 #define GOOGLEDRIVE_API_ABOUT "https://www.googleapis.com/drive/v3/about?fields=storageQuota,user"
 
-GoogleDriveStorage::GoogleDriveStorage(Common::String token, Common::String refreshToken):
-	IdStorage(token, refreshToken) {}
+GoogleDriveStorage::GoogleDriveStorage(Common::String token, Common::String refreshToken, bool enabled):
+	IdStorage(token, refreshToken, enabled) {}
 
 GoogleDriveStorage::GoogleDriveStorage(Common::String code, Networking::ErrorCallback cb) {
 	getAccessToken(code, cb);
@@ -63,6 +63,7 @@ bool GoogleDriveStorage::canReuseRefreshToken() { return true; }
 void GoogleDriveStorage::saveConfig(Common::String keyPrefix) {
 	ConfMan.set(keyPrefix + "access_token", _token, ConfMan.kCloudDomain);
 	ConfMan.set(keyPrefix + "refresh_token", _refreshToken, ConfMan.kCloudDomain);
+	saveIsEnabledFlag(keyPrefix);
 }
 
 Common::String GoogleDriveStorage::name() const {
@@ -228,12 +229,13 @@ GoogleDriveStorage *GoogleDriveStorage::loadFromConfig(Common::String keyPrefix)
 
 	Common::String accessToken = ConfMan.get(keyPrefix + "access_token", ConfMan.kCloudDomain);
 	Common::String refreshToken = ConfMan.get(keyPrefix + "refresh_token", ConfMan.kCloudDomain);
-	return new GoogleDriveStorage(accessToken, refreshToken);
+	return new GoogleDriveStorage(accessToken, refreshToken, loadIsEnabledFlag(keyPrefix));
 }
 
 void GoogleDriveStorage::removeFromConfig(Common::String keyPrefix) {
 	ConfMan.removeKey(keyPrefix + "access_token", ConfMan.kCloudDomain);
 	ConfMan.removeKey(keyPrefix + "refresh_token", ConfMan.kCloudDomain);
+	removeIsEnabledFlag(keyPrefix);
 }
 
 Common::String GoogleDriveStorage::getRootDirectoryId() {
diff --git a/backends/cloud/googledrive/googledrivestorage.h b/backends/cloud/googledrive/googledrivestorage.h
index db47e7c..792c30a 100644
--- a/backends/cloud/googledrive/googledrivestorage.h
+++ b/backends/cloud/googledrive/googledrivestorage.h
@@ -31,7 +31,7 @@ namespace GoogleDrive {
 
 class GoogleDriveStorage: public Id::IdStorage {
 	/** This private constructor is called from loadFromConfig(). */
-	GoogleDriveStorage(Common::String token, Common::String refreshToken);
+	GoogleDriveStorage(Common::String token, Common::String refreshToken, bool enabled);
 
 	/** Constructs StorageInfo based on JSON response from cloud. */
 	void infoInnerCallback(StorageInfoCallback outerCallback, Networking::JsonResponse json);
diff --git a/backends/cloud/id/idstorage.cpp b/backends/cloud/id/idstorage.cpp
index dd8805e..78c3fac 100644
--- a/backends/cloud/id/idstorage.cpp
+++ b/backends/cloud/id/idstorage.cpp
@@ -35,8 +35,8 @@ namespace Id {
 
 IdStorage::IdStorage() {}
 
-IdStorage::IdStorage(Common::String token, Common::String refreshToken):
-	BaseStorage(token, refreshToken) {}
+IdStorage::IdStorage(Common::String token, Common::String refreshToken, bool enabled):
+	BaseStorage(token, refreshToken, enabled) {}
 
 IdStorage::~IdStorage() {}
 
diff --git a/backends/cloud/id/idstorage.h b/backends/cloud/id/idstorage.h
index 35a3202..26d2618 100644
--- a/backends/cloud/id/idstorage.h
+++ b/backends/cloud/id/idstorage.h
@@ -53,7 +53,7 @@ protected:
 
 public:
 	IdStorage();
-	IdStorage(Common::String token, Common::String refreshToken);
+	IdStorage(Common::String token, Common::String refreshToken, bool enabled);
 	virtual ~IdStorage();
 
 	/** Public Cloud API comes down there. */
diff --git a/backends/cloud/onedrive/onedrivestorage.cpp b/backends/cloud/onedrive/onedrivestorage.cpp
index 6d05d84..e7ac5b7 100644
--- a/backends/cloud/onedrive/onedrivestorage.cpp
+++ b/backends/cloud/onedrive/onedrivestorage.cpp
@@ -42,8 +42,8 @@ namespace OneDrive {
 #define ONEDRIVE_API_SPECIAL_APPROOT_ID "https://graph.microsoft.com/v1.0/drive/special/approot:/"
 #define ONEDRIVE_API_SPECIAL_APPROOT "https://graph.microsoft.com/v1.0/drive/special/approot"
 
-OneDriveStorage::OneDriveStorage(Common::String token, Common::String refreshToken):
-	BaseStorage(token, refreshToken) {}
+OneDriveStorage::OneDriveStorage(Common::String token, Common::String refreshToken, bool enabled):
+	BaseStorage(token, refreshToken, enabled) {}
 
 OneDriveStorage::OneDriveStorage(Common::String code, Networking::ErrorCallback cb) {
 	getAccessToken(code, cb);
@@ -62,6 +62,7 @@ bool OneDriveStorage::canReuseRefreshToken() { return false; }
 void OneDriveStorage::saveConfig(Common::String keyPrefix) {
 	ConfMan.set(keyPrefix + "access_token", _token, ConfMan.kCloudDomain);
 	ConfMan.set(keyPrefix + "refresh_token", _refreshToken, ConfMan.kCloudDomain);
+	saveIsEnabledFlag(keyPrefix);
 }
 
 Common::String OneDriveStorage::name() const {
@@ -206,12 +207,13 @@ OneDriveStorage *OneDriveStorage::loadFromConfig(Common::String keyPrefix) {
 
 	Common::String accessToken = ConfMan.get(keyPrefix + "access_token", ConfMan.kCloudDomain);
 	Common::String refreshToken = ConfMan.get(keyPrefix + "refresh_token", ConfMan.kCloudDomain);
-	return new OneDriveStorage(accessToken, refreshToken);
+	return new OneDriveStorage(accessToken, refreshToken, loadIsEnabledFlag(keyPrefix));
 }
 
 void OneDriveStorage::removeFromConfig(Common::String keyPrefix) {
 	ConfMan.removeKey(keyPrefix + "access_token", ConfMan.kCloudDomain);
 	ConfMan.removeKey(keyPrefix + "refresh_token", ConfMan.kCloudDomain);
+	removeIsEnabledFlag(keyPrefix);
 }
 
 } // End of namespace OneDrive
diff --git a/backends/cloud/onedrive/onedrivestorage.h b/backends/cloud/onedrive/onedrivestorage.h
index cc46772..edf6a5f 100644
--- a/backends/cloud/onedrive/onedrivestorage.h
+++ b/backends/cloud/onedrive/onedrivestorage.h
@@ -31,7 +31,7 @@ namespace OneDrive {
 
 class OneDriveStorage: public Cloud::BaseStorage {
 	/** This private constructor is called from loadFromConfig(). */
-	OneDriveStorage(Common::String token, Common::String refreshToken);
+	OneDriveStorage(Common::String token, Common::String refreshToken, bool enabled);
 
 	/** Constructs StorageInfo based on JSON response from cloud. */
 	void infoInnerCallback(StorageInfoCallback outerCallback, Networking::JsonResponse json);
diff --git a/backends/cloud/storage.cpp b/backends/cloud/storage.cpp
index 3a9ae53..ed7f8f0 100644
--- a/backends/cloud/storage.cpp
+++ b/backends/cloud/storage.cpp
@@ -34,10 +34,18 @@ namespace Cloud {
 
 Storage::Storage():
 	_runningRequestsCount(0), _savesSyncRequest(nullptr), _syncRestartRequestsed(false),
-	_downloadFolderRequest(nullptr) {}
+	_downloadFolderRequest(nullptr), _isEnabled(false) {}
 
 Storage::~Storage() {}
 
+bool Storage::isEnabled() const {
+	return _isEnabled;
+}
+
+void Storage::enable() {
+	_isEnabled = true;
+}
+
 Networking::ErrorCallback Storage::getErrorPrintingCallback() {
 	return new Common::Callback<Storage, Networking::ErrorResponse>(this, &Storage::printErrorResponse);
 }
@@ -121,6 +129,12 @@ Networking::Request *Storage::downloadById(Common::String remoteId, Common::Stri
 }
 
 Networking::Request *Storage::downloadFolder(Common::String remotePath, Common::String localPath, FileArrayCallback callback, Networking::ErrorCallback errorCallback, bool recursive) {
+	if (!_isEnabled) {
+		warning("Storage::downloadFolder: cannot be run while Storage is disabled");
+		if (errorCallback)
+			(*errorCallback)(Networking::ErrorResponse(nullptr, false, true, "Storage is disabled.", -1));
+		return nullptr;
+	}
 	if (!errorCallback)
 		errorCallback = getErrorPrintingCallback();
 	return addRequest(new FolderDownloadRequest(this, callback, errorCallback, remotePath, localPath, recursive));
@@ -128,6 +142,13 @@ Networking::Request *Storage::downloadFolder(Common::String remotePath, Common::
 
 SavesSyncRequest *Storage::syncSaves(BoolCallback callback, Networking::ErrorCallback errorCallback) {
 	_runningRequestsMutex.lock();
+	if (!_isEnabled) {
+		warning("Storage::syncSaves: cannot be run while Storage is disabled");
+		if (errorCallback)
+			(*errorCallback)(Networking::ErrorResponse(nullptr, false, true, "Storage is disabled.", -1));
+		_runningRequestsMutex.unlock();
+		return nullptr;
+	}
 	if (_savesSyncRequest) {
 		warning("Storage::syncSaves: there is a sync in progress already");
 		_syncRestartRequestsed = true;
diff --git a/backends/cloud/storage.h b/backends/cloud/storage.h
index e914834..aa6455b 100644
--- a/backends/cloud/storage.h
+++ b/backends/cloud/storage.h
@@ -70,6 +70,9 @@ protected:
 	/** FolderDownloadRequest-related */
 	FolderDownloadRequest *_downloadFolderRequest;
 
+	/** Whether user manually enabled the Storage. */
+	bool _isEnabled;
+
 	/** Returns default error callback (printErrorResponse). */
 	virtual Networking::ErrorCallback getErrorPrintingCallback();
 
@@ -116,6 +119,16 @@ public:
 	virtual Common::String name() const = 0;
 
 	/**
+	 * Return whether Storage has been manually enabled by user.
+	 */
+	bool isEnabled() const;
+
+	/**
+	 * Set _isEnabled to true.
+	 */
+	void enable();
+
+	/**
 	 * Public Cloud API comes down there.
 	 *
 	 * All Cloud API methods return Networking::Request *, which
diff --git a/gui/options.cpp b/gui/options.cpp
index 7d9558a..6645e9d 100644
--- a/gui/options.cpp
+++ b/gui/options.cpp
@@ -114,7 +114,8 @@ enum {
 	kConnectStorageCmd = 'Cnnt',
 	kOpenUrlStorageCmd = 'OpUr',
 	kPasteCodeStorageCmd = 'PsCd',
-	kDisconnectStorageCmd = 'DcSt'
+	kDisconnectStorageCmd = 'DcSt',
+	kEnableStorageCmd = 'EnSt'
 };
 #endif
 
@@ -1481,6 +1482,8 @@ GlobalOptionsDialog::GlobalOptionsDialog(LauncherDialog *launcher)
 	_selectedStorageIndex = CloudMan.getStorageIndex();
 	_storagePopUpDesc = 0;
 	_storagePopUp = 0;
+	_storageDisabledHint = 0;
+	_storageEnableButton = 0;
 	_storageUsernameDesc = 0;
 	_storageUsername = 0;
 	_storageUsedSpaceDesc = 0;
@@ -1872,6 +1875,9 @@ void GlobalOptionsDialog::addCloudControls(GuiObject *boss, const Common::String
 		_storagePopUp->appendEntry(list[i], i);
 	_storagePopUp->setSelected(_selectedStorageIndex);
 
+	_storageDisabledHint = new StaticTextWidget(boss, prefix + "StorageDisabledHint", _c("4. Storage is yet disabled. Verify that username is correct and enable it:", context));
+	_storageEnableButton = new ButtonWidget(boss, prefix + "StorageEnableButton", _("Enable storage"), _("Confirm you want to use this account for this storage"), kEnableStorageCmd);
+
 	_storageUsernameDesc = new StaticTextWidget(boss, prefix + "StorageUsernameDesc", _("Username:"), _("Username used by this storage"));
 	_storageUsername = new StaticTextWidget(boss, prefix + "StorageUsernameLabel", "<none>", "", ThemeEngine::kFontStyleNormal);
 
@@ -2220,6 +2226,13 @@ void GlobalOptionsDialog::handleCommand(CommandSender *sender, uint32 cmd, uint3
 		reflowLayout();
 		break;
 	}
+	case kEnableStorageCmd: {
+		CloudMan.enableStorage();
+		_redrawCloudTab = true;
+
+		// also, automatically start saves sync when user enables the storage
+		// fall through
+	}
 	case kSyncSavesStorageCmd: {
 		CloudMan.syncSaves(
 			new Common::Callback<GlobalOptionsDialog, Cloud::Storage::BoolResponse>(this, &GlobalOptionsDialog::storageSavesSyncedCallback)
@@ -2467,9 +2480,28 @@ void GlobalOptionsDialog::setupCloudTab() {
 	bool storageConnected = (username != "");
 	bool shown = (_selectedStorageIndex != Cloud::kStorageNoneId);
 	bool shownConnectedInfo = (shown && storageConnected);
+	bool showingCurrentStorage = (shownConnectedInfo && _selectedStorageIndex == CloudMan.getStorageIndex());
+	bool enabled = (shownConnectedInfo && CloudMan.isStorageEnabled());
 
 	// there goes layout for connected Storage
 
+	if (_storageDisabledHint) _storageDisabledHint->setVisible(showingCurrentStorage && !enabled);
+	if (_storageEnableButton) _storageEnableButton->setVisible(showingCurrentStorage && !enabled);
+
+	// calculate shift
+	int16 x, y;
+	uint16 w, h;
+	int16 shiftUp = 0;
+	if (!showingCurrentStorage || enabled) {
+		// "storage is disabled" hint is not shown, shift everything up
+		if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.StorageDisabledHint", x, y, w, h))
+			warning("GlobalOptions_Cloud_Container.StorageUsernameDesc's position is undefined");
+		shiftUp = y;
+		if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.StorageUsernameDesc", x, y, w, h))
+			warning("GlobalOptions_Cloud_Container.StorageWizardNotConnectedHint's position is undefined");
+		shiftUp = y - shiftUp;
+	}
+
 	if (_storageUsernameDesc) _storageUsernameDesc->setVisible(shownConnectedInfo);
 	if (_storageUsername) {
 		_storageUsername->setLabel(username);
@@ -2499,37 +2531,42 @@ void GlobalOptionsDialog::setupCloudTab() {
 		_storageLastSync->setLabel(sync);
 		_storageLastSync->setVisible(shownConnectedInfo);
 	}
-	if (_storageSyncSavesButton)
-		_storageSyncSavesButton->setVisible(shownConnectedInfo && _selectedStorageIndex == CloudMan.getStorageIndex());
+	if (_storageSyncSavesButton) {
+		_storageSyncSavesButton->setVisible(showingCurrentStorage);
+		_storageSyncSavesButton->setEnabled(enabled);
+	}
 
-	int16 x, y;
-	uint16 w, h;
-	int16 downloadHintY, downloadButtonY, disconnectHintY;
-	if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.StorageDownloadHint", x, y, w, h))
-		warning("GlobalOptions_Cloud_Container.StorageDownloadHint's position is undefined");
-	downloadHintY = y;
-	if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.DownloadButton", x, y, w, h))
-		warning("GlobalOptions_Cloud_Container.DownloadButton's position is undefined");
-	downloadButtonY = y;
-	if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.StorageDisconnectHint", x, y, w, h))
-		warning("GlobalOptions_Cloud_Container.StorageDisconnectHint's position is undefined");
-	disconnectHintY = y;
-
-	bool showDownloadButton = (shownConnectedInfo && _selectedStorageIndex == CloudMan.getStorageIndex() && _selectedStorageIndex != Cloud::kStorageGoogleDriveId); // cannot download via Google Drive
+	bool showDownloadButton = (showingCurrentStorage && _selectedStorageIndex != Cloud::kStorageGoogleDriveId); // cannot download via Google Drive
 	if (_storageDownloadHint) _storageDownloadHint->setVisible(showDownloadButton);
-	if (_storageDownloadButton) _storageDownloadButton->setVisible(showDownloadButton);
+	if (_storageDownloadButton) {
+		_storageDownloadButton->setVisible(showDownloadButton);
+		_storageDownloadButton->setEnabled(enabled);
+	}
 	if (_storageDisconnectHint) _storageDisconnectHint->setVisible(shownConnectedInfo);
 	if (_storageDisconnectButton) _storageDisconnectButton->setVisible(shownConnectedInfo);
 
-	if (showDownloadButton) {
-		if (_storageDownloadHint) _storageDownloadHint->setPos(_storageDownloadHint->getRelX(), downloadHintY);
-		if (_storageDownloadButton) _storageDownloadButton->setPos(_storageDownloadButton->getRelX(), downloadButtonY);
-		if (_storageDisconnectHint) _storageDisconnectHint->setPos(_storageDisconnectHint->getRelX(), disconnectHintY);
-		if (_storageDisconnectButton)_storageDisconnectButton->setPos(_storageDisconnectButton->getRelX(), disconnectHintY + downloadButtonY - downloadHintY);
-	} else {
-		if (_storageDisconnectHint) _storageDisconnectHint->setPos(_storageDisconnectHint->getRelX(), downloadHintY);
-		if (_storageDisconnectButton)_storageDisconnectButton->setPos(_storageDisconnectButton->getRelX(), downloadButtonY);
-	}
+	int16 disconnectWidgetsAdditionalShift = 0;
+	if (!showDownloadButton) {
+		if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.StorageDownloadHint", x, y, w, h))
+			warning("GlobalOptions_Cloud_Container.StorageDownloadHint's position is undefined");
+		disconnectWidgetsAdditionalShift = y;
+		if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.StorageDisconnectHint", x, y, w, h))
+			warning("GlobalOptions_Cloud_Container.DownloadButton's position is undefined");
+		disconnectWidgetsAdditionalShift = y - disconnectWidgetsAdditionalShift;
+	}
+
+	shiftWidget(_storageUsernameDesc, "GlobalOptions_Cloud_Container.StorageUsernameDesc", 0, -shiftUp);
+	shiftWidget(_storageUsername, "GlobalOptions_Cloud_Container.StorageUsernameLabel", 0, -shiftUp);
+	shiftWidget(_storageUsedSpaceDesc, "GlobalOptions_Cloud_Container.StorageUsedSpaceDesc", 0, -shiftUp);
+	shiftWidget(_storageUsedSpace, "GlobalOptions_Cloud_Container.StorageUsedSpaceLabel", 0, -shiftUp);
+	shiftWidget(_storageSyncHint, "GlobalOptions_Cloud_Container.StorageSyncHint", 0, -shiftUp);
+	shiftWidget(_storageLastSyncDesc, "GlobalOptions_Cloud_Container.StorageLastSyncDesc", 0, -shiftUp);
+	shiftWidget(_storageLastSync, "GlobalOptions_Cloud_Container.StorageLastSyncLabel", 0, -shiftUp);
+	shiftWidget(_storageSyncSavesButton, "GlobalOptions_Cloud_Container.SyncSavesButton", 0, -shiftUp);
+	shiftWidget(_storageDownloadHint, "GlobalOptions_Cloud_Container.StorageDownloadHint", 0, -shiftUp);
+	shiftWidget(_storageDownloadButton, "GlobalOptions_Cloud_Container.DownloadButton", 0, -shiftUp);
+	shiftWidget(_storageDisconnectHint, "GlobalOptions_Cloud_Container.StorageDisconnectHint", 0, -shiftUp - disconnectWidgetsAdditionalShift);
+	shiftWidget(_storageDisconnectButton, "GlobalOptions_Cloud_Container.DisconnectButton", 0, -shiftUp - disconnectWidgetsAdditionalShift);
 
 	// there goes layout for non-connected Storage (connection wizard)
 
@@ -2560,8 +2597,7 @@ void GlobalOptionsDialog::setupCloudTab() {
 	}
 
 	if (!shownConnectedInfo) {
-		int16 shiftUp;
-		if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.StorageUsernameDesc", x, y, w, h))
+		if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.StorageDisabledHint", x, y, w, h))
 			warning("GlobalOptions_Cloud_Container.StorageUsernameDesc's position is undefined");
 		shiftUp = y;
 		if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.StorageWizardNotConnectedHint", x, y, w, h))
diff --git a/gui/options.h b/gui/options.h
index a1cdcec..10ff62d 100644
--- a/gui/options.h
+++ b/gui/options.h
@@ -298,6 +298,8 @@ protected:
 	uint32 _selectedStorageIndex;
 	StaticTextWidget *_storagePopUpDesc;
 	PopUpWidget      *_storagePopUp;
+	StaticTextWidget *_storageDisabledHint;
+	ButtonWidget	 *_storageEnableButton;
 	StaticTextWidget *_storageUsernameDesc;
 	StaticTextWidget *_storageUsername;
 	StaticTextWidget *_storageUsedSpaceDesc;
diff --git a/gui/themes/scummclassic.zip b/gui/themes/scummclassic.zip
index 15dd50a..0eba01a 100644
Binary files a/gui/themes/scummclassic.zip and b/gui/themes/scummclassic.zip differ
diff --git a/gui/themes/scummclassic/classic_layout.stx b/gui/themes/scummclassic/classic_layout.stx
index 5dbd4ca..1ed4f6b 100644
--- a/gui/themes/scummclassic/classic_layout.stx
+++ b/gui/themes/scummclassic/classic_layout.stx
@@ -625,6 +625,16 @@
 				</layout>
 			</layout>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'>
+				<layout type = 'vertical' padding = '0, 0, 8, 0' spacing = '4'>
+					<widget name = 'StorageDisabledHint'
+							height = 'Globals.Line.Height'
+					/>
+					<widget name = 'StorageEnableButton'
+							type = 'Button'
+					/>
+				</layout>
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'>
 				<layout type = 'vertical' padding = '0, 0, 6, 0' spacing = '2'>
 					<widget name = 'StorageUsernameDesc'
 							type = 'CloudTabLabel'
diff --git a/gui/themes/scummclassic/classic_layout_lowres.stx b/gui/themes/scummclassic/classic_layout_lowres.stx
index d849513..d73327d 100644
--- a/gui/themes/scummclassic/classic_layout_lowres.stx
+++ b/gui/themes/scummclassic/classic_layout_lowres.stx
@@ -630,6 +630,16 @@
 				</layout>
 			</layout>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'>
+				<layout type = 'vertical' padding = '0, 0, 3, 0' spacing = '4'>
+					<widget name = 'StorageDisabledHint'
+							height = 'Globals.Line.Height'
+					/>
+					<widget name = 'StorageEnableButton'
+							type = 'Button'
+					/>
+				</layout>
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'>
 				<layout type = 'vertical' padding = '0, 0, 3, 0' spacing = '1'>
 					<widget name = 'StorageUsernameDesc'
 							type = 'CloudTabLabel'
diff --git a/gui/themes/scummmodern.zip b/gui/themes/scummmodern.zip
index b998e35..cf59388 100644
Binary files a/gui/themes/scummmodern.zip and b/gui/themes/scummmodern.zip differ
diff --git a/gui/themes/scummmodern/scummmodern_layout.stx b/gui/themes/scummmodern/scummmodern_layout.stx
index f6d5d61..ad4f6d7 100644
--- a/gui/themes/scummmodern/scummmodern_layout.stx
+++ b/gui/themes/scummmodern/scummmodern_layout.stx
@@ -639,6 +639,16 @@
 				</layout>
 			</layout>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'>
+				<layout type = 'vertical' padding = '0, 0, 8, 0' spacing = '4'>
+					<widget name = 'StorageDisabledHint'
+							height = 'Globals.Line.Height'
+					/>
+					<widget name = 'StorageEnableButton'
+							type = 'Button'
+					/>
+				</layout>
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'>
 				<layout type = 'vertical' padding = '0, 0, 6, 0' spacing = '2'>
 					<widget name = 'StorageUsernameDesc'
 							type = 'CloudTabLabel'
diff --git a/gui/themes/scummmodern/scummmodern_layout_lowres.stx b/gui/themes/scummmodern/scummmodern_layout_lowres.stx
index e44aaa9..bb06082 100644
--- a/gui/themes/scummmodern/scummmodern_layout_lowres.stx
+++ b/gui/themes/scummmodern/scummmodern_layout_lowres.stx
@@ -628,6 +628,16 @@
 				</layout>
 			</layout>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'>
+				<layout type = 'vertical' padding = '0, 0, 3, 0' spacing = '4'>
+					<widget name = 'StorageDisabledHint'
+							height = 'Globals.Line.Height'
+					/>
+					<widget name = 'StorageEnableButton'
+							type = 'Button'
+					/>
+				</layout>
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'>
 				<layout type = 'vertical' padding = '0, 0, 3, 0' spacing = '1'>
 					<widget name = 'StorageUsernameDesc'
 							type = 'CloudTabLabel'
diff --git a/gui/themes/scummremastered.zip b/gui/themes/scummremastered.zip
index 57775ab..f9bf2e4 100644
Binary files a/gui/themes/scummremastered.zip and b/gui/themes/scummremastered.zip differ
diff --git a/gui/themes/scummremastered/remastered_layout.stx b/gui/themes/scummremastered/remastered_layout.stx
index f6d5d61..ad4f6d7 100644
--- a/gui/themes/scummremastered/remastered_layout.stx
+++ b/gui/themes/scummremastered/remastered_layout.stx
@@ -639,6 +639,16 @@
 				</layout>
 			</layout>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'>
+				<layout type = 'vertical' padding = '0, 0, 8, 0' spacing = '4'>
+					<widget name = 'StorageDisabledHint'
+							height = 'Globals.Line.Height'
+					/>
+					<widget name = 'StorageEnableButton'
+							type = 'Button'
+					/>
+				</layout>
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'>
 				<layout type = 'vertical' padding = '0, 0, 6, 0' spacing = '2'>
 					<widget name = 'StorageUsernameDesc'
 							type = 'CloudTabLabel'
diff --git a/gui/themes/scummremastered/remastered_layout_lowres.stx b/gui/themes/scummremastered/remastered_layout_lowres.stx
index 32191aa..5a265e2 100644
--- a/gui/themes/scummremastered/remastered_layout_lowres.stx
+++ b/gui/themes/scummremastered/remastered_layout_lowres.stx
@@ -628,6 +628,16 @@
 				</layout>
 			</layout>
 			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'>
+				<layout type = 'vertical' padding = '0, 0, 3, 0' spacing = '4'>
+					<widget name = 'StorageDisabledHint'
+							height = 'Globals.Line.Height'
+					/>
+					<widget name = 'StorageEnableButton'
+							type = 'Button'
+					/>
+				</layout>
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'>
 				<layout type = 'vertical' padding = '0, 0, 3, 0' spacing = '1'>
 					<widget name = 'StorageUsernameDesc'
 							type = 'CloudTabLabel'


Commit: cffd6d79abe613201db64995bf4779edc77c9858
    https://github.com/scummvm/scummvm/commit/cffd6d79abe613201db64995bf4779edc77c9858
Author: Alexander Tkachev (alexander at tkachov.ru)
Date: 2019-07-30T14:51:41-04:00

Commit Message:
GUI: Fix a label in Cloud tab

Changed paths:
    gui/options.cpp


diff --git a/gui/options.cpp b/gui/options.cpp
index 6645e9d..c6c46d2 100644
--- a/gui/options.cpp
+++ b/gui/options.cpp
@@ -1875,7 +1875,7 @@ void GlobalOptionsDialog::addCloudControls(GuiObject *boss, const Common::String
 		_storagePopUp->appendEntry(list[i], i);
 	_storagePopUp->setSelected(_selectedStorageIndex);
 
-	_storageDisabledHint = new StaticTextWidget(boss, prefix + "StorageDisabledHint", _c("4. Storage is yet disabled. Verify that username is correct and enable it:", context));
+	_storageDisabledHint = new StaticTextWidget(boss, prefix + "StorageDisabledHint", _c("4. Storage is not yet enabled. Verify that username is correct and enable it:", context));
 	_storageEnableButton = new ButtonWidget(boss, prefix + "StorageEnableButton", _("Enable storage"), _("Confirm you want to use this account for this storage"), kEnableStorageCmd);
 
 	_storageUsernameDesc = new StaticTextWidget(boss, prefix + "StorageUsernameDesc", _("Username:"), _("Username used by this storage"));





More information about the Scummvm-git-logs mailing list