@@ -234,6 +240,96 @@ bool isSyncStatusError(const OCC::SyncResult::Status status)
return false;
}
+bool confirmAccountRemoval(const QString &accountName)
+{
+ QDialog dialog(QApplication::activeWindow());
+ dialog.setObjectName(QStringLiteral("accountRemovalDialog"));
+ dialog.setWindowModality(Qt::WindowModal);
+ dialog.setWindowTitle({});
+ dialog.setFixedWidth(500);
+
+#if defined(Q_OS_MACOS) && QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)
+ dialog.setWindowFlag(Qt::ExpandedClientAreaHint, true);
+ dialog.setWindowFlag(Qt::NoTitleBarBackgroundHint, true);
+#endif
+
+ auto *mainLayout = new QVBoxLayout(&dialog);
+ mainLayout->setContentsMargins(24, 24, 24, 24);
+ mainLayout->setSpacing(14);
+
+ auto *headerLabel = new QLabel(OCC::UserModel::tr("Remove account connection?"), &dialog);
+ headerLabel->setObjectName(QStringLiteral("accountRemovalHeader"));
+ headerLabel->setWordWrap(true);
+ mainLayout->addWidget(headerLabel);
+
+ auto *messageLabel = new QLabel(&dialog);
+ messageLabel->setObjectName(QStringLiteral("accountRemovalMessage"));
+ messageLabel->setTextFormat(Qt::PlainText);
+ messageLabel->setText(
+ OCC::UserModel::tr("Do you want to remove the connection to the account %1?").arg(accountName));
+ messageLabel->setWordWrap(true);
+ mainLayout->addWidget(messageLabel);
+
+ auto *noteLabel = new QLabel(OCC::UserModel::tr("No files will be deleted."), &dialog);
+ noteLabel->setObjectName(QStringLiteral("accountRemovalNote"));
+ noteLabel->setWordWrap(true);
+ mainLayout->addWidget(noteLabel);
+
+ mainLayout->addSpacing(10);
+
+ auto *buttonLayout = new QHBoxLayout;
+ buttonLayout->setContentsMargins(0, 0, 0, 0);
+ buttonLayout->setSpacing(8);
+ buttonLayout->addStretch();
+
+ auto *cancelButton = new QPushButton(OCC::UserModel::tr("Cancel"), &dialog);
+ auto *removeButton = new QPushButton(OCC::UserModel::tr("Remove connection"), &dialog);
+ removeButton->setObjectName(QStringLiteral("accountRemovalPrimaryButton"));
+ removeButton->setDefault(true);
+
+ for (auto *button : {cancelButton, removeButton}) {
+ button->setMinimumHeight(36);
+ button->setMinimumWidth(112);
+ button->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
+ auto buttonFont = button->font();
+ buttonFont.setPointSize(16);
+ buttonFont.setWeight(QFont::Medium);
+ button->setFont(buttonFont);
+ }
+
+ buttonLayout->addWidget(cancelButton);
+ buttonLayout->addWidget(removeButton);
+ mainLayout->addLayout(buttonLayout);
+
+ QObject::connect(cancelButton, &QPushButton::clicked, &dialog, &QDialog::reject);
+ QObject::connect(removeButton, &QPushButton::clicked, &dialog, &QDialog::accept);
+
+ dialog.setStyleSheet(QStringLiteral(
+ "QDialog#accountRemovalDialog { background: white; }"
+ "QLabel#accountRemovalHeader { color: #111111; font-size: 20px; font-weight: bold; }"
+ "QLabel#accountRemovalMessage { color: #111111; font-size: 15px; }"
+ "QLabel#accountRemovalNote { color: #606060; font-size: 15px; }"
+ "QPushButton {"
+ " min-height: 36px;"
+ " max-height: 36px;"
+ " border: 1px solid #d5e0e7;"
+ " border-radius: 8px;"
+ " padding: 0px 18px;"
+ " background-color: #e7eef4;"
+ " color: #111111;"
+ " font-size: 16pt;"
+ " font-weight: 500;"
+ " }"
+ "QPushButton:hover { background-color: #e7eef4; }"
+ "QPushButton:pressed { background-color: #dce8f0; }"
+ "QPushButton#accountRemovalPrimaryButton { background-color: #2b659a; border: none; color: white; }"
+ "QPushButton#accountRemovalPrimaryButton:hover { background-color: #2b659a; color: white; }"
+ "QPushButton#accountRemovalPrimaryButton:pressed { background-color: #245783; color: white; }"
+ ));
+
+ return dialog.exec() == QDialog::Accepted;
+}
+
} // namespace
namespace OCC {
@@ -2358,17 +2454,7 @@ void UserModel::removeAccount(const int id)
return;
}
- QMessageBox messageBox(QMessageBox::Question,
- tr("Confirm Account Removal"),
- tr("Do you really want to remove the connection to the account %1?
"
- "Note: This will not delete any files.
")
- .arg(Utility::escape(_users[id]->name())),
- QMessageBox::NoButton);
- const auto * const yesButton = messageBox.addButton(tr("Remove connection"), QMessageBox::YesRole);
- messageBox.addButton(tr("Cancel"), QMessageBox::NoRole);
-
- messageBox.exec();
- if (messageBox.clickedButton() != yesButton) {
+ if (!confirmAccountRemoval(_users[id]->name())) {
return;
}
diff --git a/src/gui/wizard/accountwizardcontroller.cpp b/src/gui/wizard/accountwizardcontroller.cpp
new file mode 100644
index 0000000000000..ff157e0c3c86f
--- /dev/null
+++ b/src/gui/wizard/accountwizardcontroller.cpp
@@ -0,0 +1,1183 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "wizard/accountwizardcontroller.h"
+
+#include "account.h"
+#include "accountmanager.h"
+#include "clientproxy.h"
+#include "common/utility.h"
+#include "common/vfs.h"
+#include "configfile.h"
+#include "creds/credentialsfactory.h"
+#include "creds/webflowcredentials.h"
+#include "filesystem.h"
+#include "folder.h"
+#include "folderman.h"
+#include "guiutility.h"
+#include "networkjobs.h"
+#include "owncloudpropagator_p.h"
+#include "selectivesyncdialog.h"
+#include "theme.h"
+
+#ifdef BUILD_FILE_PROVIDER_MODULE
+#include "gui/macOS/fileprovidersettingscontroller.h"
+#endif
+
+#ifdef Q_OS_MACOS
+#include "common/utility_mac_sandbox.h"
+#endif
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+using namespace Qt::StringLiterals;
+
+namespace OCC {
+
+Q_LOGGING_CATEGORY(lcAccountWizardController, "nextcloud.gui.accountwizardcontroller", QtInfoMsg)
+
+AccountWizardController::AccountWizardController(QObject *parent)
+ : QObject(parent)
+{
+ initialiseAccount();
+
+ ConfigFile cfg;
+ const auto largeFolderLimit = cfg.newBigFolderSizeLimit();
+ _askBeforeLargeFolders = largeFolderLimit.first;
+ _largeFolderThresholdMb = static_cast(largeFolderLimit.second);
+ _askBeforeExternalStorage = cfg.confirmExternalStorage();
+
+ if (canUseVirtualFiles()) {
+ _syncMode = VirtualFiles;
+ }
+}
+
+AccountWizardController::~AccountWizardController() = default;
+
+void AccountWizardController::initialiseAccount()
+{
+ ConfigFile cfg;
+ if (!cfg.overrideServerUrl().isEmpty()) {
+ Theme::instance()->setOverrideServerUrl(cfg.overrideServerUrl());
+ Theme::instance()->setForceOverrideServerUrl(true);
+ Theme::instance()->setVfsEnabled(cfg.isVfsEnabled());
+ Theme::instance()->setStartLoginFlowAutomatically(true);
+ }
+
+ const auto defaultUrl =
+ Theme::instance()->multipleOverrideServers() ? QString{} : Theme::instance()->overrideServerUrl();
+
+ _remoteFolder = Theme::instance()->defaultServerFolder();
+ setServerUrl(defaultUrl);
+ const auto hasForcedConcreteServerUrl =
+ Theme::instance()->forceOverrideServerUrl() && !defaultUrl.isEmpty() && !Theme::instance()->multipleOverrideServers();
+ setServerUrlEditable(!hasForcedConcreteServerUrl);
+}
+
+void AccountWizardController::ensureAccount()
+{
+ if (_account) {
+ return;
+ }
+
+ _account = AccountManager::createAccount();
+ _account->setCredentials(CredentialsFactory::create("dummy"));
+}
+
+AccountWizardController::Step AccountWizardController::currentStep() const
+{
+ return _currentStep;
+}
+
+QString AccountWizardController::serverUrl() const
+{
+ return _serverUrl;
+}
+
+void AccountWizardController::setServerUrl(const QString &serverUrl)
+{
+ if (_serverUrl == serverUrl) {
+ return;
+ }
+
+ _serverUrl = serverUrl;
+ emit serverUrlChanged();
+}
+
+bool AccountWizardController::serverUrlEditable() const
+{
+ return _serverUrlEditable;
+}
+
+bool AccountWizardController::busy() const
+{
+ return _busy;
+}
+
+QString AccountWizardController::errorText() const
+{
+ return _errorText;
+}
+
+QUrl AccountWizardController::loginUrl() const
+{
+ return _loginUrl;
+}
+
+QString AccountWizardController::authStatusText() const
+{
+ return _authStatusText;
+}
+
+QString AccountWizardController::userDisplayName() const
+{
+ return _userDisplayName;
+}
+
+QString AccountWizardController::serverDisplayName() const
+{
+ return _serverDisplayName;
+}
+
+QString AccountWizardController::avatarUrl() const
+{
+ return _avatarUrl;
+}
+
+QString AccountWizardController::syncEverythingDescription() const
+{
+ return _syncEverythingDescription.isEmpty()
+ ? tr("Will require local storage")
+ : _syncEverythingDescription;
+}
+
+QString AccountWizardController::localSyncFolder() const
+{
+ return _localSyncFolder;
+}
+
+QString AccountWizardController::localSyncFolderDisplay() const
+{
+ return QDir::toNativeSeparators(_localSyncFolder);
+}
+
+QString AccountWizardController::localSyncFolderError() const
+{
+ return _localSyncFolderError;
+}
+
+bool AccountWizardController::localSyncFolderRequired() const
+{
+#ifdef BUILD_FILE_PROVIDER_MODULE
+ return _syncMode != VirtualFiles;
+#else
+ return true;
+#endif
+}
+
+AccountWizardController::SyncMode AccountWizardController::syncMode() const
+{
+ return _syncMode;
+}
+
+bool AccountWizardController::canFinish() const
+{
+ return !localSyncFolderRequired() || _localSyncFolderValid;
+}
+
+bool AccountWizardController::canUseVirtualFiles() const
+{
+#ifdef BUILD_FILE_PROVIDER_MODULE
+ return true;
+#elif defined(Q_OS_WIN)
+ return bestAvailableVfsMode() == Vfs::WindowsCfApi;
+#else
+ return bestAvailableVfsMode() != Vfs::Off && Theme::instance()->showVirtualFilesOption();
+#endif
+}
+
+bool AccountWizardController::needsSyncOptions() const
+{
+ return _needsSyncOptions;
+}
+
+bool AccountWizardController::canSkipFolderConfiguration() const
+{
+ return true;
+}
+
+bool AccountWizardController::askBeforeLargeFolders() const
+{
+ return _askBeforeLargeFolders;
+}
+
+int AccountWizardController::largeFolderThresholdMb() const
+{
+ return _largeFolderThresholdMb;
+}
+
+bool AccountWizardController::askBeforeExternalStorage() const
+{
+ return _askBeforeExternalStorage;
+}
+
+QString AccountWizardController::appName() const
+{
+ return Theme::instance()->appNameGUI();
+}
+
+QString AccountWizardController::serverUrlPlaceholder() const
+{
+ return Theme::instance()->wizardUrlHint();
+}
+
+QString AccountWizardController::normalizeServerUrlInput(const QString &serverUrl, const QString &davPath)
+{
+ auto result = serverUrl.simplified();
+ if (result.endsWith("index.php"_L1)) {
+ result.chop(9);
+ }
+
+ auto cleanedDavPath = davPath;
+ if (!cleanedDavPath.isEmpty() && result.endsWith(cleanedDavPath)) {
+ result.chop(cleanedDavPath.length());
+ }
+ if (cleanedDavPath.endsWith('/'_L1)) {
+ cleanedDavPath.chop(1);
+ if (!cleanedDavPath.isEmpty() && result.endsWith(cleanedDavPath)) {
+ result.chop(cleanedDavPath.length());
+ }
+ }
+
+ return result;
+}
+
+void AccountWizardController::submitServerUrl()
+{
+ if (_busy) {
+ return;
+ }
+
+ auto normalizedServerUrl = normalizeServerUrlInput(_serverUrl);
+ const auto url = QUrl::fromUserInput(normalizedServerUrl);
+ if (!url.isValid() || url.host().isEmpty()) {
+ setErrorText(tr("Server address does not seem to be valid"));
+ return;
+ }
+
+ ensureAccount();
+ normalizedServerUrl = normalizeServerUrlInput(_serverUrl, _account->davPath());
+ const auto accountUrl = QUrl::fromUserInput(normalizedServerUrl);
+ setServerUrl(accountUrl.toString());
+ startServerCheck(accountUrl);
+}
+
+void AccountWizardController::startServerCheck(const QUrl &serverUrl)
+{
+ _account->setUrl(serverUrl);
+ _account->setCredentials(CredentialsFactory::create("dummy"));
+ _account->setProxyType(_proxySettings._proxyType);
+
+ switch (_proxySettings._proxyType) {
+ case QNetworkProxy::HttpCachingProxy:
+ case QNetworkProxy::FtpCachingProxy:
+ case QNetworkProxy::NoProxy:
+ case QNetworkProxy::DefaultProxy:
+ _account->networkAccessManager()->setProxy({QNetworkProxy::NoProxy});
+ break;
+ case QNetworkProxy::Socks5Proxy:
+ case QNetworkProxy::HttpProxy:
+ _account->setProxyHostName(_proxySettings._host);
+ _account->setProxyPort(_proxySettings._port);
+ _account->setProxyNeedsAuth(_proxySettings._needsAuth == WizardProxySettingsDialog::ProxyAuthentication::AuthenticationRequired);
+ if (_account->proxyNeedsAuth()) {
+ _account->setProxyUser(_proxySettings._user);
+ _account->setProxyPassword(_proxySettings._password);
+ }
+ break;
+ }
+
+ _account->setSslConfiguration(QSslConfiguration::defaultConfiguration());
+ _account->networkAccessManager()->clearAccessCache();
+
+ setErrorText({});
+ setBusy(true);
+ setAuthStatusText(tr("Checking server address") + QStringLiteral("…"));
+
+ if (ClientProxy::isUsingSystemDefault() || _account->proxyType() == QNetworkProxy::DefaultProxy) {
+ ClientProxy::lookupSystemProxyAsync(_account->url(), this, SLOT(slotSystemProxyLookupDone(QNetworkProxy)));
+ } else {
+ _account->networkAccessManager()->setProxy(QNetworkProxy(QNetworkProxy::DefaultProxy));
+ QMetaObject::invokeMethod(this, "slotFindServer", Qt::QueuedConnection);
+ }
+}
+
+void AccountWizardController::slotSystemProxyLookupDone(const QNetworkProxy &proxy)
+{
+ _account->networkAccessManager()->setProxy(proxy);
+ slotFindServer();
+}
+
+void AccountWizardController::slotFindServer()
+{
+ auto *job = new CheckServerJob(_account, this);
+ job->setIgnoreCredentialFailure(true);
+ connect(job, &CheckServerJob::instanceFound, this, &AccountWizardController::slotFoundServer);
+ connect(job, &CheckServerJob::instanceNotFound, this, &AccountWizardController::slotFindServerBehindRedirect);
+ connect(job, &CheckServerJob::timeout, this, &AccountWizardController::slotNoServerFoundTimeout);
+ job->setTimeout((_account->url().scheme() == "https"_L1) ? 30 * 1000 : 10 * 1000);
+ job->start();
+}
+
+void AccountWizardController::slotFindServerBehindRedirect()
+{
+ auto redirectCheckJob = _account->sendRequest("GET", _account->url());
+ redirectCheckJob->setTimeout(qMin(2000ll, redirectCheckJob->timeoutMsec()));
+
+ auto permanentRedirects = std::make_shared(0);
+ connect(redirectCheckJob, &AbstractNetworkJob::redirected, this,
+ [permanentRedirects, account = _account](QNetworkReply *reply, const QUrl &targetUrl, int count) {
+ const auto httpCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+ if (count == *permanentRedirects && (httpCode == 301 || httpCode == 308)) {
+ qCInfo(lcAccountWizardController) << account->url() << "was redirected to" << targetUrl;
+ account->setUrl(targetUrl);
+ *permanentRedirects += 1;
+ }
+ });
+
+ connect(redirectCheckJob, &SimpleNetworkJob::finishedSignal, this, [this] {
+ auto *job = new CheckServerJob(_account, this);
+ job->setIgnoreCredentialFailure(true);
+ connect(job, &CheckServerJob::instanceFound, this, &AccountWizardController::slotFoundServer);
+ connect(job, &CheckServerJob::instanceNotFound, this, &AccountWizardController::slotNoServerFound);
+ connect(job, &CheckServerJob::timeout, this, &AccountWizardController::slotNoServerFoundTimeout);
+ job->setTimeout((_account->url().scheme() == "https"_L1) ? 30 * 1000 : 10 * 1000);
+ job->start();
+ });
+}
+
+void AccountWizardController::slotFoundServer(const QUrl &url, const QJsonObject &info)
+{
+ const auto serverVersion = CheckServerJob::version(info);
+ _account->setServerVersion(serverVersion);
+
+ if (url != _account->url()) {
+ _account->setUrl(url);
+ setServerUrl(url.toString());
+ }
+
+ setServerDisplayName(url.host());
+ setAuthStatusText(tr("Preparing browser login") + QStringLiteral("…"));
+
+ if (_account->isPublicShareLink()) {
+ setBusy(false);
+ setErrorText(tr("Public share link setup is still handled by the classic account wizard."));
+ emit legacyWizardRequested();
+ return;
+ }
+
+ slotDetermineAuthType();
+}
+
+void AccountWizardController::slotNoServerFound(QNetworkReply *reply)
+{
+ const auto job = qobject_cast(sender());
+ QString message;
+ if (!_account->url().isValid()) {
+ message = tr("Invalid URL");
+ } else {
+ message = tr("Failed to connect to %1 at %2:\n%3")
+ .arg(Theme::instance()->appNameGUI(),
+ _account->url().toString(),
+ job ? job->errorString() : QString{});
+ }
+
+ setBusy(false);
+ setErrorText(message);
+ _account->resetRejectedCertificates();
+
+ if (checkDowngradeAdvised(reply)) {
+ qCInfo(lcAccountWizardController) << "HTTPS connection failed and HTTP retry might be possible.";
+ }
+}
+
+void AccountWizardController::slotNoServerFoundTimeout(const QUrl &url)
+{
+ setBusy(false);
+ setErrorText(tr("Timeout while trying to connect to %1 at %2.")
+ .arg(Utility::escape(Theme::instance()->appNameGUI()), Utility::escape(url.toString())));
+}
+
+void AccountWizardController::slotDetermineAuthType()
+{
+ auto *job = new DetermineAuthTypeJob(_account, this);
+ connect(job, &DetermineAuthTypeJob::authType, this, [this](DetermineAuthTypeJob::AuthType type) {
+ switch (type) {
+ case DetermineAuthTypeJob::LoginFlowV2:
+ startFlow2Auth();
+ break;
+#ifdef WITH_WEBENGINE
+ case DetermineAuthTypeJob::WebViewFlow:
+ if (ConfigFile().forceLoginV2()) {
+ startFlow2Auth();
+ break;
+ }
+ [[fallthrough]];
+#endif
+ case DetermineAuthTypeJob::Basic:
+ case DetermineAuthTypeJob::NoAuthType:
+ setBusy(false);
+ setErrorText(tr("This server requires an authentication method that is still handled by the classic account wizard."));
+ emit legacyWizardRequested();
+ break;
+ }
+ });
+ job->start();
+}
+
+void AccountWizardController::startFlow2Auth()
+{
+ _account->setCredentials(CredentialsFactory::create("http"));
+
+ const auto oldAuth = _flow2Auth.release();
+ if (oldAuth) {
+ oldAuth->deleteLater();
+ }
+
+ _flow2Auth = std::make_unique(_account.data(), this);
+ connect(_flow2Auth.get(), &Flow2Auth::result, this, &AccountWizardController::slotFlow2AuthResult, Qt::QueuedConnection);
+ connect(_flow2Auth.get(), &Flow2Auth::statusChanged, this, &AccountWizardController::slotFlow2StatusChanged);
+
+ setBusy(false);
+ setErrorText({});
+ setCurrentStep(BrowserAuthStep);
+ _flow2Auth->start();
+}
+
+void AccountWizardController::openBrowserLogin()
+{
+ if (_flow2Auth) {
+ setErrorText({});
+ _flow2Auth->openBrowser();
+ }
+}
+
+void AccountWizardController::copyLoginLink()
+{
+ if (_flow2Auth) {
+ setErrorText({});
+ _flow2Auth->copyLinkToClipboard();
+ }
+}
+
+void AccountWizardController::openSignup()
+{
+ Utility::openBrowser(QUrl(QStringLiteral("https://nextcloud.com/register")));
+}
+
+void AccountWizardController::openSelfHostedServerGuide()
+{
+ Utility::openBrowser(QUrl(QStringLiteral("https://docs.nextcloud.com/server/latest/admin_manual/installation/#installation")));
+}
+
+void AccountWizardController::openProxySettings()
+{
+ const auto serverUrl = QUrl::fromUserInput(_serverUrl);
+ if (!_proxySettingsDialog) {
+ _proxySettingsDialog = new WizardProxySettingsDialog(serverUrl, _proxySettings);
+ _proxySettingsDialog->setAttribute(Qt::WA_DeleteOnClose);
+ connect(_proxySettingsDialog, &WizardProxySettingsDialog::proxySettingsAccepted, this,
+ [this](const WizardProxySettingsDialog::WizardProxySettings &proxySettings) {
+ _proxySettings = proxySettings;
+ });
+ } else {
+ _proxySettingsDialog->setServerUrl(serverUrl);
+ _proxySettingsDialog->setProxySettings(_proxySettings);
+ }
+
+ _proxySettingsDialog->open();
+}
+
+void AccountWizardController::pollNow()
+{
+ if (_flow2Auth) {
+ _flow2Auth->slotPollNow();
+ }
+}
+
+void AccountWizardController::slotFlow2AuthResult(Flow2Auth::Result result, const QString &errorString, const QString &user, const QString &appPassword)
+{
+ switch (result) {
+ case Flow2Auth::NotSupported:
+ setErrorText(tr("Unable to open the Browser, please copy the link to your Browser."));
+ break;
+ case Flow2Auth::Error:
+ setBusy(false);
+ setErrorText(errorString);
+ break;
+ case Flow2Auth::LoggedIn:
+ connectToAuthenticatedAccount(_account->url().toString(), user, appPassword);
+ break;
+ }
+}
+
+void AccountWizardController::slotFlow2StatusChanged(Flow2Auth::PollStatus status, int secondsLeft)
+{
+ if (_flow2Auth) {
+ setLoginUrl(_flow2Auth->authorisationLink());
+ }
+
+ switch (status) {
+ case Flow2Auth::statusPollCountdown:
+ setBusy(false);
+ setAuthStatusText(tr("Waiting for authorization") + QStringLiteral("… (%1)").arg(secondsLeft));
+ break;
+ case Flow2Auth::statusPollNow:
+ setBusy(true);
+ setAuthStatusText(tr("Polling for authorization") + QStringLiteral("…"));
+ break;
+ case Flow2Auth::statusFetchToken:
+ setBusy(true);
+ setAuthStatusText(tr("Starting authorization") + QStringLiteral("…"));
+ break;
+ case Flow2Auth::statusCopyLinkToClipboard:
+ setBusy(false);
+ setAuthStatusText(tr("Link copied to clipboard."));
+ break;
+ }
+}
+
+void AccountWizardController::connectToAuthenticatedAccount(const QString &url, const QString &user, const QString &appPassword)
+{
+ setBusy(true);
+ setErrorText({});
+ setAuthStatusText(tr("Checking account access") + QStringLiteral("…"));
+
+ auto *credentials = new WebFlowCredentials(user, appPassword);
+ _account->setCredentials(credentials);
+ credentials->persist();
+
+ const auto fetchUserNameJob = new JsonApiJob(_account, QStringLiteral("/ocs/v1.php/cloud/user"), this);
+ connect(fetchUserNameJob, &JsonApiJob::jsonReceived, this, [this, url](const QJsonDocument &json, int statusCode) {
+ if (statusCode != 100) {
+ qCWarning(lcAccountWizardController) << "Could not fetch username.";
+ }
+
+ sender()->deleteLater();
+
+ const auto objData = json.object().value("ocs"_L1).toObject().value("data"_L1).toObject();
+ const auto userId = objData.value("id"_L1).toString(QString());
+ const auto displayName = objData.value("display-name"_L1).toString(QString());
+ _account->setDavUser(userId);
+ _account->setDavDisplayName(displayName);
+ setUserDisplayName(displayName.isEmpty() ? userId : displayName);
+ setServerUrl(url);
+ setAvatarUrl({});
+ fetchUserAvatar();
+
+ testOwnCloudConnect();
+ });
+ fetchUserNameJob->start();
+}
+
+void AccountWizardController::testOwnCloudConnect()
+{
+ auto *job = new PropfindJob(_account, "/", this);
+ job->setIgnoreCredentialFailure(true);
+ job->setFollowRedirects(false);
+ job->setProperties(QList() << "getlastmodified");
+ connect(job, &PropfindJob::result, this, &AccountWizardController::completeAuthentication);
+ connect(job, &PropfindJob::finishedWithError, this, &AccountWizardController::slotAuthError);
+ job->start();
+}
+
+void AccountWizardController::slotAuthError(QNetworkReply *reply)
+{
+ QString errorMessage;
+
+ if (!reply) {
+ setBusy(false);
+ setErrorText(tr("There was an invalid response to an authenticated WebDAV request"));
+ return;
+ }
+
+ if (reply->error() == QNetworkReply::ContentAccessDenied) {
+ const auto davException = OCC::getExceptionFromReply(reply);
+ if (!davException.first.isEmpty() && davException.first == QByteArrayLiteral(R"(OCA\TermsOfService\TermsNotSignedException)")) {
+ setBusy(false);
+ setErrorText(tr("Terms of service acceptance is still handled by the classic account wizard."));
+ emit legacyWizardRequested();
+ return;
+ }
+ }
+
+ const auto redirectUrl = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
+ if (!redirectUrl.isEmpty()) {
+ auto adjustedRedirectUrl = redirectUrl;
+ auto path = adjustedRedirectUrl.path();
+ const auto expectedPath = u'/' + _account->davPath();
+ if (path.endsWith(expectedPath)) {
+ path.chop(expectedPath.size());
+ adjustedRedirectUrl.setPath(path);
+ _account->setUrl(adjustedRedirectUrl);
+ testOwnCloudConnect();
+ return;
+ }
+
+ errorMessage = tr("The authenticated request to the server was redirected to \"%1\". The URL is bad, the server is misconfigured.")
+ .arg(Utility::escape(redirectUrl.toString()));
+ } else if (reply->error() == QNetworkReply::ContentNotFoundError) {
+ completeAuthentication();
+ return;
+ } else if (reply->error() != QNetworkReply::NoError) {
+ const auto job = qobject_cast(sender());
+ if (!_account->credentials()->stillValid(reply)) {
+ errorMessage = tr("Access forbidden by server. To verify that you have proper access, open the service in your browser.");
+ } else if (job) {
+ errorMessage = job->errorStringParsingBody();
+ }
+ } else {
+ errorMessage = tr("There was an invalid response to an authenticated WebDAV request");
+ }
+
+ setBusy(false);
+ setCurrentStep(BrowserAuthStep);
+ setErrorText(errorMessage);
+}
+
+void AccountWizardController::completeAuthentication()
+{
+ setBusy(false);
+ setAuthStatusText(tr("Account connected."));
+ initialiseLocalSyncFolder();
+ fetchRootFolderSize();
+
+#if defined(Q_OS_WIN) || defined(Q_OS_MACOS)
+ setNeedsSyncOptions(!canUseVirtualFiles());
+#else
+ setNeedsSyncOptions(true);
+#endif
+
+ if (needsSyncOptions()) {
+ setCurrentStep(SyncOptionsStep);
+ } else {
+ finish();
+ }
+}
+
+void AccountWizardController::fetchUserAvatar()
+{
+ if (!_account || _account->davUser().isEmpty() || _account->isPublicShareLink()) {
+ return;
+ }
+
+ auto avatarSize = 80;
+ if (Theme::isHidpi()) {
+ avatarSize *= 2;
+ }
+
+ const auto avatarJob = new AvatarJob(_account, _account->davUser(), avatarSize, this);
+ avatarJob->setTimeout(20 * 1000);
+ connect(avatarJob, &AvatarJob::avatarPixmap, this, [this](const QImage &avatarImage) {
+ if (avatarImage.isNull()) {
+ return;
+ }
+
+ _account->setAvatar(avatarImage);
+
+ QByteArray pngData;
+ QBuffer pngBuffer(&pngData);
+ pngBuffer.open(QIODevice::WriteOnly);
+ AvatarJob::makeCircularAvatar(avatarImage).save(&pngBuffer, "PNG");
+ setAvatarUrl(QStringLiteral("data:image/png;base64,") + QString::fromLatin1(pngData.toBase64()));
+ });
+ avatarJob->start();
+}
+
+void AccountWizardController::fetchRootFolderSize()
+{
+ if (!_account) {
+ return;
+ }
+
+ const auto quotaJob = new PropfindJob(_account, _remoteFolder, this);
+ quotaJob->setProperties(QList() << "http://owncloud.org/ns:size");
+
+ connect(quotaJob, &PropfindJob::result, this, [this](const QVariantMap &result) {
+ bool ok = false;
+ auto size = result.value("size"_L1).toLongLong(&ok);
+ if (!ok) {
+ const auto floatingPointSize = result.value("size"_L1).toDouble(&ok);
+ size = ok ? static_cast(floatingPointSize) : -1;
+ }
+
+ if (size >= 0) {
+ setSyncEverythingDescription(tr("Will require %1 of storage").arg(Utility::octetsToString(size)));
+ }
+ });
+ connect(quotaJob, &PropfindJob::finishedWithError, this, [this](QNetworkReply *) {
+ setSyncEverythingDescription({});
+ });
+ quotaJob->start();
+}
+
+void AccountWizardController::cancel()
+{
+ emit finished(QDialog::Rejected);
+}
+
+void AccountWizardController::goBack()
+{
+ setErrorText({});
+ if (_currentStep == BrowserAuthStep || _currentStep == SyncOptionsStep) {
+ const auto oldAuth = _flow2Auth.release();
+ if (oldAuth) {
+ oldAuth->deleteLater();
+ }
+ setCurrentStep(ServerStep);
+ setBusy(false);
+ setAuthStatusText({});
+ }
+}
+
+void AccountWizardController::finish()
+{
+ if (!_account) {
+ emit finished(QDialog::Rejected);
+ return;
+ }
+
+ if (localSyncFolderRequired()) {
+ validateLocalSyncFolder();
+ if (!_localSyncFolderValid || !ensureLocalSyncFolder()) {
+ return;
+ }
+ }
+
+ if (_syncMode == SyncEverything) {
+ ConfigFile cfgFile;
+ cfgFile.setNewBigFolderSizeLimit(_askBeforeLargeFolders, _largeFolderThresholdMb);
+ cfgFile.setConfirmExternalStorage(_askBeforeExternalStorage);
+ }
+
+ const auto accountState = applyAccountChanges();
+ if (localSyncFolderRequired() && !createSyncFolder(accountState)) {
+ AccountManager::instance()->deleteAccount(accountState);
+ return;
+ }
+
+ setCurrentStep(CompletedStep);
+ emit finished(QDialog::Accepted);
+}
+
+void AccountWizardController::skipFolderConfiguration()
+{
+ if (!_account) {
+ emit finished(QDialog::Rejected);
+ return;
+ }
+
+ applyAccountChanges();
+ setCurrentStep(CompletedStep);
+ emit finished(QDialog::Accepted);
+}
+
+AccountState *AccountWizardController::applyAccountChanges()
+{
+ auto manager = AccountManager::instance();
+ AccountState *accountState = nullptr;
+
+#ifdef BUILD_FILE_PROVIDER_MODULE
+ if (_syncMode == VirtualFiles) {
+ accountState = manager->addAccount(_account);
+ const auto accountId = accountState->account()->userIdAtHostWithPort();
+ Mac::FileProviderSettingsController::instance()->setVfsEnabledForAccount(accountId, true, false);
+ } else
+#endif
+ {
+ accountState = manager->addAccount(_account);
+ }
+
+ manager->saveAccount(_account);
+ _account = AccountManager::createAccount();
+ return accountState;
+}
+
+void AccountWizardController::initialiseLocalSyncFolder()
+{
+ QString localFolder = Theme::instance()->defaultClientFolder();
+ if (!QDir(localFolder).isAbsolute()) {
+#ifdef Q_OS_MACOS
+ localFolder = Utility::getRealHomeDirectory() + QLatin1Char('/') + localFolder;
+#else
+ localFolder = QDir::homePath() + QLatin1Char('/') + localFolder;
+#endif
+ }
+
+ ConfigFile cfg;
+ const auto overrideLocalDir = !cfg.overrideLocalDir().isEmpty();
+ if (overrideLocalDir) {
+ localFolder = cfg.overrideLocalDir();
+ }
+
+ const auto strategy = overrideLocalDir
+ ? FolderMan::GoodPathStrategy::AllowOverrideExistingPath
+ : FolderMan::GoodPathStrategy::AllowOnlyNewPath;
+ setLocalSyncFolder(FolderMan::instance()->findGoodPathForNewSyncFolder(localFolder, localFolderServerUrl(), strategy), overrideLocalDir);
+}
+
+void AccountWizardController::setLocalSyncFolder(const QString &localSyncFolder, bool selectedByUser)
+{
+ const auto normalizedLocalSyncFolder = QDir::fromNativeSeparators(localSyncFolder);
+ const auto localSyncFolderSelected = _localSyncFolderSelected || selectedByUser;
+ if (_localSyncFolder == normalizedLocalSyncFolder && _localSyncFolderSelected == localSyncFolderSelected) {
+ validateLocalSyncFolder();
+ return;
+ }
+
+ _localSyncFolder = normalizedLocalSyncFolder;
+ _localSyncFolderSelected = localSyncFolderSelected;
+ emit localSyncFolderChanged();
+ validateLocalSyncFolder();
+}
+
+void AccountWizardController::validateLocalSyncFolder()
+{
+ const auto oldCanFinish = canFinish();
+ const auto pathValidity = FolderMan::instance()->checkPathValidityForNewFolder(_localSyncFolder, localFolderServerUrl());
+ auto localSyncFolderError = pathValidity.second;
+#ifdef Q_OS_MACOS
+ if (localSyncFolderRequired() && !_localSyncFolderSelected) {
+ localSyncFolderError = tr("Please choose a local sync folder.");
+ }
+#endif
+ const auto localSyncFolderValid = !localSyncFolderRequired() || localSyncFolderError.isEmpty();
+
+ if (_localSyncFolderError != localSyncFolderError) {
+ _localSyncFolderError = localSyncFolderError;
+ emit localSyncFolderErrorChanged();
+ }
+
+ if (_localSyncFolderValid != localSyncFolderValid) {
+ _localSyncFolderValid = localSyncFolderValid;
+ if (oldCanFinish != canFinish()) {
+ emit canFinishChanged();
+ }
+ }
+}
+
+bool AccountWizardController::ensureLocalSyncFolder()
+{
+ const auto localFolder = FolderDefinition::prepareLocalPath(_localSyncFolder);
+ QDir dir(localFolder);
+ if (!dir.exists()) {
+ qCInfo(lcAccountWizardController) << "Creating local sync folder" << localFolder;
+ if (!dir.mkpath(".")) {
+ setErrorText(tr("Could not create local folder %1").arg(Utility::escape(QDir::toNativeSeparators(localFolder))));
+ return false;
+ }
+ }
+
+ FileSystem::setFolderMinimumPermissions(localFolder);
+ Utility::setupFavLink(localFolder);
+ return true;
+}
+
+bool AccountWizardController::createSyncFolder(AccountState *accountState)
+{
+ if (!accountState) {
+ setErrorText(tr("Account setup failed while creating the sync folder."));
+ return false;
+ }
+
+ FolderDefinition folderDefinition;
+ folderDefinition.localPath = FolderDefinition::prepareLocalPath(_localSyncFolder);
+ folderDefinition.targetPath = FolderDefinition::prepareTargetPath(_remoteFolder);
+ folderDefinition.ignoreHiddenFiles = FolderMan::instance()->ignoreHiddenFiles();
+
+#ifndef BUILD_FILE_PROVIDER_MODULE
+ if (_syncMode == VirtualFiles) {
+ folderDefinition.virtualFilesMode = bestAvailableVfsMode();
+ }
+#endif
+
+#ifdef Q_OS_WIN
+ if (FolderMan::instance()->navigationPaneHelper().showInExplorerNavigationPane()) {
+ folderDefinition.navigationPaneClsid = QUuid::createUuid();
+ }
+#endif
+
+ auto *folderMan = FolderMan::instance();
+ folderMan->setSyncEnabled(false);
+ const auto folder = folderMan->addFolder(accountState, folderDefinition);
+ folderMan->setSyncEnabled(true);
+
+ if (!folder) {
+ setErrorText(tr("Could not create the sync folder."));
+ return false;
+ }
+
+ if (folderDefinition.virtualFilesMode != Vfs::Off && _syncMode == VirtualFiles) {
+ folder->setRootPinState(PinState::OnlineOnly);
+ }
+
+ folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, _selectiveSyncBlacklist);
+ folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncWhiteList, QStringList() << QLatin1String("/"));
+ folderMan->scheduleAllFolders();
+ return true;
+}
+
+QUrl AccountWizardController::localFolderServerUrl() const
+{
+ if (!_account) {
+ return {};
+ }
+
+ auto url = _account->url();
+ url.setUserName(_account->credentials() ? _account->credentials()->user() : QString{});
+ return url;
+}
+
+void AccountWizardController::setSyncMode(int syncMode)
+{
+ if (syncMode < SyncEverything || syncMode > VirtualFiles) {
+ return;
+ }
+
+ const auto newSyncMode = static_cast(syncMode);
+ if (_syncMode == newSyncMode) {
+ return;
+ }
+
+ if (newSyncMode == VirtualFiles && !canUseVirtualFiles()) {
+ return;
+ }
+
+ const auto oldLocalSyncFolderRequired = localSyncFolderRequired();
+ const auto oldCanFinish = canFinish();
+ _syncMode = newSyncMode;
+ emit syncModeChanged();
+
+ if (oldLocalSyncFolderRequired != localSyncFolderRequired()) {
+ emit localSyncFolderRequiredChanged();
+ }
+ validateLocalSyncFolder();
+ if (oldCanFinish != canFinish()) {
+ emit canFinishChanged();
+ }
+}
+
+void AccountWizardController::chooseLocalSyncFolder()
+{
+ QString startFolder = _localSyncFolder;
+ if (startFolder.isEmpty()) {
+#ifdef Q_OS_MACOS
+ startFolder = Utility::getRealHomeDirectory();
+#else
+ startFolder = QDir::homePath();
+#endif
+ }
+
+ const auto selectedFolder = QFileDialog::getExistingDirectory(nullptr,
+ tr("Local Sync Folder"),
+ startFolder,
+ QFileDialog::ShowDirsOnly);
+ if (selectedFolder.isEmpty()) {
+ return;
+ }
+
+ setLocalSyncFolder(selectedFolder, true);
+}
+
+void AccountWizardController::openSelectiveSync()
+{
+ if (!_account) {
+ return;
+ }
+
+ setSyncMode(SelectiveSync);
+ if (!_selectiveSyncDialog) {
+ _selectiveSyncDialog = new SelectiveSyncDialog(_account, _remoteFolder, _selectiveSyncBlacklist);
+ _selectiveSyncDialog->setAttribute(Qt::WA_DeleteOnClose);
+ connect(_selectiveSyncDialog, &SelectiveSyncDialog::finished, this, [this] {
+ if (!_selectiveSyncDialog) {
+ return;
+ }
+ if (_selectiveSyncDialog->result() == QDialog::Accepted) {
+ _selectiveSyncBlacklist = _selectiveSyncDialog->createBlackList();
+ } else if (_selectiveSyncBlacklist == QStringList("/")) {
+ _selectiveSyncBlacklist = _selectiveSyncDialog->oldBlackList();
+ }
+ });
+ }
+ _selectiveSyncDialog->open();
+}
+
+void AccountWizardController::openAdvancedOptions()
+{
+ emit advancedOptionsRequested();
+}
+
+void AccountWizardController::setAskBeforeLargeFolders(bool ask)
+{
+ if (_askBeforeLargeFolders == ask) {
+ return;
+ }
+ _askBeforeLargeFolders = ask;
+ emit askBeforeLargeFoldersChanged();
+}
+
+void AccountWizardController::setLargeFolderThresholdMb(int thresholdMb)
+{
+ const auto boundedThreshold = qMax(0, thresholdMb);
+ if (_largeFolderThresholdMb == boundedThreshold) {
+ return;
+ }
+ _largeFolderThresholdMb = boundedThreshold;
+ emit largeFolderThresholdMbChanged();
+}
+
+void AccountWizardController::setAskBeforeExternalStorage(bool ask)
+{
+ if (_askBeforeExternalStorage == ask) {
+ return;
+ }
+ _askBeforeExternalStorage = ask;
+ emit askBeforeExternalStorageChanged();
+}
+
+void AccountWizardController::setCurrentStep(Step step)
+{
+ if (_currentStep == step) {
+ return;
+ }
+ _currentStep = step;
+ emit currentStepChanged();
+}
+
+void AccountWizardController::setBusy(bool busy)
+{
+ if (_busy == busy) {
+ return;
+ }
+ _busy = busy;
+ emit busyChanged();
+}
+
+void AccountWizardController::setErrorText(const QString &errorText)
+{
+ if (_errorText == errorText) {
+ return;
+ }
+ _errorText = errorText;
+ emit errorTextChanged();
+}
+
+void AccountWizardController::setLoginUrl(const QUrl &loginUrl)
+{
+ if (_loginUrl == loginUrl) {
+ return;
+ }
+ _loginUrl = loginUrl;
+ emit loginUrlChanged();
+}
+
+void AccountWizardController::setAuthStatusText(const QString &authStatusText)
+{
+ if (_authStatusText == authStatusText) {
+ return;
+ }
+ _authStatusText = authStatusText;
+ emit authStatusTextChanged();
+}
+
+void AccountWizardController::setUserDisplayName(const QString &userDisplayName)
+{
+ if (_userDisplayName == userDisplayName) {
+ return;
+ }
+ _userDisplayName = userDisplayName;
+ emit userDisplayNameChanged();
+}
+
+void AccountWizardController::setServerDisplayName(const QString &serverDisplayName)
+{
+ if (_serverDisplayName == serverDisplayName) {
+ return;
+ }
+ _serverDisplayName = serverDisplayName;
+ emit serverDisplayNameChanged();
+}
+
+void AccountWizardController::setAvatarUrl(const QString &avatarUrl)
+{
+ if (_avatarUrl == avatarUrl) {
+ return;
+ }
+ _avatarUrl = avatarUrl;
+ emit avatarUrlChanged();
+}
+
+void AccountWizardController::setSyncEverythingDescription(const QString &syncEverythingDescription)
+{
+ if (_syncEverythingDescription == syncEverythingDescription) {
+ return;
+ }
+ _syncEverythingDescription = syncEverythingDescription;
+ emit syncEverythingDescriptionChanged();
+}
+
+void AccountWizardController::setNeedsSyncOptions(bool needsSyncOptions)
+{
+ if (_needsSyncOptions == needsSyncOptions) {
+ return;
+ }
+ _needsSyncOptions = needsSyncOptions;
+ emit needsSyncOptionsChanged();
+}
+
+void AccountWizardController::setServerUrlEditable(bool editable)
+{
+ if (_serverUrlEditable == editable) {
+ return;
+ }
+ _serverUrlEditable = editable;
+ emit serverUrlEditableChanged();
+}
+
+bool AccountWizardController::checkDowngradeAdvised(QNetworkReply *reply) const
+{
+ if (!reply || reply->url().scheme() != "https"_L1) {
+ return false;
+ }
+
+ switch (reply->error()) {
+ case QNetworkReply::NoError:
+ case QNetworkReply::ContentNotFoundError:
+ case QNetworkReply::AuthenticationRequiredError:
+ case QNetworkReply::HostNotFoundError:
+ return false;
+ default:
+ break;
+ }
+
+ return !reply->hasRawHeader("Strict-Transport-Security"_L1);
+}
+
+} // namespace OCC
diff --git a/src/gui/wizard/accountwizardcontroller.h b/src/gui/wizard/accountwizardcontroller.h
new file mode 100644
index 0000000000000..5826f2d6fea5e
--- /dev/null
+++ b/src/gui/wizard/accountwizardcontroller.h
@@ -0,0 +1,229 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#ifndef ACCOUNTWIZARDCONTROLLER_H
+#define ACCOUNTWIZARDCONTROLLER_H
+
+#include
+#include
+#include
+#include
+
+#include
+
+#include "accountfwd.h"
+#include "creds/flow2auth.h"
+#include "networkjobs.h"
+#include "wizard/wizardproxysettingsdialog.h"
+
+class QNetworkReply;
+
+namespace OCC {
+
+class SelectiveSyncDialog;
+class AccountState;
+
+/**
+ * Backend for the QML account wizard.
+ *
+ * The controller owns the account setup state and keeps credentials, network
+ * jobs and authentication details in C++. QML is intentionally limited to
+ * rendering these properties and invoking high-level actions.
+ */
+class AccountWizardController : public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(Step currentStep READ currentStep NOTIFY currentStepChanged)
+ Q_PROPERTY(QString serverUrl READ serverUrl WRITE setServerUrl NOTIFY serverUrlChanged)
+ Q_PROPERTY(bool serverUrlEditable READ serverUrlEditable NOTIFY serverUrlEditableChanged)
+ Q_PROPERTY(bool busy READ busy NOTIFY busyChanged)
+ Q_PROPERTY(QString errorText READ errorText NOTIFY errorTextChanged)
+ Q_PROPERTY(QUrl loginUrl READ loginUrl NOTIFY loginUrlChanged)
+ Q_PROPERTY(QString authStatusText READ authStatusText NOTIFY authStatusTextChanged)
+ Q_PROPERTY(QString userDisplayName READ userDisplayName NOTIFY userDisplayNameChanged)
+ Q_PROPERTY(QString serverDisplayName READ serverDisplayName NOTIFY serverDisplayNameChanged)
+ Q_PROPERTY(QString avatarUrl READ avatarUrl NOTIFY avatarUrlChanged)
+ Q_PROPERTY(QString syncEverythingDescription READ syncEverythingDescription NOTIFY syncEverythingDescriptionChanged)
+ Q_PROPERTY(QString localSyncFolder READ localSyncFolder NOTIFY localSyncFolderChanged)
+ Q_PROPERTY(QString localSyncFolderDisplay READ localSyncFolderDisplay NOTIFY localSyncFolderChanged)
+ Q_PROPERTY(QString localSyncFolderError READ localSyncFolderError NOTIFY localSyncFolderErrorChanged)
+ Q_PROPERTY(bool localSyncFolderRequired READ localSyncFolderRequired NOTIFY localSyncFolderRequiredChanged)
+ Q_PROPERTY(SyncMode syncMode READ syncMode NOTIFY syncModeChanged)
+ Q_PROPERTY(bool canFinish READ canFinish NOTIFY canFinishChanged)
+ Q_PROPERTY(bool canUseVirtualFiles READ canUseVirtualFiles CONSTANT)
+ Q_PROPERTY(bool needsSyncOptions READ needsSyncOptions NOTIFY needsSyncOptionsChanged)
+ Q_PROPERTY(bool canSkipFolderConfiguration READ canSkipFolderConfiguration CONSTANT)
+ Q_PROPERTY(bool askBeforeLargeFolders READ askBeforeLargeFolders NOTIFY askBeforeLargeFoldersChanged)
+ Q_PROPERTY(int largeFolderThresholdMb READ largeFolderThresholdMb NOTIFY largeFolderThresholdMbChanged)
+ Q_PROPERTY(bool askBeforeExternalStorage READ askBeforeExternalStorage NOTIFY askBeforeExternalStorageChanged)
+ Q_PROPERTY(QString appName READ appName CONSTANT)
+ Q_PROPERTY(QString serverUrlPlaceholder READ serverUrlPlaceholder CONSTANT)
+
+public:
+ enum Step {
+ ServerStep = 0,
+ BrowserAuthStep,
+ SyncOptionsStep,
+ CompletedStep
+ };
+ Q_ENUM(Step)
+
+ enum SyncMode {
+ SyncEverything = 0,
+ SelectiveSync,
+ VirtualFiles
+ };
+ Q_ENUM(SyncMode)
+
+ explicit AccountWizardController(QObject *parent = nullptr);
+ ~AccountWizardController() override;
+
+ [[nodiscard]] Step currentStep() const;
+ [[nodiscard]] QString serverUrl() const;
+ void setServerUrl(const QString &serverUrl);
+ [[nodiscard]] bool serverUrlEditable() const;
+ [[nodiscard]] bool busy() const;
+ [[nodiscard]] QString errorText() const;
+ [[nodiscard]] QUrl loginUrl() const;
+ [[nodiscard]] QString authStatusText() const;
+ [[nodiscard]] QString userDisplayName() const;
+ [[nodiscard]] QString serverDisplayName() const;
+ [[nodiscard]] QString avatarUrl() const;
+ [[nodiscard]] QString syncEverythingDescription() const;
+ [[nodiscard]] QString localSyncFolder() const;
+ [[nodiscard]] QString localSyncFolderDisplay() const;
+ [[nodiscard]] QString localSyncFolderError() const;
+ [[nodiscard]] bool localSyncFolderRequired() const;
+ [[nodiscard]] SyncMode syncMode() const;
+ [[nodiscard]] bool canFinish() const;
+ [[nodiscard]] bool canUseVirtualFiles() const;
+ [[nodiscard]] bool needsSyncOptions() const;
+ [[nodiscard]] bool canSkipFolderConfiguration() const;
+ [[nodiscard]] bool askBeforeLargeFolders() const;
+ [[nodiscard]] int largeFolderThresholdMb() const;
+ [[nodiscard]] bool askBeforeExternalStorage() const;
+ [[nodiscard]] QString appName() const;
+ [[nodiscard]] QString serverUrlPlaceholder() const;
+
+ [[nodiscard]] static QString normalizeServerUrlInput(const QString &serverUrl, const QString &davPath = {});
+
+ Q_INVOKABLE void submitServerUrl();
+ Q_INVOKABLE void openBrowserLogin();
+ Q_INVOKABLE void copyLoginLink();
+ Q_INVOKABLE void openSignup();
+ Q_INVOKABLE void openSelfHostedServerGuide();
+ Q_INVOKABLE void openProxySettings();
+ Q_INVOKABLE void cancel();
+ Q_INVOKABLE void goBack();
+ Q_INVOKABLE void finish();
+ Q_INVOKABLE void skipFolderConfiguration();
+ Q_INVOKABLE void setSyncMode(int syncMode);
+ Q_INVOKABLE void chooseLocalSyncFolder();
+ Q_INVOKABLE void openSelectiveSync();
+ Q_INVOKABLE void openAdvancedOptions();
+ Q_INVOKABLE void setAskBeforeLargeFolders(bool ask);
+ Q_INVOKABLE void setLargeFolderThresholdMb(int thresholdMb);
+ Q_INVOKABLE void setAskBeforeExternalStorage(bool ask);
+ Q_INVOKABLE void pollNow();
+
+signals:
+ void currentStepChanged();
+ void serverUrlChanged();
+ void serverUrlEditableChanged();
+ void busyChanged();
+ void errorTextChanged();
+ void loginUrlChanged();
+ void authStatusTextChanged();
+ void userDisplayNameChanged();
+ void serverDisplayNameChanged();
+ void avatarUrlChanged();
+ void syncEverythingDescriptionChanged();
+ void localSyncFolderChanged();
+ void localSyncFolderErrorChanged();
+ void localSyncFolderRequiredChanged();
+ void syncModeChanged();
+ void canFinishChanged();
+ void needsSyncOptionsChanged();
+ void askBeforeLargeFoldersChanged();
+ void largeFolderThresholdMbChanged();
+ void askBeforeExternalStorageChanged();
+ void finished(int result);
+ void legacyWizardRequested();
+ void advancedOptionsRequested();
+
+private slots:
+ void slotSystemProxyLookupDone(const QNetworkProxy &proxy);
+ void slotFindServer();
+ void slotFindServerBehindRedirect();
+ void slotFoundServer(const QUrl &url, const QJsonObject &info);
+ void slotNoServerFound(QNetworkReply *reply);
+ void slotNoServerFoundTimeout(const QUrl &url);
+ void slotDetermineAuthType();
+ void slotFlow2AuthResult(OCC::Flow2Auth::Result result, const QString &errorString, const QString &user, const QString &appPassword);
+ void slotFlow2StatusChanged(OCC::Flow2Auth::PollStatus status, int secondsLeft);
+ void slotAuthError(QNetworkReply *reply);
+
+private:
+ void initialiseAccount();
+ void ensureAccount();
+ void startServerCheck(const QUrl &serverUrl);
+ void startFlow2Auth();
+ void connectToAuthenticatedAccount(const QString &url, const QString &user, const QString &appPassword);
+ void testOwnCloudConnect();
+ void completeAuthentication();
+ void fetchUserAvatar();
+ void fetchRootFolderSize();
+ AccountState *applyAccountChanges();
+ void initialiseLocalSyncFolder();
+ void setLocalSyncFolder(const QString &localSyncFolder, bool selectedByUser = false);
+ void validateLocalSyncFolder();
+ [[nodiscard]] bool ensureLocalSyncFolder();
+ [[nodiscard]] bool createSyncFolder(AccountState *accountState);
+ [[nodiscard]] QUrl localFolderServerUrl() const;
+ void setCurrentStep(Step step);
+ void setBusy(bool busy);
+ void setErrorText(const QString &errorText);
+ void setLoginUrl(const QUrl &loginUrl);
+ void setAuthStatusText(const QString &authStatusText);
+ void setUserDisplayName(const QString &userDisplayName);
+ void setServerDisplayName(const QString &serverDisplayName);
+ void setAvatarUrl(const QString &avatarUrl);
+ void setSyncEverythingDescription(const QString &syncEverythingDescription);
+ void setNeedsSyncOptions(bool needsSyncOptions);
+ void setServerUrlEditable(bool editable);
+ [[nodiscard]] bool checkDowngradeAdvised(QNetworkReply *reply) const;
+
+ AccountPtr _account;
+ std::unique_ptr _flow2Auth;
+ QPointer _selectiveSyncDialog;
+ QPointer _proxySettingsDialog;
+ Step _currentStep = ServerStep;
+ QString _serverUrl;
+ bool _serverUrlEditable = true;
+ bool _busy = false;
+ QString _errorText;
+ QUrl _loginUrl;
+ QString _authStatusText;
+ QString _userDisplayName;
+ QString _serverDisplayName;
+ QString _avatarUrl;
+ QString _syncEverythingDescription;
+ QString _localSyncFolder;
+ QString _localSyncFolderError;
+ bool _localSyncFolderValid = false;
+ bool _localSyncFolderSelected = false;
+ SyncMode _syncMode = SyncEverything;
+ bool _needsSyncOptions = false;
+ bool _askBeforeLargeFolders = true;
+ int _largeFolderThresholdMb = 500;
+ bool _askBeforeExternalStorage = true;
+ QString _remoteFolder;
+ QStringList _selectiveSyncBlacklist;
+ WizardProxySettingsDialog::WizardProxySettings _proxySettings;
+};
+
+} // namespace OCC
+
+#endif // ACCOUNTWIZARDCONTROLLER_H
diff --git a/src/gui/wizard/qml/AccountWizardWindow.qml b/src/gui/wizard/qml/AccountWizardWindow.qml
new file mode 100644
index 0000000000000..1a32aff73c9b2
--- /dev/null
+++ b/src/gui/wizard/qml/AccountWizardWindow.qml
@@ -0,0 +1,250 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import QtQuick.Window
+import com.nextcloud.desktopclient
+
+ApplicationWindow {
+ id: root
+
+ property var controller
+ property bool controllerFinished: false
+
+ LayoutMirroring.enabled: Application.layoutDirection === Qt.RightToLeft
+ LayoutMirroring.childrenInherit: true
+
+ width: 500
+ height: 520
+ minimumWidth: 480
+ minimumHeight: 420
+ title: ""
+ color: "white"
+ palette.window: "white"
+ palette.windowText: "#111111"
+ palette.base: "white"
+ palette.text: "#111111"
+ palette.button: "white"
+ palette.buttonText: "#111111"
+ palette.mid: "#8a949c"
+ palette.placeholderText: Qt.rgba(0, 0, 0, 0.50)
+
+ background: Rectangle {
+ color: "white"
+ }
+
+ onClosing: function(close) {
+ if (!controllerFinished && controller) {
+ controller.cancel()
+ }
+ }
+
+ Shortcut {
+ sequence: StandardKey.Cancel
+ onActivated: {
+ if (root.controller) {
+ root.controller.cancel()
+ }
+ }
+ }
+
+ Connections {
+ target: controller
+
+ function onFinished() {
+ root.controllerFinished = true
+ root.close()
+ }
+
+ function onAdvancedOptionsRequested() {
+ advancedOptionsDialog.open()
+ }
+ }
+
+ AdvancedOptionsDialog {
+ id: advancedOptionsDialog
+ x: Math.round((root.width - width) / 2)
+ y: Math.round((root.height - height) / 2)
+ controller: root.controller
+ }
+
+ WizardDialogFrame {
+ anchors.fill: parent
+
+ Loader {
+ anchors.fill: parent
+ sourceComponent: {
+ if (!root.controller) {
+ return null
+ }
+ switch (root.controller.currentStep) {
+ case AccountWizardController.BrowserAuthStep:
+ return browserAuthPage
+ case AccountWizardController.SyncOptionsStep:
+ return syncOptionsPage
+ default:
+ return serverPage
+ }
+ }
+ }
+
+ footer: [
+ WizardButton {
+ visible: root.controller && root.controller.currentStep !== AccountWizardController.ServerStep
+ enabled: root.controller && !root.controller.busy
+ Layout.fillWidth: root.controller
+ && (root.controller.currentStep === AccountWizardController.BrowserAuthStep
+ || root.controller.currentStep === AccountWizardController.SyncOptionsStep)
+ Layout.preferredWidth: root.controller
+ && (root.controller.currentStep === AccountWizardController.BrowserAuthStep
+ || root.controller.currentStep === AccountWizardController.SyncOptionsStep)
+ ? 1
+ : implicitWidth
+ text: root.controller && root.controller.currentStep === AccountWizardController.BrowserAuthStep
+ ? qsTr("Cancel")
+ : root.controller && root.controller.currentStep === AccountWizardController.SyncOptionsStep
+ ? qsTr("Cancel")
+ : qsTr("Back")
+ onClicked: {
+ if (root.controller.currentStep === AccountWizardController.BrowserAuthStep
+ || root.controller.currentStep === AccountWizardController.SyncOptionsStep) {
+ root.controller.cancel()
+ } else {
+ root.controller.goBack()
+ }
+ }
+ },
+
+ WizardButton {
+ visible: root.controller && root.controller.currentStep === AccountWizardController.SyncOptionsStep
+ enabled: root.controller && !root.controller.busy
+ text: qsTr("Set up later")
+ Layout.fillWidth: true
+ Layout.preferredWidth: 1
+ onClicked: root.controller.skipFolderConfiguration()
+ },
+
+ WizardButton {
+ visible: root.controller && root.controller.currentStep === AccountWizardController.ServerStep
+ enabled: root.controller && !root.controller.busy
+ text: qsTr("Sign up")
+ textSuffix: "\u2197"
+ Layout.fillWidth: true
+ Layout.preferredWidth: 1
+ onClicked: root.controller.openSignup()
+ },
+
+ WizardButton {
+ visible: root.controller && root.controller.currentStep === AccountWizardController.ServerStep
+ enabled: root.controller && !root.controller.busy
+ text: qsTr("Self-host")
+ textSuffix: "\u2197"
+ Layout.fillWidth: true
+ Layout.preferredWidth: 1
+ onClicked: root.controller.openSelfHostedServerGuide()
+ },
+
+ Button {
+ visible: root.controller && root.controller.currentStep === AccountWizardController.ServerStep
+ enabled: root.controller && !root.controller.busy
+ flat: true
+ text: qsTr("Proxy settings")
+ font.pointSize: 16
+ font.weight: Font.Medium
+ Layout.fillWidth: true
+ Layout.preferredWidth: 1
+ Layout.preferredHeight: 36
+ onClicked: root.controller.openProxySettings()
+ },
+
+ Item {
+ visible: root.controller
+ && root.controller.currentStep !== AccountWizardController.ServerStep
+ && root.controller.currentStep !== AccountWizardController.BrowserAuthStep
+ && root.controller.currentStep !== AccountWizardController.SyncOptionsStep
+ Layout.fillWidth: visible
+ },
+
+ WizardButton {
+ visible: root.controller && root.controller.currentStep === AccountWizardController.BrowserAuthStep
+ enabled: root.controller && !root.controller.busy && root.controller.loginUrl.toString() !== ""
+ text: qsTr("Copy link")
+ iconSource: "image://svgimage-custom-color/copy.svg/" + palette.buttonText
+ iconBeforeText: true
+ Layout.fillWidth: true
+ Layout.preferredWidth: 1
+ onClicked: root.controller.copyLoginLink()
+ },
+
+ WizardButton {
+ visible: root.controller && root.controller.currentStep !== AccountWizardController.ServerStep
+ primary: true
+ enabled: root.controller
+ && !root.controller.busy
+ && (root.controller.currentStep !== AccountWizardController.SyncOptionsStep
+ || root.controller.canFinish)
+ Layout.fillWidth: root.controller
+ && (root.controller.currentStep === AccountWizardController.BrowserAuthStep
+ || root.controller.currentStep === AccountWizardController.SyncOptionsStep)
+ Layout.preferredWidth: root.controller
+ && (root.controller.currentStep === AccountWizardController.BrowserAuthStep
+ || root.controller.currentStep === AccountWizardController.SyncOptionsStep)
+ ? 1
+ : implicitWidth
+ text: {
+ if (!root.controller) {
+ return ""
+ }
+ switch (root.controller.currentStep) {
+ case AccountWizardController.BrowserAuthStep:
+ return qsTr("Open")
+ case AccountWizardController.SyncOptionsStep:
+ return qsTr("Done")
+ default:
+ return qsTr("Log in")
+ }
+ }
+ textSuffix: root.controller && root.controller.currentStep === AccountWizardController.BrowserAuthStep
+ ? "\u2197"
+ : ""
+ onClicked: {
+ switch (root.controller.currentStep) {
+ case AccountWizardController.BrowserAuthStep:
+ root.controller.openBrowserLogin()
+ break
+ case AccountWizardController.SyncOptionsStep:
+ root.controller.finish()
+ break
+ default:
+ root.controller.submitServerUrl()
+ }
+ }
+ }
+ ]
+ }
+
+ Component {
+ id: serverPage
+ ServerPage {
+ controller: root.controller
+ }
+ }
+
+ Component {
+ id: browserAuthPage
+ BrowserAuthPage {
+ controller: root.controller
+ }
+ }
+
+ Component {
+ id: syncOptionsPage
+ SyncOptionsPage {
+ controller: root.controller
+ }
+ }
+}
diff --git a/src/gui/wizard/qml/AdvancedOptionsDialog.qml b/src/gui/wizard/qml/AdvancedOptionsDialog.qml
new file mode 100644
index 0000000000000..c287eec6d9501
--- /dev/null
+++ b/src/gui/wizard/qml/AdvancedOptionsDialog.qml
@@ -0,0 +1,84 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Style
+import "../../tray"
+
+Dialog {
+ id: root
+
+ required property var controller
+ readonly property color primaryTextColor: "#111111"
+
+ modal: true
+ width: 360
+ padding: 24
+ header: null
+ footer: null
+
+ background: Rectangle {
+ radius: 12
+ color: "white"
+ }
+
+ contentItem: ColumnLayout {
+ spacing: 14
+
+ EnforcedPlainTextLabel {
+ text: qsTr("Advanced options")
+ color: root.primaryTextColor
+ font.pixelSize: Style.pixelSize + 8
+ font.bold: true
+ Layout.fillWidth: true
+ }
+
+ CheckBox {
+ text: qsTr("Ask before syncing folders larger than")
+ checked: root.controller.askBeforeLargeFolders
+ font.pixelSize: Style.pixelSize + 2
+ onToggled: root.controller.setAskBeforeLargeFolders(checked)
+ Layout.fillWidth: true
+ }
+
+ SpinBox {
+ from: 0
+ to: 1048576
+ value: root.controller.largeFolderThresholdMb
+ enabled: root.controller.askBeforeLargeFolders
+ editable: true
+ font.pixelSize: Style.pixelSize + 2
+ onValueModified: root.controller.setLargeFolderThresholdMb(value)
+ textFromValue: function(value) { return qsTr("%1 MB").arg(value) }
+ valueFromText: function(text) { return parseInt(text) }
+ Layout.fillWidth: true
+ }
+
+ CheckBox {
+ text: qsTr("Ask before syncing external storage")
+ checked: root.controller.askBeforeExternalStorage
+ font.pixelSize: Style.pixelSize + 2
+ onToggled: root.controller.setAskBeforeExternalStorage(checked)
+ Layout.fillWidth: true
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ Layout.topMargin: 8
+
+ Item {
+ Layout.fillWidth: true
+ }
+
+ WizardButton {
+ primary: true
+ text: qsTr("Done")
+ onClicked: root.close()
+ }
+ }
+ }
+}
diff --git a/src/gui/wizard/qml/BrowserAuthPage.qml b/src/gui/wizard/qml/BrowserAuthPage.qml
new file mode 100644
index 0000000000000..2e24b85dd98d0
--- /dev/null
+++ b/src/gui/wizard/qml/BrowserAuthPage.qml
@@ -0,0 +1,83 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Style
+import "../../tray"
+
+Item {
+ id: root
+
+ required property var controller
+ readonly property color primaryTextColor: "#111111"
+ readonly property color primaryButtonColor: "#2B659A"
+
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.margins: 24
+ spacing: 14
+
+ Item {
+ Layout.fillHeight: true
+ }
+
+ Image {
+ Layout.alignment: Qt.AlignHCenter
+ source: "image://svgimage-custom-color/globe.svg/" + root.primaryButtonColor
+ sourceSize.width: 72
+ sourceSize.height: 72
+ Layout.preferredWidth: 72
+ Layout.preferredHeight: 72
+ fillMode: Image.PreserveAspectFit
+ }
+
+ EnforcedPlainTextLabel {
+ text: qsTr("Switch to your browser")
+ color: root.primaryTextColor
+ font.pixelSize: Style.pixelSize + 8
+ font.bold: true
+ horizontalAlignment: Text.AlignHCenter
+ Layout.fillWidth: true
+ wrapMode: Text.WordWrap
+ }
+
+ RowLayout {
+ visible: root.controller.busy && root.controller.authStatusText !== ""
+ Layout.fillWidth: true
+ spacing: 8
+
+ Item {
+ Layout.fillWidth: true
+ }
+
+ BusyIndicator {
+ running: root.controller.busy
+ visible: running
+ Layout.preferredWidth: 20
+ Layout.preferredHeight: 20
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+ }
+
+ EnforcedPlainTextLabel {
+ visible: root.controller.errorText !== ""
+ text: root.controller.errorText
+ color: Style.errorBoxBackgroundColor
+ font.pixelSize: Style.pixelSize + 1
+ horizontalAlignment: Text.AlignHCenter
+ Layout.fillWidth: true
+ wrapMode: Text.WordWrap
+ }
+
+ Item {
+ Layout.fillHeight: true
+ }
+ }
+}
diff --git a/src/gui/wizard/qml/OptionRow.qml b/src/gui/wizard/qml/OptionRow.qml
new file mode 100644
index 0000000000000..f728f6b60faae
--- /dev/null
+++ b/src/gui/wizard/qml/OptionRow.qml
@@ -0,0 +1,90 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Style
+import "../../tray"
+
+Control {
+ id: root
+
+ property alias title: titleLabel.text
+ property alias description: descriptionLabel.text
+ property string iconSource: ""
+ property bool selected: false
+ readonly property color primaryTextColor: "#111111"
+ readonly property color hintTextColor: Qt.rgba(0, 0, 0, 0.62)
+
+ signal clicked()
+
+ implicitHeight: descriptionLabel.text === "" ? 42 : 56
+ padding: 10
+
+ background: Rectangle {
+ radius: 6
+ border.width: 1
+ border.color: root.selected ? "#d8e7f1" : "#e1e8ee"
+ color: root.selected
+ ? "#eef5fb"
+ : "#f5f8fa"
+ }
+
+ contentItem: RowLayout {
+ spacing: 10
+
+ Rectangle {
+ Layout.preferredWidth: 18
+ Layout.preferredHeight: 18
+ Layout.alignment: Qt.AlignVCenter
+ radius: width / 2
+ border.width: 2
+ border.color: root.selected ? "#0076b5" : "#0076b5"
+ color: "transparent"
+
+ Rectangle {
+ anchors.centerIn: parent
+ width: 8
+ height: 8
+ radius: width / 2
+ color: "#0076b5"
+ visible: root.selected
+ }
+ }
+
+ ColumnLayout {
+ spacing: 0
+ Layout.fillWidth: true
+ Layout.alignment: Qt.AlignVCenter
+
+ EnforcedPlainTextLabel {
+ id: titleLabel
+ Layout.fillWidth: true
+ color: root.primaryTextColor
+ font.bold: true
+ font.pixelSize: Style.pixelSize + 1
+ elide: Text.ElideRight
+ }
+
+ EnforcedPlainTextLabel {
+ id: descriptionLabel
+ visible: text !== ""
+ Layout.fillWidth: true
+ color: root.hintTextColor
+ font.pixelSize: Style.pixelSize
+ wrapMode: Text.WordWrap
+ maximumLineCount: 2
+ }
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.clicked()
+ }
+}
diff --git a/src/gui/wizard/qml/ServerPage.qml b/src/gui/wizard/qml/ServerPage.qml
new file mode 100644
index 0000000000000..8512f366233de
--- /dev/null
+++ b/src/gui/wizard/qml/ServerPage.qml
@@ -0,0 +1,128 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import com.nextcloud.desktopclient
+import Style
+import "../../tray"
+
+Item {
+ id: root
+
+ required property var controller
+ readonly property color primaryTextColor: "#111111"
+ readonly property color hintTextColor: Qt.rgba(0, 0, 0, 0.62)
+
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.leftMargin: 24
+ anchors.rightMargin: 24
+ anchors.topMargin: 24
+ anchors.bottomMargin: 24
+ spacing: 4
+
+ EnforcedPlainTextLabel {
+ text: qsTr("Log in to %1").arg(root.controller.appName)
+ color: root.primaryTextColor
+ font.pixelSize: Style.pixelSize + 8
+ font.bold: true
+ Layout.fillWidth: true
+ wrapMode: Text.WordWrap
+ }
+
+ EnforcedPlainTextLabel {
+ text: qsTr("Enter the link to your %1 web interface from the browser or the link to a folder shared with you.").arg(root.controller.appName)
+ color: root.hintTextColor
+ font.pixelSize: Style.pixelSize + 2
+ Layout.fillWidth: true
+ wrapMode: Text.WordWrap
+ }
+
+ Item {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 60
+ Layout.topMargin: 22
+
+ RowLayout {
+ anchors.fill: parent
+ anchors.topMargin: 6
+ spacing: 8
+
+ WizardTextField {
+ id: serverUrlField
+ Layout.fillWidth: true
+ text: root.controller.serverUrl
+ enabled: !root.controller.busy
+ readOnly: !root.controller.serverUrlEditable
+ placeholderText: root.controller.serverUrlPlaceholder
+ inputMethodHints: Qt.ImhUrlCharactersOnly | Qt.ImhNoAutoUppercase
+ selectByMouse: true
+ onTextEdited: root.controller.serverUrl = text
+ onAccepted: root.controller.submitServerUrl()
+ }
+
+ WizardButton {
+ primary: true
+ Layout.preferredWidth: 76
+ enabled: !root.controller.busy
+ text: qsTr("Log in")
+ onClicked: root.controller.submitServerUrl()
+ }
+ }
+
+ Rectangle {
+ x: 8
+ y: 0
+ width: serverAddressLabel.implicitWidth + 8
+ height: serverAddressLabel.implicitHeight
+ color: "white"
+
+ EnforcedPlainTextLabel {
+ id: serverAddressLabel
+ anchors.centerIn: parent
+ text: qsTr("Server address")
+ color: root.hintTextColor
+ font.pixelSize: Style.pixelSize
+ }
+ }
+ }
+
+ EnforcedPlainTextLabel {
+ visible: root.controller.errorText !== ""
+ text: root.controller.errorText
+ color: Style.errorBoxBackgroundColor
+ font.pixelSize: Style.pixelSize + 1
+ Layout.fillWidth: true
+ wrapMode: Text.WordWrap
+ }
+
+ RowLayout {
+ visible: root.controller.busy && root.controller.authStatusText !== ""
+ Layout.fillWidth: true
+ spacing: 8
+
+ BusyIndicator {
+ running: root.controller.busy
+ visible: running
+ Layout.preferredWidth: 20
+ Layout.preferredHeight: 20
+ }
+
+ EnforcedPlainTextLabel {
+ text: root.controller.authStatusText
+ color: root.hintTextColor
+ font.pixelSize: Style.pixelSize + 1
+ Layout.fillWidth: true
+ wrapMode: Text.WordWrap
+ }
+ }
+
+ Item {
+ Layout.fillHeight: true
+ }
+ }
+}
diff --git a/src/gui/wizard/qml/SyncOptionsPage.qml b/src/gui/wizard/qml/SyncOptionsPage.qml
new file mode 100644
index 0000000000000..944238be5b269
--- /dev/null
+++ b/src/gui/wizard/qml/SyncOptionsPage.qml
@@ -0,0 +1,174 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Style
+import com.nextcloud.desktopclient
+import "../../tray"
+
+Item {
+ id: root
+
+ required property var controller
+ readonly property string serverLabel: root.controller.serverDisplayName !== ""
+ ? root.controller.serverDisplayName
+ : root.controller.serverUrl.replace(/^https?:\/\//, "").replace(/\/$/, "")
+ readonly property color primaryTextColor: "#111111"
+ readonly property color hintTextColor: Qt.rgba(0, 0, 0, 0.62)
+
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.leftMargin: 28
+ anchors.rightMargin: 28
+ anchors.topMargin: 40
+ anchors.bottomMargin: 16
+ spacing: 2
+
+ Item {
+ Layout.preferredWidth: 80
+ Layout.preferredHeight: 80
+ Layout.alignment: Qt.AlignHCenter
+
+ Rectangle {
+ anchors.fill: parent
+ radius: width / 2
+ color: "#dfe8ee"
+ visible: accountAvatar.status !== Image.Ready
+
+ EnforcedPlainTextLabel {
+ anchors.centerIn: parent
+ text: root.controller.userDisplayName !== "" ? root.controller.userDisplayName.charAt(0).toUpperCase() : ""
+ color: root.primaryTextColor
+ font.pixelSize: Style.pixelSize + 22
+ font.bold: true
+ }
+ }
+
+ Image {
+ id: accountAvatar
+ anchors.fill: parent
+ source: root.controller.avatarUrl
+ sourceSize.width: 80
+ sourceSize.height: 80
+ fillMode: Image.PreserveAspectCrop
+ cache: false
+ visible: status === Image.Ready
+ }
+ }
+
+ EnforcedPlainTextLabel {
+ text: root.controller.userDisplayName
+ color: root.primaryTextColor
+ font.pixelSize: Style.pixelSize + 6
+ font.bold: true
+ horizontalAlignment: Text.AlignHCenter
+ Layout.fillWidth: true
+ wrapMode: Text.WordWrap
+ }
+
+ EnforcedPlainTextLabel {
+ text: root.serverLabel
+ color: root.hintTextColor
+ font.pixelSize: Style.pixelSize + 2
+ horizontalAlignment: Text.AlignHCenter
+ Layout.fillWidth: true
+ elide: Text.ElideMiddle
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ Layout.topMargin: 24
+ spacing: 8
+
+ OptionRow {
+ Layout.fillWidth: true
+ visible: root.controller.canUseVirtualFiles
+ title: qsTr("Virtual files")
+ description: qsTr("Download files on-demand")
+ selected: root.controller.syncMode === AccountWizardController.VirtualFiles
+ onClicked: root.controller.setSyncMode(AccountWizardController.VirtualFiles)
+ }
+
+ OptionRow {
+ Layout.fillWidth: true
+ title: qsTr("Synchronize everything")
+ description: root.controller.syncEverythingDescription
+ selected: root.controller.syncMode === AccountWizardController.SyncEverything
+ onClicked: root.controller.setSyncMode(AccountWizardController.SyncEverything)
+ }
+
+ OptionRow {
+ Layout.fillWidth: true
+ title: qsTr("Choose what to sync")
+ description: ""
+ selected: root.controller.syncMode === AccountWizardController.SelectiveSync
+ onClicked: root.controller.openSelectiveSync()
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ Layout.topMargin: 18
+ spacing: 6
+ visible: root.controller.localSyncFolderRequired
+
+ EnforcedPlainTextLabel {
+ text: qsTr("Local sync folder")
+ color: root.primaryTextColor
+ font.pixelSize: Style.pixelSize + 1
+ font.bold: true
+ Layout.fillWidth: true
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: 8
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 36
+ radius: 6
+ border.width: 1
+ border.color: root.controller.localSyncFolderError === "" ? "#e1e8ee" : "#d84b4b"
+ color: "#f5f8fa"
+
+ EnforcedPlainTextLabel {
+ anchors.fill: parent
+ anchors.leftMargin: 10
+ anchors.rightMargin: 10
+ verticalAlignment: Text.AlignVCenter
+ text: root.controller.localSyncFolderDisplay
+ color: root.primaryTextColor
+ font.pixelSize: Style.pixelSize
+ elide: Text.ElideMiddle
+ }
+ }
+
+ WizardButton {
+ text: qsTr("Choose")
+ Layout.preferredWidth: 96
+ Layout.preferredHeight: 36
+ onClicked: root.controller.chooseLocalSyncFolder()
+ }
+ }
+
+ EnforcedPlainTextLabel {
+ visible: root.controller.localSyncFolderError !== ""
+ text: root.controller.localSyncFolderError
+ color: "#b00020"
+ font.pixelSize: Style.pixelSize
+ Layout.fillWidth: true
+ wrapMode: Text.WordWrap
+ maximumLineCount: 2
+ }
+ }
+
+ Item {
+ Layout.fillHeight: true
+ }
+ }
+}
diff --git a/src/gui/wizard/qml/WizardButton.qml b/src/gui/wizard/qml/WizardButton.qml
new file mode 100644
index 0000000000000..0e3a5085807fb
--- /dev/null
+++ b/src/gui/wizard/qml/WizardButton.qml
@@ -0,0 +1,91 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+import QtQuick
+import QtQuick.Controls
+import Style
+
+Button {
+ id: root
+
+ property bool primary: false
+ property string iconSource: ""
+ property bool iconBeforeText: false
+ property string textSuffix: ""
+ readonly property color primaryColor: "#2B659A"
+ readonly property color primaryPressedColor: "#245783"
+ readonly property color secondaryColor: "#e7eef4"
+ readonly property color secondaryPressedColor: "#dce8f0"
+ readonly property color secondaryBorderColor: "#d5e0e7"
+ readonly property color disabledColor: "#eef3f7"
+ readonly property color disabledBorderColor: "#dde7ee"
+
+ implicitHeight: 36
+ leftPadding: 18
+ rightPadding: 18
+ font.pointSize: 16
+ font.weight: Font.Medium
+
+ contentItem: Item {
+ implicitWidth: contentRow.implicitWidth
+ implicitHeight: contentRow.implicitHeight
+
+ Row {
+ id: contentRow
+
+ anchors.centerIn: parent
+ spacing: 6
+
+ Image {
+ visible: root.iconSource !== "" && root.iconBeforeText
+ source: root.iconSource
+ sourceSize.width: 16
+ sourceSize.height: 16
+ width: visible ? 16 : 0
+ height: 16
+ anchors.verticalCenter: parent.verticalCenter
+ fillMode: Image.PreserveAspectFit
+ }
+
+ Text {
+ text: root.textSuffix === "" ? root.text : root.text + " " + root.textSuffix
+ font: root.font
+ color: root.enabled
+ ? (root.primary ? "white" : root.palette.buttonText)
+ : "#8a949c"
+ anchors.verticalCenter: parent.verticalCenter
+ elide: Text.ElideRight
+ }
+
+ Image {
+ visible: root.iconSource !== "" && !root.iconBeforeText
+ source: root.iconSource
+ sourceSize.width: 16
+ sourceSize.height: 16
+ width: visible ? 16 : 0
+ height: 16
+ anchors.verticalCenter: parent.verticalCenter
+ fillMode: Image.PreserveAspectFit
+ }
+ }
+ }
+
+ background: Rectangle {
+ radius: 8
+ border.width: root.primary ? 0 : 1
+ border.color: root.enabled ? root.secondaryBorderColor : root.disabledBorderColor
+ color: {
+ if (!root.enabled) {
+ return root.disabledColor
+ }
+ if (root.primary) {
+ return root.down ? root.primaryPressedColor : root.primaryColor
+ }
+ return root.down
+ ? root.secondaryPressedColor
+ : root.secondaryColor
+ }
+ }
+}
diff --git a/src/gui/wizard/qml/WizardDialogFrame.qml b/src/gui/wizard/qml/WizardDialogFrame.qml
new file mode 100644
index 0000000000000..77432179d8aea
--- /dev/null
+++ b/src/gui/wizard/qml/WizardDialogFrame.qml
@@ -0,0 +1,49 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+Pane {
+ id: root
+
+ default property alias contents: body.data
+ property alias footer: footerLayout.data
+ readonly property int windowMargin: 24
+ readonly property int footerButtonHeight: 36
+
+ padding: 0
+
+ background: Rectangle {
+ color: "transparent"
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: 0
+
+ Item {
+ id: body
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ }
+
+ Item {
+ Layout.fillWidth: true
+ Layout.preferredHeight: root.footerButtonHeight + root.windowMargin
+
+ RowLayout {
+ id: footerLayout
+ anchors.fill: parent
+ anchors.leftMargin: root.windowMargin
+ anchors.rightMargin: root.windowMargin
+ anchors.topMargin: 0
+ anchors.bottomMargin: root.windowMargin
+ spacing: 8
+ }
+ }
+ }
+}
diff --git a/src/gui/wizard/qml/WizardTextField.qml b/src/gui/wizard/qml/WizardTextField.qml
new file mode 100644
index 0000000000000..c23a76f75668a
--- /dev/null
+++ b/src/gui/wizard/qml/WizardTextField.qml
@@ -0,0 +1,31 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+import QtQuick
+import QtQuick.Controls
+import Style
+
+TextField {
+ id: root
+
+ implicitHeight: Style.standardPrimaryButtonHeight
+ leftPadding: 12
+ rightPadding: 12
+ topPadding: 0
+ bottomPadding: 0
+ verticalAlignment: TextInput.AlignVCenter
+ font.pixelSize: Style.pixelSize + 3
+ color: "black"
+ placeholderTextColor: Qt.rgba(0, 0, 0, 0.50)
+ selectionColor: Style.ncBlue
+ selectedTextColor: "white"
+
+ background: Rectangle {
+ radius: 8
+ color: "white"
+ border.width: 1
+ border.color: root.activeFocus ? Style.ncBlue : Qt.rgba(0, 0, 0, 0.24)
+ }
+}
diff --git a/src/gui/wizard/wizardproxysettingsdialog.cpp b/src/gui/wizard/wizardproxysettingsdialog.cpp
index 241ae5a868375..f07fbdbeeb876 100644
--- a/src/gui/wizard/wizardproxysettingsdialog.cpp
+++ b/src/gui/wizard/wizardproxysettingsdialog.cpp
@@ -5,59 +5,218 @@
#include "wizardproxysettingsdialog.h"
-#include
+#include "ui_proxysettings.h"
+
+#include
+#include
+#include
#include
+#include
+#include
+#include
+#include
+#include
+#include
namespace OCC {
Q_LOGGING_CATEGORY(lcWizardProxySettings, "nextcloud.gui.wizard.proxysettings", QtInfoMsg)
+namespace {
+const QString invalidInputStyle = QStringLiteral(
+ "min-height: 40px;"
+ "max-height: 40px;"
+ "border: 1px solid red;"
+ "border-radius: 8px;"
+ "padding: 0px 12px;"
+ "background: white;"
+ "font-size: 15pt;"
+);
+
+class WindowDragHandle : public QWidget
+{
+public:
+ using QWidget::QWidget;
+
+protected:
+ void mousePressEvent(QMouseEvent *event) override
+ {
+ if (event->button() == Qt::LeftButton) {
+ if (const auto *window = this->window(); window && window->windowHandle()) {
+ window->windowHandle()->startSystemMove();
+ event->accept();
+ return;
+ }
+ }
+
+ QWidget::mousePressEvent(event);
+ }
+};
+}
+
WizardProxySettingsDialog::WizardProxySettingsDialog(QUrl serverURL,
WizardProxySettings proxySettings,
QWidget *parent)
: QDialog(parent)
+ , _ui(std::make_unique())
{
- _ui.setupUi(this);
-
- setWindowModality(Qt::WindowModal);
- setWindowTitle(tr("Proxy Settings", "Dialog window title for proxy settings"));
+#if defined(Q_OS_MACOS) && QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)
+ setWindowFlag(Qt::ExpandedClientAreaHint, true);
+ setWindowFlag(Qt::NoTitleBarBackgroundHint, true);
+#endif
- _ui.hostLineEdit->setPlaceholderText(tr("Hostname of proxy server"));
- _ui.userLineEdit->setPlaceholderText(tr("Username for proxy server"));
- _ui.passwordLineEdit->setPlaceholderText(tr("Password for proxy server"));
+ _ui->setupUi(this);
- _ui.typeComboBox->addItem(tr("HTTP(S) proxy"), QNetworkProxy::HttpProxy);
- _ui.typeComboBox->addItem(tr("SOCKS5 proxy"), QNetworkProxy::Socks5Proxy);
+ setWindowModality(Qt::WindowModal);
+ setWindowTitle({});
+ setMinimumSize(500, 448);
+ resize(500, 448);
- _ui.authRequiredcheckBox->setEnabled(true);
+ if (layout()) {
+ layout()->setContentsMargins(24, 24, 24, 24);
+ layout()->setSpacing(14);
+ }
+ _ui->gridLayout_2->setContentsMargins(0, 8, 0, 0);
+ _ui->gridLayout_2->setHorizontalSpacing(8);
+ _ui->gridLayout_2->setVerticalSpacing(8);
+ _ui->verticalLayout_2->setContentsMargins(0, 8, 0, 0);
+ _ui->verticalLayout_2->setSpacing(10);
+ _ui->horizontalLayout_3->setSpacing(8);
+ _ui->horizontalLayout_4->setSpacing(8);
+ _ui->horizontalLayout_10->setSpacing(8);
+ _ui->horizontalSpacer_4->changeSize(0, 0, QSizePolicy::Fixed, QSizePolicy::Minimum);
+ _ui->horizontalSpacer_5->changeSize(0, 0, QSizePolicy::Fixed, QSizePolicy::Minimum);
+ _ui->horizontalSpacer_6->changeSize(0, 0, QSizePolicy::Fixed, QSizePolicy::Minimum);
+
+ _ui->hostLineEdit->setPlaceholderText(tr("Hostname of proxy server"));
+ _ui->userLineEdit->setPlaceholderText(tr("Username for proxy server"));
+ _ui->passwordLineEdit->setPlaceholderText(tr("Password for proxy server"));
+ _ui->typeComboBox->setMinimumHeight(40);
+ _ui->hostLineEdit->setMinimumHeight(40);
+ _ui->portSpinBox->setMinimumHeight(40);
+ _ui->userLineEdit->setMinimumHeight(40);
+ _ui->passwordLineEdit->setMinimumHeight(40);
+ _ui->buttonBox->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
+
+ _ui->typeComboBox->addItem(tr("HTTP(S) proxy"), QNetworkProxy::HttpProxy);
+ _ui->typeComboBox->addItem(tr("SOCKS5 proxy"), QNetworkProxy::Socks5Proxy);
+
+ _ui->authRequiredcheckBox->setEnabled(true);
// Explicitly set up the enabled status of the proxy auth widgets to ensure
// toggling the parent enables/disables the children
- _ui.userLineEdit->setEnabled(true);
- _ui.passwordLineEdit->setEnabled(true);
- _ui.authWidgets->setEnabled(_ui.authRequiredcheckBox->isChecked());
- connect(_ui.authRequiredcheckBox, &QAbstractButton::toggled, _ui.authWidgets, &QWidget::setEnabled);
+ _ui->userLineEdit->setEnabled(true);
+ _ui->passwordLineEdit->setEnabled(true);
+ _ui->authWidgets->setEnabled(_ui->authRequiredcheckBox->isChecked());
+ connect(_ui->authRequiredcheckBox, &QAbstractButton::toggled, _ui->authWidgets, &QWidget::setEnabled);
- connect(_ui.manualProxyRadioButton, &QAbstractButton::toggled, _ui.manualSettings, &QWidget::setVisible);
- connect(_ui.manualProxyRadioButton, &QAbstractButton::toggled, this, &WizardProxySettingsDialog::validateProxySettings);
+ connect(_ui->manualProxyRadioButton, &QAbstractButton::toggled, _ui->manualSettings, &QWidget::setVisible);
+ connect(_ui->manualProxyRadioButton, &QAbstractButton::toggled, this, &WizardProxySettingsDialog::validateProxySettings);
- connect(_ui.typeComboBox, static_cast(&QComboBox::currentIndexChanged), this, &WizardProxySettingsDialog::validateProxySettings);
- connect(_ui.authRequiredcheckBox, &QAbstractButton::toggled, this, &WizardProxySettingsDialog::validateProxySettings);
+ connect(_ui->typeComboBox, static_cast(&QComboBox::currentIndexChanged), this, &WizardProxySettingsDialog::validateProxySettings);
+ connect(_ui->authRequiredcheckBox, &QAbstractButton::toggled, this, &WizardProxySettingsDialog::validateProxySettings);
// Warn about empty proxy host
- connect(_ui.hostLineEdit, &QLineEdit::textChanged, this, &WizardProxySettingsDialog::validateProxySettings);
- connect(_ui.userLineEdit, &QLineEdit::textChanged, this, &WizardProxySettingsDialog::validateProxySettings);
- connect(_ui.passwordLineEdit, &QLineEdit::textChanged, this, &WizardProxySettingsDialog::validateProxySettings);
- connect(_ui.portSpinBox, &QSpinBox::valueChanged, this, &WizardProxySettingsDialog::validateProxySettings);
- connect(_ui.authRequiredcheckBox, &QAbstractButton::toggled, this, &WizardProxySettingsDialog::validateProxySettings);
+ connect(_ui->hostLineEdit, &QLineEdit::textChanged, this, &WizardProxySettingsDialog::validateProxySettings);
+ connect(_ui->userLineEdit, &QLineEdit::textChanged, this, &WizardProxySettingsDialog::validateProxySettings);
+ connect(_ui->passwordLineEdit, &QLineEdit::textChanged, this, &WizardProxySettingsDialog::validateProxySettings);
+ connect(_ui->portSpinBox, &QSpinBox::valueChanged, this, &WizardProxySettingsDialog::validateProxySettings);
+ connect(_ui->authRequiredcheckBox, &QAbstractButton::toggled, this, &WizardProxySettingsDialog::validateProxySettings);
- connect(_ui.buttonBox, &QDialogButtonBox::accepted,
+ connect(_ui->buttonBox, &QDialogButtonBox::accepted,
this, &WizardProxySettingsDialog::settingsDone);
- connect(_ui.buttonBox, &QDialogButtonBox::rejected,
+ connect(_ui->buttonBox, &QDialogButtonBox::rejected,
this, &WizardProxySettingsDialog::reject);
setServerUrl(std::move(serverURL));
setProxySettings(std::move(proxySettings));
+ customizeStyle();
+ _ui->buttonBox->setCenterButtons(false);
+ if (_ui->buttonBox->layout()) {
+ _ui->buttonBox->layout()->setAlignment(Qt::AlignRight);
+ _ui->buttonBox->layout()->setSpacing(8);
+ }
+
+ for (auto *button : _ui->buttonBox->buttons()) {
+ auto buttonFont = button->font();
+ buttonFont.setPointSize(16);
+ buttonFont.setWeight(QFont::Medium);
+ button->setFont(buttonFont);
+ button->setMinimumHeight(36);
+ button->setMinimumWidth(112);
+ button->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
+ }
+
+#if defined(Q_OS_MACOS) && QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)
+ _windowDragHandle = new WindowDragHandle(this);
+ _windowDragHandle->setObjectName(QLatin1String("wizard_proxy_window_drag_handle"));
+ _windowDragHandle->setFixedHeight(28);
+ _windowDragHandle->setGeometry(0, 0, width(), _windowDragHandle->height());
+ _windowDragHandle->raise();
+#endif
+}
+
+WizardProxySettingsDialog::~WizardProxySettingsDialog() = default;
+
+void WizardProxySettingsDialog::resizeEvent(QResizeEvent *event)
+{
+ QDialog::resizeEvent(event);
+
+#if defined(Q_OS_MACOS) && QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)
+ if (_windowDragHandle) {
+ _windowDragHandle->setGeometry(0, 0, width(), _windowDragHandle->height());
+ _windowDragHandle->raise();
+ }
+#endif
+}
+
+void WizardProxySettingsDialog::customizeStyle()
+{
+ setStyleSheet(QStringLiteral(
+ "QDialog, QWidget#ProxySettings { background: white; }"
+ "QGroupBox { border: none; margin-top: 28px; padding-top: 0px; font-size: 20px; font-weight: bold; }"
+ "QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; padding: 0px; }"
+ "QRadioButton, QCheckBox, QLabel { font-size: 15pt; }"
+ "QLineEdit, QSpinBox, QComboBox {"
+ " min-height: 40px;"
+ " max-height: 40px;"
+ " border: 1px solid rgb(196, 196, 196);"
+ " border-radius: 8px;"
+ " padding: 0px 12px;"
+ " background: white;"
+ " font-size: 15pt;"
+ " }"
+ "QComboBox { padding-right: 36px; }"
+ "QComboBox::drop-down {"
+ " subcontrol-origin: padding;"
+ " subcontrol-position: top right;"
+ " width: 32px;"
+ " border-left: none;"
+ " }"
+ "QComboBox::down-arrow {"
+ " image: url(:/client/theme/black/caret-down.svg);"
+ " width: 16px;"
+ " height: 16px;"
+ " }"
+ "QLineEdit:focus, QSpinBox:focus, QComboBox:focus { border: 1px solid palette(highlight); }"
+ "QDialogButtonBox { border: none; padding: 0px; }"
+ "QPushButton {"
+ " min-height: 36px;"
+ " max-height: 36px;"
+ " border: none;"
+ " border-radius: 8px;"
+ " padding: 0px 18px;"
+ " background-color: #e7eef4;"
+ " font-size: 16pt;"
+ " font-weight: 500;"
+ " }"
+ "QPushButton:hover { background-color: #e7eef4; }"
+ "QPushButton:pressed { background-color: #dce8f0; }"
+ "QPushButton:default { background-color: #2b659a; color: white; }"
+ "QPushButton:default:hover { background-color: #2b659a; color: white; }"
+ "QPushButton:default:pressed { background-color: #245783; color: white; }"
+ "QPushButton:disabled { color: rgb(150, 150, 150); background: rgb(242, 242, 242); }"
+ ));
}
void WizardProxySettingsDialog::setServerUrl(QUrl serverUrl)
@@ -72,41 +231,34 @@ void WizardProxySettingsDialog::setServerUrl(QUrl serverUrl)
void WizardProxySettingsDialog::setProxySettings(WizardProxySettings proxySettings)
{
- if (_settings == proxySettings) {
- return;
- }
-
_settings = std::move(proxySettings);
- if (!_settings._user.isEmpty()) {
- _ui.userLineEdit->setText(_settings._user);
- }
- if (!_settings._password.isEmpty()) {
- _ui.passwordLineEdit->setText(_settings._password);
- }
- if (!_settings._host.isEmpty()) {
- _ui.hostLineEdit->setText(_settings._host);
+ _ui->userLineEdit->setText(_settings._user);
+ _ui->passwordLineEdit->setText(_settings._password);
+ _ui->hostLineEdit->setText(_settings._host);
+ if (_settings._port > 0) {
+ _ui->portSpinBox->setValue(_settings._port);
}
- _ui.authRequiredcheckBox->setChecked(_settings._needsAuth != ProxyAuthentication::NoAuthentication);
+ _ui->authRequiredcheckBox->setChecked(_settings._needsAuth != ProxyAuthentication::NoAuthentication);
switch (_settings._proxyType)
{
case QNetworkProxy::NoProxy:
- _ui.noProxyRadioButton->setChecked(true);
- _ui.noProxyRadioButton->setFocus();
- _ui.manualSettings->setVisible(false);
+ _ui->noProxyRadioButton->setChecked(true);
+ _ui->noProxyRadioButton->setFocus();
+ _ui->manualSettings->setVisible(false);
break;
case QNetworkProxy::ProxyType::DefaultProxy:
- _ui.systemProxyRadioButton->setChecked(true);
- _ui.systemProxyRadioButton->setFocus();
- _ui.manualSettings->setVisible(false);
+ _ui->systemProxyRadioButton->setChecked(true);
+ _ui->systemProxyRadioButton->setFocus();
+ _ui->manualSettings->setVisible(false);
break;
case QNetworkProxy::Socks5Proxy:
case QNetworkProxy::HttpProxy:
- _ui.manualProxyRadioButton->setChecked(true);
- _ui.manualProxyRadioButton->setFocus();
- _ui.manualSettings->setVisible(true);
+ _ui->manualProxyRadioButton->setChecked(true);
+ _ui->manualProxyRadioButton->setFocus();
+ _ui->manualSettings->setVisible(true);
break;
case QNetworkProxy::HttpCachingProxy:
case QNetworkProxy::FtpCachingProxy:
@@ -118,45 +270,45 @@ void WizardProxySettingsDialog::setProxySettings(WizardProxySettings proxySettin
void WizardProxySettingsDialog::checkEmptyProxyHost()
{
- if (_ui.hostLineEdit->isEnabled() && _ui.hostLineEdit->text().isEmpty()) {
- _ui.hostLineEdit->setStyleSheet("border: 1px solid red");
+ if (_ui->manualProxyRadioButton->isChecked() && _ui->hostLineEdit->isEnabled() && _ui->hostLineEdit->text().isEmpty()) {
+ _ui->hostLineEdit->setStyleSheet(invalidInputStyle);
} else {
- _ui.hostLineEdit->setStyleSheet(QString());
+ _ui->hostLineEdit->setStyleSheet(QString());
}
}
void WizardProxySettingsDialog::checkEmptyProxyCredentials()
{
- if (!_ui.authRequiredcheckBox->isChecked()) {
- _ui.userLineEdit->setStyleSheet(QString());
- _ui.passwordLineEdit->setStyleSheet(QString());
+ if (!_ui->authRequiredcheckBox->isChecked()) {
+ _ui->userLineEdit->setStyleSheet(QString());
+ _ui->passwordLineEdit->setStyleSheet(QString());
return;
}
- if (_ui.userLineEdit->text().isEmpty()) {
- _ui.userLineEdit->setStyleSheet("border: 1px solid red");
- } else {
- _ui.userLineEdit->setStyleSheet(QString());
- }
-
- if (_ui.passwordLineEdit->text().isEmpty()) {
- _ui.passwordLineEdit->setStyleSheet("border: 1px solid red");
- } else {
- _ui.passwordLineEdit->setStyleSheet(QString());
- }
+ if (_ui->userLineEdit->text().isEmpty()) {
+ _ui->userLineEdit->setStyleSheet(invalidInputStyle);
+ } else {
+ _ui->userLineEdit->setStyleSheet(QString());
+ }
+
+ if (_ui->passwordLineEdit->text().isEmpty()) {
+ _ui->passwordLineEdit->setStyleSheet(invalidInputStyle);
+ } else {
+ _ui->passwordLineEdit->setStyleSheet(QString());
+ }
}
void WizardProxySettingsDialog::checkAccountLocalhost()
{
auto visible = false;
- if (_ui.manualProxyRadioButton->isChecked()) {
+ if (_ui->manualProxyRadioButton->isChecked()) {
const auto host = _serverURL.host();
// Some typical url for localhost
if (host == "localhost" || host.startsWith("127.") || host == "[::1]") {
visible = true;
}
}
- _ui.labelLocalhost->setVisible(visible);
+ _ui->labelLocalhost->setVisible(visible);
}
void WizardProxySettingsDialog::validateProxySettings()
@@ -165,37 +317,38 @@ void WizardProxySettingsDialog::validateProxySettings()
checkEmptyProxyCredentials();
checkAccountLocalhost();
- _settings._user = _ui.userLineEdit->text();
- _settings._password = _ui.passwordLineEdit->text();
- _settings._host = _ui.hostLineEdit->text();
- _settings._port = _ui.portSpinBox->value();
- _settings._needsAuth = _ui.authRequiredcheckBox->isChecked() ? ProxyAuthentication::AuthenticationRequired : ProxyAuthentication::NoAuthentication;
+ _settings._user = _ui->userLineEdit->text();
+ _settings._password = _ui->passwordLineEdit->text();
+ _settings._host = _ui->hostLineEdit->text();
+ _settings._port = _ui->portSpinBox->value();
+ _settings._needsAuth = _ui->authRequiredcheckBox->isChecked() ? ProxyAuthentication::AuthenticationRequired : ProxyAuthentication::NoAuthentication;
_settings._proxyType = QNetworkProxy::NoProxy;
_valid = false;
- if (_ui.noProxyRadioButton->isChecked()) {
+ if (_ui->noProxyRadioButton->isChecked()) {
_settings._proxyType = QNetworkProxy::NoProxy;
_valid = true;
- } else if (_ui.systemProxyRadioButton->isChecked()) {
+ } else if (_ui->systemProxyRadioButton->isChecked()) {
_settings._proxyType = QNetworkProxy::DefaultProxy;
_valid = true;
- } else if (_ui.manualProxyRadioButton->isChecked()) {
- _settings._proxyType = _ui.typeComboBox->itemData(_ui.typeComboBox->currentIndex()).value();
+ } else if (_ui->manualProxyRadioButton->isChecked()) {
+ _settings._proxyType = _ui->typeComboBox->itemData(_ui->typeComboBox->currentIndex()).value();
_valid = true;
if (_settings._host.isEmpty()) {
_settings._proxyType = QNetworkProxy::NoProxy;
_valid = false;
}
- if (_ui.authRequiredcheckBox->isChecked() && (_settings._user.isEmpty() || _settings._password.isEmpty())) {
+ if (_ui->authRequiredcheckBox->isChecked() && (_settings._user.isEmpty() || _settings._password.isEmpty())) {
_settings._proxyType = QNetworkProxy::NoProxy;
_valid = false;
}
}
- const auto okButton = _ui.buttonBox->button(QDialogButtonBox::Ok);
+ const auto okButton = _ui->buttonBox->button(QDialogButtonBox::Ok);
if (okButton) {
okButton->setEnabled(_valid);
+ okButton->setDefault(true);
}
}
diff --git a/src/gui/wizard/wizardproxysettingsdialog.h b/src/gui/wizard/wizardproxysettingsdialog.h
index 01dd921369092..0bd08fdb97fe1 100644
--- a/src/gui/wizard/wizardproxysettingsdialog.h
+++ b/src/gui/wizard/wizardproxysettingsdialog.h
@@ -7,8 +7,13 @@
#include
#include
+#include
-#include "ui_proxysettings.h"
+#include
+
+class QResizeEvent;
+class QWidget;
+class Ui_ProxySettings;
namespace OCC {
@@ -26,7 +31,7 @@ class WizardProxySettingsDialog : public QDialog
QString _user;
QString _password;
QString _host;
- quint16 _port;
+ quint16 _port = 8080;
ProxyAuthentication _needsAuth = ProxyAuthentication::NoAuthentication;
QNetworkProxy::ProxyType _proxyType = QNetworkProxy::NoProxy;
@@ -36,6 +41,7 @@ class WizardProxySettingsDialog : public QDialog
explicit WizardProxySettingsDialog(QUrl serverURL,
WizardProxySettings proxySettings,
QWidget *parent = nullptr);
+ ~WizardProxySettingsDialog() override;
void setServerUrl(QUrl serverUrl);
@@ -56,14 +62,23 @@ private Q_SLOTS:
void settingsDone();
+protected:
+ void resizeEvent(QResizeEvent *event) override;
+
private:
- Ui_ProxySettings _ui{};
+ void customizeStyle();
+
+ std::unique_ptr _ui;
QUrl _serverURL;
bool _valid = false;
WizardProxySettings _settings;
+
+#if defined(Q_OS_MACOS) && QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)
+ QWidget *_windowDragHandle = nullptr;
+#endif
};
}
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 22702af53a303..9a59cc7b49e76 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -151,6 +151,7 @@ nextcloud_add_test(LongPath)
nextcloud_add_benchmark(LargeSync)
nextcloud_add_test(Account)
+nextcloud_add_test(AccountWizardController)
nextcloud_add_test(Folder)
nextcloud_add_test(FolderMan)
nextcloud_add_test(RemoteWipe)
diff --git a/test/testaccountwizardcontroller.cpp b/test/testaccountwizardcontroller.cpp
new file mode 100644
index 0000000000000..bb87b394d037c
--- /dev/null
+++ b/test/testaccountwizardcontroller.cpp
@@ -0,0 +1,63 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "gui/wizard/accountwizardcontroller.h"
+
+#include
+#include
+#include
+
+using namespace OCC;
+
+class TestAccountWizardController : public QObject
+{
+ Q_OBJECT
+
+private slots:
+ void initTestCase()
+ {
+ QStandardPaths::setTestModeEnabled(true);
+ }
+
+ void normalizesCommonServerUrlSuffixes()
+ {
+ QCOMPARE(AccountWizardController::normalizeServerUrlInput(QStringLiteral(" https://cloud.example/index.php ")),
+ QStringLiteral("https://cloud.example/"));
+ QCOMPARE(AccountWizardController::normalizeServerUrlInput(QStringLiteral("https://cloud.example/remote.php/dav/"), QStringLiteral("remote.php/dav/")),
+ QStringLiteral("https://cloud.example/"));
+ }
+
+ void invalidServerUrlStaysOnServerStep()
+ {
+ AccountWizardController controller;
+ QSignalSpy errorSpy(&controller, &AccountWizardController::errorTextChanged);
+
+ controller.setServerUrl(QStringLiteral("not a url"));
+ controller.submitServerUrl();
+
+ QCOMPARE(controller.currentStep(), AccountWizardController::ServerStep);
+ QVERIFY(!controller.errorText().isEmpty());
+ QCOMPARE(errorSpy.count(), 1);
+ }
+
+ void advancedSafeguardsEmitChanges()
+ {
+ AccountWizardController controller;
+ QSignalSpy askLargeSpy(&controller, &AccountWizardController::askBeforeLargeFoldersChanged);
+ QSignalSpy thresholdSpy(&controller, &AccountWizardController::largeFolderThresholdMbChanged);
+ QSignalSpy externalSpy(&controller, &AccountWizardController::askBeforeExternalStorageChanged);
+
+ controller.setAskBeforeLargeFolders(!controller.askBeforeLargeFolders());
+ controller.setLargeFolderThresholdMb(controller.largeFolderThresholdMb() + 1);
+ controller.setAskBeforeExternalStorage(!controller.askBeforeExternalStorage());
+
+ QCOMPARE(askLargeSpy.count(), 1);
+ QCOMPARE(thresholdSpy.count(), 1);
+ QCOMPARE(externalSpy.count(), 1);
+ }
+};
+
+QTEST_MAIN(TestAccountWizardController)
+#include "testaccountwizardcontroller.moc"
diff --git a/test/testiconutils.cpp b/test/testiconutils.cpp
index bd6d81ece96f4..138204b5760b1 100644
--- a/test/testiconutils.cpp
+++ b/test/testiconutils.cpp
@@ -71,6 +71,13 @@ private slots:
QVERIFY(!OCC::Ui::IconUtils::createSvgImageWithCustomColor(whiteImages.at(0), QColorConstants::Svg::blue).isNull());
}
+ void testCreateSvgPixmapWithWizardSyncOptionIcons()
+ {
+ QVERIFY(!OCC::Ui::IconUtils::createSvgImageWithCustomColor(QStringLiteral("folder.svg"), QColor(QStringLiteral("#111111"))).isNull());
+ QVERIFY(!OCC::Ui::IconUtils::createSvgImageWithCustomColor(QStringLiteral("sync.svg"), QColor(QStringLiteral("#111111"))).isNull());
+ QVERIFY(!OCC::Ui::IconUtils::createSvgImageWithCustomColor(QStringLiteral("wizard-files.svg"), QColor(QStringLiteral("#111111"))).isNull());
+ }
+
void testPixmapForBackground()
{
const QDir blackSvgDir(QString(QString{OCC::Theme::themePrefix}) + QStringLiteral("black"));
diff --git a/theme.qrc.in b/theme.qrc.in
index 8ebb6e3d7b590..f5afe78ff3e23 100644
--- a/theme.qrc.in
+++ b/theme.qrc.in
@@ -154,6 +154,7 @@
theme/colored/folder@2x.png
theme/colored/wizard-files.png
theme/colored/wizard-files@2x.png
+ theme/colored/wizard-files.svg
theme/colored/wizard-groupware.png
theme/colored/wizard-groupware@2x.png
theme/colored/wizard-nextcloud.png
@@ -214,6 +215,7 @@
theme/black/user.svg
theme/black/wizard-files.png
theme/black/wizard-files@2x.png
+ theme/black/wizard-files.svg
theme/black/wizard-groupware.png
theme/black/wizard-groupware@2x.png
theme/black/wizard-groupware.svg
@@ -254,6 +256,7 @@
theme/white/user.svg
theme/white/wizard-files.png
theme/white/wizard-files@2x.png
+ theme/white/wizard-files.svg
theme/white/wizard-groupware.png
theme/white/wizard-groupware@2x.png
theme/white/wizard-groupware.svg
@@ -286,6 +289,7 @@
theme/reply.svg
theme/magnifying-glass.svg
theme/external.svg
+ theme/globe.svg
theme/delete.svg
theme/send.svg
theme/talk-app.svg
diff --git a/theme/globe.svg b/theme/globe.svg
new file mode 100644
index 0000000000000..fed1991f254fc
--- /dev/null
+++ b/theme/globe.svg
@@ -0,0 +1,5 @@
+
+