From 11e5f9506311f273994a7fff193cfbbfb842d3da Mon Sep 17 00:00:00 2001 From: Rello Date: Tue, 12 May 2026 14:32:32 +0200 Subject: [PATCH 01/14] feature: New Account wizard with QML implementation Signed-off-by: Rello --- resources.qrc | 8 + src/gui/CMakeLists.txt | 2 + src/gui/owncloudgui.cpp | 2 + src/gui/owncloudsetupwizard.cpp | 82 +- src/gui/owncloudsetupwizard.h | 6 + src/gui/wizard/accountwizardcontroller.cpp | 814 +++++++++++++++++++ src/gui/wizard/accountwizardcontroller.h | 188 +++++ src/gui/wizard/qml/AccountWizardWindow.qml | 160 ++++ src/gui/wizard/qml/AdvancedOptionsDialog.qml | 50 ++ src/gui/wizard/qml/BrowserAuthPage.qml | 88 ++ src/gui/wizard/qml/OptionRow.qml | 70 ++ src/gui/wizard/qml/ServerPage.qml | 95 +++ src/gui/wizard/qml/SyncOptionsPage.qml | 70 ++ src/gui/wizard/qml/WizardButton.qml | 44 + src/gui/wizard/qml/WizardDialogFrame.qml | 56 ++ test/CMakeLists.txt | 1 + test/testaccountwizardcontroller.cpp | 63 ++ 17 files changed, 1798 insertions(+), 1 deletion(-) create mode 100644 src/gui/wizard/accountwizardcontroller.cpp create mode 100644 src/gui/wizard/accountwizardcontroller.h create mode 100644 src/gui/wizard/qml/AccountWizardWindow.qml create mode 100644 src/gui/wizard/qml/AdvancedOptionsDialog.qml create mode 100644 src/gui/wizard/qml/BrowserAuthPage.qml create mode 100644 src/gui/wizard/qml/OptionRow.qml create mode 100644 src/gui/wizard/qml/ServerPage.qml create mode 100644 src/gui/wizard/qml/SyncOptionsPage.qml create mode 100644 src/gui/wizard/qml/WizardButton.qml create mode 100644 src/gui/wizard/qml/WizardDialogFrame.qml create mode 100644 test/testaccountwizardcontroller.cpp diff --git a/resources.qrc b/resources.qrc index f26f90192acfe..02cc1abae64d7 100644 --- a/resources.qrc +++ b/resources.qrc @@ -59,6 +59,14 @@ src/gui/ResolveConflictsDialog.qml src/gui/ConflictDelegate.qml src/gui/ConflictItemFileInfo.qml + src/gui/wizard/qml/AccountWizardWindow.qml + src/gui/wizard/qml/AdvancedOptionsDialog.qml + src/gui/wizard/qml/BrowserAuthPage.qml + src/gui/wizard/qml/OptionRow.qml + src/gui/wizard/qml/ServerPage.qml + src/gui/wizard/qml/SyncOptionsPage.qml + src/gui/wizard/qml/WizardButton.qml + src/gui/wizard/qml/WizardDialogFrame.qml src/gui/macOS/ui/FileProviderSettings.qml src/gui/macOS/ui/FileProviderFileDelegate.qml src/gui/integration/FileActionsWindow.qml diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 6c2bc3d8da353..3c4c38e7deb77 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -227,6 +227,8 @@ set(client_SRCS creds/webflowcredentialsdialog.cpp wizard/abstractcredswizardpage.h wizard/abstractcredswizardpage.cpp + wizard/accountwizardcontroller.h + wizard/accountwizardcontroller.cpp wizard/owncloudadvancedsetuppage.h wizard/owncloudadvancedsetuppage.cpp wizard/owncloudconnectionmethoddialog.h diff --git a/src/gui/owncloudgui.cpp b/src/gui/owncloudgui.cpp index f84eceb881dd9..cd154f48f281d 100644 --- a/src/gui/owncloudgui.cpp +++ b/src/gui/owncloudgui.cpp @@ -25,6 +25,7 @@ #include "wheelhandler.h" #include "syncconflictsmodel.h" #include "syncengine.h" +#include "wizard/accountwizardcontroller.h" #include "filedetails/datefieldbackend.h" #include "filedetails/filedetails.h" #include "filedetails/shareemodel.h" @@ -143,6 +144,7 @@ ownCloudGui::ownCloudGui(Application *parent) qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "SortedShareModel"); qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "SyncConflictsModel"); qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "FileActionsModel"); + qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "AccountWizardController"); qmlRegisterUncreatableType("com.nextcloud.desktopclient", 1, 0, "QAbstractItemModel", "QAbstractItemModel"); qmlRegisterUncreatableType("com.nextcloud.desktopclient", 1, 0, "activity", "Activity"); diff --git a/src/gui/owncloudsetupwizard.cpp b/src/gui/owncloudsetupwizard.cpp index d70dd358171be..1879e950dea5a 100644 --- a/src/gui/owncloudsetupwizard.cpp +++ b/src/gui/owncloudsetupwizard.cpp @@ -17,6 +17,8 @@ #include "owncloudsetupwizard.h" #include "owncloudpropagator_p.h" #include "sslerrordialog.h" +#include "systray.h" +#include "wizard/accountwizardcontroller.h" #include "wizard/owncloudwizard.h" #include "wizard/owncloudwizardcommon.h" #include "account.h" @@ -36,6 +38,11 @@ #include #include #include +#include +#include +#include +#include +#include using namespace Qt::StringLiterals; @@ -63,6 +70,9 @@ OwncloudSetupWizard::OwncloudSetupWizard(QObject *parent) OwncloudSetupWizard::~OwncloudSetupWizard() { + if (_qmlWizardWindow) { + _qmlWizardWindow->deleteLater(); + } _ocWizard->deleteLater(); } @@ -85,9 +95,17 @@ void OwncloudSetupWizard::runWizard(QObject *obj, const char *amember, QWidget * owncloudSetupWizard = new OwncloudSetupWizard(parent); connect(owncloudSetupWizard, SIGNAL(ownCloudWizardDone(int)), obj, amember); - connect(owncloudSetupWizard->_ocWizard, &OwncloudWizard::wizardClosed, obj, [] { owncloudSetupWizard.clear(); }); FolderMan::instance()->setSyncEnabled(false); + // The widget wizard stays as a fallback until the QML flow owns every setup branch. + if (owncloudSetupWizard->startQmlWizard()) { + connect(owncloudSetupWizard->_qmlController, &AccountWizardController::finished, obj, [] { + owncloudSetupWizard.clear(); + }); + return; + } + + connect(owncloudSetupWizard->_ocWizard, &OwncloudWizard::wizardClosed, obj, [] { owncloudSetupWizard.clear(); }); owncloudSetupWizard->startWizard(); } @@ -97,6 +115,13 @@ bool OwncloudSetupWizard::bringWizardToFrontIfVisible() return false; } + if (owncloudSetupWizard->_qmlWizardWindow) { + owncloudSetupWizard->_qmlWizardWindow->show(); + owncloudSetupWizard->_qmlWizardWindow->raise(); + owncloudSetupWizard->_qmlWizardWindow->requestActivate(); + return true; + } + ownCloudGui::raiseDialog(owncloudSetupWizard->_ocWizard); return true; } @@ -148,6 +173,61 @@ void OwncloudSetupWizard::startWizard() _ocWizard->raise(); } +bool OwncloudSetupWizard::startQmlWizard() +{ + auto *engine = Systray::instance()->trayEngine(); + if (!engine) { + qCWarning(lcWizard) << "Cannot start QML account wizard without a QML engine."; + return false; + } + + _qmlController = new AccountWizardController(this); + QQmlComponent component(engine, QStringLiteral("qrc:/qml/src/gui/wizard/qml/AccountWizardWindow.qml")); + QVariantMap initialProperties; + initialProperties.insert(QStringLiteral("controller"), QVariant::fromValue(_qmlController)); + auto *createdObject = component.createWithInitialProperties(initialProperties); + + if (component.isError()) { + qCWarning(lcWizard) << "Failed to load QML account wizard:" << component.errors(); + } + + _qmlWizardWindow = qobject_cast(createdObject); + if (!_qmlWizardWindow) { + if (createdObject) { + createdObject->deleteLater(); + } + _qmlController->deleteLater(); + _qmlController = nullptr; + return false; + } + + connect(_qmlController, &AccountWizardController::finished, this, [this](int result) { + emit ownCloudWizardDone(result); + if (_qmlWizardWindow) { + _qmlWizardWindow->deleteLater(); + _qmlWizardWindow.clear(); + } + deleteLater(); + }); + + connect(_qmlController, &AccountWizardController::legacyWizardRequested, this, [this] { + if (_qmlWizardWindow) { + _qmlWizardWindow->deleteLater(); + _qmlWizardWindow.clear(); + } + _qmlController->deleteLater(); + _qmlController = nullptr; + + connect(_ocWizard, &OwncloudWizard::wizardClosed, this, [] { owncloudSetupWizard.clear(); }); + startWizard(); + }); + + _qmlWizardWindow->show(); + _qmlWizardWindow->raise(); + _qmlWizardWindow->requestActivate(); + return true; +} + // also checks if an installation is valid and determines auth type in a second step void OwncloudSetupWizard::slotCheckServer(const QUrl &serverURL, const OCC::WizardProxySettingsDialog::WizardProxySettings &proxySettings) { diff --git a/src/gui/owncloudsetupwizard.h b/src/gui/owncloudsetupwizard.h index cdc64001fe604..ec1b4574bacc0 100644 --- a/src/gui/owncloudsetupwizard.h +++ b/src/gui/owncloudsetupwizard.h @@ -19,12 +19,15 @@ #include "wizard/wizardproxysettingsdialog.h" +class QQuickWindow; + namespace OCC { class AccountState; class TermsOfServiceChecker; class OwncloudWizard; +class AccountWizardController; /** * @brief The OwncloudSetupWizard class @@ -67,6 +70,7 @@ private slots: explicit OwncloudSetupWizard(QObject *parent = nullptr); ~OwncloudSetupWizard() override; void startWizard(); + bool startQmlWizard(); void testOwnCloudConnect(); void createRemoteFolder(); void finalizeSetup(bool); @@ -75,6 +79,8 @@ private slots: bool checkDowngradeAdvised(QNetworkReply *reply); OwncloudWizard *_ocWizard = nullptr; + AccountWizardController *_qmlController = nullptr; + QPointer _qmlWizardWindow; QString _initLocalFolder; QString _remoteFolder; }; diff --git a/src/gui/wizard/accountwizardcontroller.cpp b/src/gui/wizard/accountwizardcontroller.cpp new file mode 100644 index 0000000000000..64896dd1dd1d8 --- /dev/null +++ b/src/gui/wizard/accountwizardcontroller.cpp @@ -0,0 +1,814 @@ +/* + * 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 "folderman.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 + +#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); + } + + _account = AccountManager::createAccount(); + _account->setCredentials(CredentialsFactory::create("dummy")); + + const auto defaultUrl = + Theme::instance()->multipleOverrideServers() ? QString{} : Theme::instance()->overrideServerUrl(); + _account->setUrl(defaultUrl); + + _remoteFolder = Theme::instance()->defaultServerFolder(); + setServerUrl(defaultUrl); + setServerUrlEditable(!Theme::instance()->forceOverrideServerUrl() || Theme::instance()->multipleOverrideServers()); +} + +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; +} + +AccountWizardController::SyncMode AccountWizardController::syncMode() const +{ + return _syncMode; +} + +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; + } + + const auto normalizedServerUrl = normalizeServerUrlInput(_serverUrl, _account ? _account->davPath() : QString{}); + const auto url = QUrl::fromUserInput(normalizedServerUrl); + if (!url.isValid() || url.host().isEmpty()) { + setErrorText(tr("Server address does not seem to be valid")); + return; + } + + setServerUrl(url.toString()); + startServerCheck(url); +} + +void AccountWizardController::startServerCheck(const QUrl &serverUrl) +{ + _account->setUrl(serverUrl); + _account->setCredentials(CredentialsFactory::create("dummy")); + _account->setProxyType(QNetworkProxy::DefaultProxy); + _account->setSslConfiguration(QSslConfiguration::defaultConfiguration()); + _account->networkAccessManager()->clearAccessCache(); + + setErrorText({}); + setBusy(true); + setAuthStatusText(tr("Checking server address...")); + + 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...")); + + 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:
%3") + .arg(Utility::escape(Theme::instance()->appNameGUI()), + Utility::escape(_account->url().toString()), + Utility::escape(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::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. Copy the login link and open it in 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... (%1)").arg(secondsLeft)); + break; + case Flow2Auth::statusPollNow: + setBusy(true); + setAuthStatusText(tr("Polling for authorization...")); + break; + case Flow2Auth::statusFetchToken: + setBusy(true); + setAuthStatusText(tr("Starting authorization...")); + 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...")); + + 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); + + 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.")); + +#if defined(Q_OS_WIN) || defined(Q_OS_MACOS) + setNeedsSyncOptions(!canUseVirtualFiles()); +#else + setNeedsSyncOptions(true); +#endif + + if (needsSyncOptions()) { + setCurrentStep(SyncOptionsStep); + } else { + finish(); + } +} + +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 (_syncMode == SyncEverything) { + ConfigFile cfgFile; + cfgFile.setNewBigFolderSizeLimit(_askBeforeLargeFolders, _largeFolderThresholdMb); + cfgFile.setConfirmExternalStorage(_askBeforeExternalStorage); + } + + applyAccountChanges(); + setCurrentStep(CompletedStep); + emit finished(QDialog::Accepted); +} + +void AccountWizardController::applyAccountChanges() +{ + auto manager = AccountManager::instance(); + +#ifdef BUILD_FILE_PROVIDER_MODULE + if (_syncMode == VirtualFiles) { + const auto accountState = manager->addAccount(_account); + const auto accountId = accountState->account()->userIdAtHostWithPort(); + Mac::FileProviderSettingsController::instance()->setVfsEnabledForAccount(accountId, true, false); + } else +#endif + { + manager->addAccount(_account); + } + + manager->saveAccount(_account); + _account = AccountManager::createAccount(); +} + +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; + } + + _syncMode = newSyncMode; + emit syncModeChanged(); +} + +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::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..0967bb016a895 --- /dev/null +++ b/src/gui/wizard/accountwizardcontroller.h @@ -0,0 +1,188 @@ +/* + * 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" + +class QNetworkReply; + +namespace OCC { + +class SelectiveSyncDialog; + +/** + * 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(SyncMode syncMode READ syncMode NOTIFY syncModeChanged) + 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]] SyncMode syncMode() 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 cancel(); + Q_INVOKABLE void goBack(); + Q_INVOKABLE void finish(); + Q_INVOKABLE void setSyncMode(int syncMode); + 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 syncModeChanged(); + 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 startServerCheck(const QUrl &serverUrl); + void startFlow2Auth(); + void connectToAuthenticatedAccount(const QString &url, const QString &user, const QString &appPassword); + void testOwnCloudConnect(); + void completeAuthentication(); + void applyAccountChanges(); + 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 setNeedsSyncOptions(bool needsSyncOptions); + void setServerUrlEditable(bool editable); + [[nodiscard]] bool checkDowngradeAdvised(QNetworkReply *reply) const; + + AccountPtr _account; + std::unique_ptr _flow2Auth; + QPointer _selectiveSyncDialog; + Step _currentStep = ServerStep; + QString _serverUrl; + bool _serverUrlEditable = true; + bool _busy = false; + QString _errorText; + QUrl _loginUrl; + QString _authStatusText; + QString _userDisplayName; + QString _serverDisplayName; + QString _avatarUrl; + SyncMode _syncMode = SyncEverything; + bool _needsSyncOptions = false; + bool _askBeforeLargeFolders = true; + int _largeFolderThresholdMb = 500; + bool _askBeforeExternalStorage = true; + QString _remoteFolder; + QStringList _selectiveSyncBlacklist; +}; + +} // 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..07dcae7e9dc87 --- /dev/null +++ b/src/gui/wizard/qml/AccountWizardWindow.qml @@ -0,0 +1,160 @@ +/* + * 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: 560 + height: 512 + minimumWidth: 520 + minimumHeight: 480 + title: qsTr("Add %1 account").arg(controller ? controller.appName : "") + visible: true + + onClosing: function(close) { + if (!controllerFinished && controller) { + 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 + anchors.margins: 12 + + 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 + text: qsTr("Back") + onClicked: root.controller.goBack() + }, + + WizardButton { + visible: root.controller && root.controller.currentStep === AccountWizardController.SyncOptionsStep + enabled: root.controller && !root.controller.busy + text: qsTr("Advanced") + onClicked: root.controller.openAdvancedOptions() + }, + + Item { + Layout.fillWidth: true + }, + + WizardButton { + visible: root.controller && root.controller.currentStep === AccountWizardController.ServerStep + enabled: root.controller && !root.controller.busy + text: qsTr("Cancel") + onClicked: root.controller.cancel() + }, + + WizardButton { + visible: root.controller && root.controller.currentStep === AccountWizardController.BrowserAuthStep + enabled: root.controller && !root.controller.busy && root.controller.loginUrl.toString() !== "" + text: qsTr("Copy link") + onClicked: root.controller.copyLoginLink() + }, + + WizardButton { + primary: true + enabled: root.controller && !root.controller.busy + text: { + if (!root.controller) { + return "" + } + switch (root.controller.currentStep) { + case AccountWizardController.BrowserAuthStep: + return qsTr("Open browser") + case AccountWizardController.SyncOptionsStep: + return qsTr("Done") + default: + return qsTr("Log in") + } + } + 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..d1ef2dfecaa10 --- /dev/null +++ b/src/gui/wizard/qml/AdvancedOptionsDialog.qml @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +Dialog { + id: root + + required property var controller + + title: qsTr("Advanced options") + modal: true + standardButtons: Dialog.Ok + width: 360 + + ColumnLayout { + anchors.fill: parent + spacing: 14 + + CheckBox { + text: qsTr("Ask before syncing folders larger than") + checked: root.controller.askBeforeLargeFolders + onToggled: root.controller.setAskBeforeLargeFolders(checked) + Layout.fillWidth: true + } + + SpinBox { + from: 0 + to: 1048576 + value: root.controller.largeFolderThresholdMb + enabled: root.controller.askBeforeLargeFolders + editable: true + 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 + onToggled: root.controller.setAskBeforeExternalStorage(checked) + Layout.fillWidth: true + } + } +} diff --git a/src/gui/wizard/qml/BrowserAuthPage.qml b/src/gui/wizard/qml/BrowserAuthPage.qml new file mode 100644 index 0000000000000..5cb111efba49b --- /dev/null +++ b/src/gui/wizard/qml/BrowserAuthPage.qml @@ -0,0 +1,88 @@ +/* + * 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 + +Item { + id: root + + required property var controller + + ColumnLayout { + anchors.fill: parent + anchors.margins: 40 + spacing: 22 + + Item { + Layout.fillHeight: true + Layout.minimumHeight: 8 + } + + Rectangle { + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: 112 + Layout.preferredHeight: 112 + radius: 56 + color: Qt.rgba(0, 0.51, 0.79, Style.darkMode ? 0.24 : 0.10) + + Image { + anchors.centerIn: parent + source: "image://svgimage-custom-color/external.svg/" + Style.ncBlue + sourceSize.width: 56 + sourceSize.height: 56 + fillMode: Image.PreserveAspectFit + } + } + + Label { + text: qsTr("Log in using your browser") + font.pixelSize: Style.pixelSize + 8 + font.bold: true + horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + wrapMode: Text.WordWrap + } + + Label { + text: qsTr("A browser window opened so you can grant access to your account.") + color: palette.mid + horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + wrapMode: Text.WordWrap + } + + Label { + visible: root.controller.authStatusText !== "" + text: root.controller.authStatusText + color: palette.mid + horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + wrapMode: Text.WordWrap + } + + Label { + visible: root.controller.errorText !== "" + text: root.controller.errorText + color: Style.errorBoxBackgroundColor + horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + wrapMode: Text.WordWrap + } + + BusyIndicator { + running: root.controller.busy + visible: running + Layout.alignment: Qt.AlignHCenter + } + + Item { + Layout.fillHeight: true + Layout.minimumHeight: 8 + } + } +} diff --git a/src/gui/wizard/qml/OptionRow.qml b/src/gui/wizard/qml/OptionRow.qml new file mode 100644 index 0000000000000..bedba85d58392 --- /dev/null +++ b/src/gui/wizard/qml/OptionRow.qml @@ -0,0 +1,70 @@ +/* + * 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 + +Control { + id: root + + property alias title: titleLabel.text + property alias description: descriptionLabel.text + property string iconSource: "" + property bool selected: false + + signal clicked() + + implicitHeight: 72 + padding: 14 + + background: Rectangle { + radius: 8 + color: root.selected ? Qt.rgba(0, 0.51, 0.79, Style.darkMode ? 0.24 : 0.10) : root.palette.base + border.width: root.selected ? 2 : 1 + border.color: root.selected ? Style.ncBlue : root.palette.mid + } + + contentItem: RowLayout { + spacing: 12 + + Image { + source: root.iconSource + sourceSize.width: 24 + sourceSize.height: 24 + Layout.preferredWidth: 28 + Layout.preferredHeight: 28 + fillMode: Image.PreserveAspectFit + } + + ColumnLayout { + spacing: 2 + Layout.fillWidth: true + + Label { + id: titleLabel + Layout.fillWidth: true + font.bold: true + elide: Text.ElideRight + } + + Label { + id: descriptionLabel + Layout.fillWidth: true + color: palette.mid + 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..5a11fdc6b37af --- /dev/null +++ b/src/gui/wizard/qml/ServerPage.qml @@ -0,0 +1,95 @@ +/* + * 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 + +Item { + id: root + + required property var controller + + ColumnLayout { + anchors.fill: parent + anchors.margins: 40 + spacing: 22 + + Item { + Layout.fillHeight: true + Layout.minimumHeight: 8 + } + + Image { + source: "image://svgimage-custom-color/wizard-nextcloud.svg/" + Style.ncBlue + sourceSize.width: 52 + sourceSize.height: 52 + Layout.alignment: Qt.AlignHCenter + fillMode: Image.PreserveAspectFit + } + + Label { + text: qsTr("Log in to %1").arg(root.controller.appName) + font.pixelSize: Style.pixelSize + 8 + font.bold: true + horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + wrapMode: Text.WordWrap + } + + Label { + text: qsTr("Enter the address of your server.") + color: palette.mid + horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + wrapMode: Text.WordWrap + } + + TextField { + id: serverUrlField + Layout.fillWidth: true + Layout.maximumWidth: 420 + Layout.alignment: Qt.AlignHCenter + text: root.controller.serverUrl + enabled: root.controller.serverUrlEditable && !root.controller.busy + placeholderText: root.controller.serverUrlPlaceholder + inputMethodHints: Qt.ImhUrlCharactersOnly | Qt.ImhNoAutoUppercase + selectByMouse: true + onTextEdited: root.controller.serverUrl = text + onAccepted: root.controller.submitServerUrl() + } + + Label { + visible: root.controller.errorText !== "" + text: root.controller.errorText + color: Style.errorBoxBackgroundColor + textFormat: Text.RichText + horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + wrapMode: Text.WordWrap + } + + Label { + visible: root.controller.busy && root.controller.authStatusText !== "" + text: root.controller.authStatusText + color: palette.mid + horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + } + + BusyIndicator { + running: root.controller.busy + visible: running + Layout.alignment: Qt.AlignHCenter + } + + Item { + Layout.fillHeight: true + Layout.minimumHeight: 8 + } + } +} diff --git a/src/gui/wizard/qml/SyncOptionsPage.qml b/src/gui/wizard/qml/SyncOptionsPage.qml new file mode 100644 index 0000000000000..911c342f3c9c5 --- /dev/null +++ b/src/gui/wizard/qml/SyncOptionsPage.qml @@ -0,0 +1,70 @@ +/* + * 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 + +Item { + id: root + + required property var controller + + ColumnLayout { + anchors.fill: parent + anchors.margins: 32 + spacing: 16 + + Label { + text: qsTr("Choose what to sync") + font.pixelSize: Style.pixelSize + 8 + font.bold: true + Layout.fillWidth: true + } + + Label { + text: root.controller.userDisplayName !== "" + ? qsTr("Connected as %1.").arg(root.controller.userDisplayName) + : qsTr("Your account is connected.") + color: palette.mid + Layout.fillWidth: true + wrapMode: Text.WordWrap + } + + OptionRow { + Layout.fillWidth: true + title: qsTr("Sync all files") + description: qsTr("Download files to this device.") + iconSource: "image://svgimage-custom-color/folder.svg/" + palette.windowText + selected: root.controller.syncMode === AccountWizardController.SyncEverything + onClicked: root.controller.setSyncMode(AccountWizardController.SyncEverything) + } + + OptionRow { + Layout.fillWidth: true + title: qsTr("Choose folders") + description: qsTr("Select folders before the first sync.") + iconSource: "image://svgimage-custom-color/sync.svg/" + palette.windowText + selected: root.controller.syncMode === AccountWizardController.SelectiveSync + onClicked: root.controller.openSelectiveSync() + } + + OptionRow { + Layout.fillWidth: true + visible: root.controller.canUseVirtualFiles + title: qsTr("Use virtual files") + description: qsTr("Keep files online until opened.") + iconSource: "image://svgimage-custom-color/wizard-files.svg/" + palette.windowText + selected: root.controller.syncMode === AccountWizardController.VirtualFiles + onClicked: root.controller.setSyncMode(AccountWizardController.VirtualFiles) + } + + 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..b05718d4c882e --- /dev/null +++ b/src/gui/wizard/qml/WizardButton.qml @@ -0,0 +1,44 @@ +/* + * 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 + + implicitHeight: 40 + leftPadding: 18 + rightPadding: 18 + + contentItem: Text { + text: root.text + font: root.font + color: root.enabled + ? (root.primary ? "white" : root.palette.buttonText) + : root.palette.mid + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + background: Rectangle { + radius: 8 + color: { + if (!root.enabled) { + return root.palette.button + } + if (root.primary) { + return root.down ? Qt.darker(Style.ncBlue, 1.25) : Style.ncBlue + } + return root.down ? root.palette.midlight : root.palette.alternateBase + } + border.width: root.primary ? 0 : 1 + border.color: root.palette.mid + } +} diff --git a/src/gui/wizard/qml/WizardDialogFrame.qml b/src/gui/wizard/qml/WizardDialogFrame.qml new file mode 100644 index 0000000000000..d4c467ccdb009 --- /dev/null +++ b/src/gui/wizard/qml/WizardDialogFrame.qml @@ -0,0 +1,56 @@ +/* + * 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 + + padding: 0 + + background: Rectangle { + radius: 12 + color: palette.window + border.width: 1 + border.color: palette.mid + } + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Item { + id: body + Layout.fillWidth: true + Layout.fillHeight: true + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 72 + Rectangle { + anchors.top: parent.top + width: parent.width + height: 1 + color: palette.mid + } + + RowLayout { + id: footerLayout + anchors.fill: parent + anchors.leftMargin: 32 + anchors.rightMargin: 32 + anchors.topMargin: 16 + anchors.bottomMargin: 16 + spacing: 12 + } + } + } +} 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" From d0f1da3a671015d948950849421df88e5a0a0c18 Mon Sep 17 00:00:00 2001 From: Iva Horn Date: Tue, 12 May 2026 15:55:21 +0200 Subject: [PATCH 02/14] fix: Build failed due to unnecessary arguments in an initializer. Signed-off-by: Iva Horn --- src/gui/wizard/accountwizardcontroller.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/wizard/accountwizardcontroller.cpp b/src/gui/wizard/accountwizardcontroller.cpp index 64896dd1dd1d8..a5e4b4850acb9 100644 --- a/src/gui/wizard/accountwizardcontroller.cpp +++ b/src/gui/wizard/accountwizardcontroller.cpp @@ -462,7 +462,7 @@ void AccountWizardController::connectToAuthenticatedAccount(const QString &url, setErrorText({}); setAuthStatusText(tr("Checking account access...")); - auto *credentials = new WebFlowCredentials(user, appPassword, {}, {}, {}); + auto *credentials = new WebFlowCredentials(user, appPassword); _account->setCredentials(credentials); credentials->persist(); From 336c1dde1e29472403fdd04b02bcf12d401d153d Mon Sep 17 00:00:00 2001 From: Rello Date: Tue, 12 May 2026 16:32:55 +0200 Subject: [PATCH 03/14] feature: New Account wizard with QML implementation Signed-off-by: Rello --- src/gui/wizard/accountwizardcontroller.cpp | 55 +++++++++- src/gui/wizard/accountwizardcontroller.h | 6 + src/gui/wizard/qml/AccountWizardWindow.qml | 49 +++++++-- src/gui/wizard/qml/AdvancedOptionsDialog.qml | 36 +++++- src/gui/wizard/qml/BrowserAuthPage.qml | 69 ++++++------ src/gui/wizard/qml/OptionRow.qml | 6 +- src/gui/wizard/qml/ServerPage.qml | 109 ++++++++++++------- src/gui/wizard/qml/SyncOptionsPage.qml | 7 +- src/gui/wizard/qml/WizardButton.qml | 8 +- src/gui/wizard/qml/WizardDialogFrame.qml | 23 ++-- 10 files changed, 253 insertions(+), 115 deletions(-) diff --git a/src/gui/wizard/accountwizardcontroller.cpp b/src/gui/wizard/accountwizardcontroller.cpp index 64896dd1dd1d8..45a67c9fd07fc 100644 --- a/src/gui/wizard/accountwizardcontroller.cpp +++ b/src/gui/wizard/accountwizardcontroller.cpp @@ -14,6 +14,7 @@ #include "creds/credentialsfactory.h" #include "creds/webflowcredentials.h" #include "folderman.h" +#include "guiutility.h" #include "networkjobs.h" #include "owncloudpropagator_p.h" #include "selectivesyncdialog.h" @@ -77,7 +78,9 @@ void AccountWizardController::initialiseAccount() _remoteFolder = Theme::instance()->defaultServerFolder(); setServerUrl(defaultUrl); - setServerUrlEditable(!Theme::instance()->forceOverrideServerUrl() || Theme::instance()->multipleOverrideServers()); + const auto hasForcedConcreteServerUrl = + Theme::instance()->forceOverrideServerUrl() && !defaultUrl.isEmpty() && !Theme::instance()->multipleOverrideServers(); + setServerUrlEditable(!hasForcedConcreteServerUrl); } AccountWizardController::Step AccountWizardController::currentStep() const @@ -233,7 +236,27 @@ void AccountWizardController::startServerCheck(const QUrl &serverUrl) { _account->setUrl(serverUrl); _account->setCredentials(CredentialsFactory::create("dummy")); - _account->setProxyType(QNetworkProxy::DefaultProxy); + _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(); @@ -407,6 +430,34 @@ void AccountWizardController::copyLoginLink() } } +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) { diff --git a/src/gui/wizard/accountwizardcontroller.h b/src/gui/wizard/accountwizardcontroller.h index 0967bb016a895..eddd397234bc9 100644 --- a/src/gui/wizard/accountwizardcontroller.h +++ b/src/gui/wizard/accountwizardcontroller.h @@ -16,6 +16,7 @@ #include "accountfwd.h" #include "creds/flow2auth.h" #include "networkjobs.h" +#include "wizard/wizardproxysettingsdialog.h" class QNetworkReply; @@ -98,6 +99,9 @@ class AccountWizardController : public QObject 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(); @@ -164,6 +168,7 @@ private slots: AccountPtr _account; std::unique_ptr _flow2Auth; QPointer _selectiveSyncDialog; + QPointer _proxySettingsDialog; Step _currentStep = ServerStep; QString _serverUrl; bool _serverUrlEditable = true; @@ -181,6 +186,7 @@ private slots: bool _askBeforeExternalStorage = true; QString _remoteFolder; QStringList _selectiveSyncBlacklist; + WizardProxySettingsDialog::WizardProxySettings _proxySettings; }; } // namespace OCC diff --git a/src/gui/wizard/qml/AccountWizardWindow.qml b/src/gui/wizard/qml/AccountWizardWindow.qml index 07dcae7e9dc87..cf10f592fb6f2 100644 --- a/src/gui/wizard/qml/AccountWizardWindow.qml +++ b/src/gui/wizard/qml/AccountWizardWindow.qml @@ -18,12 +18,19 @@ ApplicationWindow { LayoutMirroring.enabled: Application.layoutDirection === Qt.RightToLeft LayoutMirroring.childrenInherit: true - width: 560 - height: 512 - minimumWidth: 520 - minimumHeight: 480 + width: 500 + height: 448 + minimumWidth: 480 + minimumHeight: 420 title: qsTr("Add %1 account").arg(controller ? controller.appName : "") visible: true + flags: Qt.Dialog | Qt.FramelessWindowHint + color: "transparent" + + background: Rectangle { + radius: 14 + color: palette.window + } onClosing: function(close) { if (!controllerFinished && controller) { @@ -31,6 +38,15 @@ ApplicationWindow { } } + Shortcut { + sequence: StandardKey.Cancel + onActivated: { + if (root.controller) { + root.controller.cancel() + } + } + } + Connections { target: controller @@ -53,7 +69,6 @@ ApplicationWindow { WizardDialogFrame { anchors.fill: parent - anchors.margins: 12 Loader { anchors.fill: parent @@ -87,15 +102,30 @@ ApplicationWindow { onClicked: root.controller.openAdvancedOptions() }, - Item { - Layout.fillWidth: true + WizardButton { + visible: root.controller && root.controller.currentStep === AccountWizardController.ServerStep + enabled: root.controller && !root.controller.busy + text: qsTr("Sign up") + onClicked: root.controller.openSignup() }, WizardButton { visible: root.controller && root.controller.currentStep === AccountWizardController.ServerStep enabled: root.controller && !root.controller.busy - text: qsTr("Cancel") - onClicked: root.controller.cancel() + text: qsTr("Self-host") + 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") + onClicked: root.controller.openProxySettings() + }, + + Item { + Layout.fillWidth: true }, WizardButton { @@ -106,6 +136,7 @@ ApplicationWindow { }, WizardButton { + visible: root.controller && root.controller.currentStep !== AccountWizardController.ServerStep primary: true enabled: root.controller && !root.controller.busy text: { diff --git a/src/gui/wizard/qml/AdvancedOptionsDialog.qml b/src/gui/wizard/qml/AdvancedOptionsDialog.qml index d1ef2dfecaa10..657c3593761c1 100644 --- a/src/gui/wizard/qml/AdvancedOptionsDialog.qml +++ b/src/gui/wizard/qml/AdvancedOptionsDialog.qml @@ -6,21 +6,34 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Style Dialog { id: root required property var controller - title: qsTr("Advanced options") modal: true - standardButtons: Dialog.Ok width: 360 + padding: 22 + header: null + footer: null - ColumnLayout { - anchors.fill: parent + background: Rectangle { + radius: 12 + color: palette.window + } + + contentItem: ColumnLayout { spacing: 14 + Label { + text: qsTr("Advanced options") + font.pixelSize: Style.pixelSize + 4 + font.bold: true + Layout.fillWidth: true + } + CheckBox { text: qsTr("Ask before syncing folders larger than") checked: root.controller.askBeforeLargeFolders @@ -46,5 +59,20 @@ Dialog { 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 index 5cb111efba49b..588249143132b 100644 --- a/src/gui/wizard/qml/BrowserAuthPage.qml +++ b/src/gui/wizard/qml/BrowserAuthPage.qml @@ -15,52 +15,44 @@ Item { ColumnLayout { anchors.fill: parent - anchors.margins: 40 - spacing: 22 - - Item { - Layout.fillHeight: true - Layout.minimumHeight: 8 - } - - Rectangle { - Layout.alignment: Qt.AlignHCenter - Layout.preferredWidth: 112 - Layout.preferredHeight: 112 - radius: 56 - color: Qt.rgba(0, 0.51, 0.79, Style.darkMode ? 0.24 : 0.10) - - Image { - anchors.centerIn: parent - source: "image://svgimage-custom-color/external.svg/" + Style.ncBlue - sourceSize.width: 56 - sourceSize.height: 56 - fillMode: Image.PreserveAspectFit - } - } + anchors.leftMargin: 36 + anchors.rightMargin: 36 + anchors.topMargin: 38 + anchors.bottomMargin: 12 + spacing: 12 Label { text: qsTr("Log in using your browser") - font.pixelSize: Style.pixelSize + 8 + font.pixelSize: Style.pixelSize + 6 font.bold: true - horizontalAlignment: Text.AlignHCenter Layout.fillWidth: true wrapMode: Text.WordWrap } Label { - text: qsTr("A browser window opened so you can grant access to your account.") + text: qsTr("Authorize this device in the browser window that opened.") color: palette.mid - horizontalAlignment: Text.AlignHCenter Layout.fillWidth: true wrapMode: Text.WordWrap } + Item { + Layout.fillWidth: true + Layout.preferredHeight: 126 + + Image { + anchors.centerIn: parent + source: "image://svgimage-custom-color/external.svg/" + Style.ncBlue + sourceSize.width: 82 + sourceSize.height: 82 + fillMode: Image.PreserveAspectFit + } + } + Label { visible: root.controller.authStatusText !== "" text: root.controller.authStatusText color: palette.mid - horizontalAlignment: Text.AlignHCenter Layout.fillWidth: true wrapMode: Text.WordWrap } @@ -69,20 +61,29 @@ Item { visible: root.controller.errorText !== "" text: root.controller.errorText color: Style.errorBoxBackgroundColor - horizontalAlignment: Text.AlignHCenter Layout.fillWidth: true wrapMode: Text.WordWrap } - BusyIndicator { - running: root.controller.busy - visible: running - Layout.alignment: Qt.AlignHCenter + RowLayout { + visible: root.controller.busy + Layout.fillWidth: true + spacing: 8 + + BusyIndicator { + running: root.controller.busy + visible: running + Layout.preferredWidth: 20 + Layout.preferredHeight: 20 + } + + Item { + Layout.fillWidth: true + } } Item { Layout.fillHeight: true - Layout.minimumHeight: 8 } } } diff --git a/src/gui/wizard/qml/OptionRow.qml b/src/gui/wizard/qml/OptionRow.qml index bedba85d58392..df2e25be91209 100644 --- a/src/gui/wizard/qml/OptionRow.qml +++ b/src/gui/wizard/qml/OptionRow.qml @@ -23,9 +23,9 @@ Control { background: Rectangle { radius: 8 - color: root.selected ? Qt.rgba(0, 0.51, 0.79, Style.darkMode ? 0.24 : 0.10) : root.palette.base - border.width: root.selected ? 2 : 1 - border.color: root.selected ? Style.ncBlue : root.palette.mid + color: root.selected + ? Style.infoBoxBackgroundColor + : (Style.darkMode ? Qt.rgba(1, 1, 1, 0.05) : Qt.rgba(0, 0, 0, 0.035)) } contentItem: RowLayout { diff --git a/src/gui/wizard/qml/ServerPage.qml b/src/gui/wizard/qml/ServerPage.qml index 5a11fdc6b37af..1510acceae07e 100644 --- a/src/gui/wizard/qml/ServerPage.qml +++ b/src/gui/wizard/qml/ServerPage.qml @@ -16,51 +16,74 @@ Item { ColumnLayout { anchors.fill: parent - anchors.margins: 40 - spacing: 22 - - Item { - Layout.fillHeight: true - Layout.minimumHeight: 8 - } - - Image { - source: "image://svgimage-custom-color/wizard-nextcloud.svg/" + Style.ncBlue - sourceSize.width: 52 - sourceSize.height: 52 - Layout.alignment: Qt.AlignHCenter - fillMode: Image.PreserveAspectFit - } + anchors.leftMargin: 36 + anchors.rightMargin: 36 + anchors.topMargin: 38 + anchors.bottomMargin: 12 + spacing: 10 Label { text: qsTr("Log in to %1").arg(root.controller.appName) - font.pixelSize: Style.pixelSize + 8 + font.pixelSize: Style.pixelSize + 6 font.bold: true - horizontalAlignment: Text.AlignHCenter Layout.fillWidth: true wrapMode: Text.WordWrap } Label { - text: qsTr("Enter the address of your server.") + 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: palette.mid - horizontalAlignment: Text.AlignHCenter Layout.fillWidth: true wrapMode: Text.WordWrap } - TextField { - id: serverUrlField + Item { Layout.fillWidth: true - Layout.maximumWidth: 420 - Layout.alignment: Qt.AlignHCenter - text: root.controller.serverUrl - enabled: root.controller.serverUrlEditable && !root.controller.busy - placeholderText: root.controller.serverUrlPlaceholder - inputMethodHints: Qt.ImhUrlCharactersOnly | Qt.ImhNoAutoUppercase - selectByMouse: true - onTextEdited: root.controller.serverUrl = text - onAccepted: root.controller.submitServerUrl() + Layout.preferredHeight: 64 + + RowLayout { + anchors.fill: parent + anchors.topMargin: 6 + spacing: 6 + + TextField { + id: serverUrlField + Layout.fillWidth: true + Layout.preferredHeight: 38 + 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: 72 + 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: palette.window + + Label { + id: serverAddressLabel + anchors.centerIn: parent + text: qsTr("Server address") + color: palette.mid + font.pixelSize: Math.max(Style.pixelSize - 2, 10) + } + } } Label { @@ -68,28 +91,32 @@ Item { text: root.controller.errorText color: Style.errorBoxBackgroundColor textFormat: Text.RichText - horizontalAlignment: Text.AlignHCenter Layout.fillWidth: true wrapMode: Text.WordWrap } - Label { + RowLayout { visible: root.controller.busy && root.controller.authStatusText !== "" - text: root.controller.authStatusText - color: palette.mid - horizontalAlignment: Text.AlignHCenter Layout.fillWidth: true - } + spacing: 8 + + BusyIndicator { + running: root.controller.busy + visible: running + Layout.preferredWidth: 20 + Layout.preferredHeight: 20 + } - BusyIndicator { - running: root.controller.busy - visible: running - Layout.alignment: Qt.AlignHCenter + Label { + text: root.controller.authStatusText + color: palette.mid + Layout.fillWidth: true + wrapMode: Text.WordWrap + } } Item { Layout.fillHeight: true - Layout.minimumHeight: 8 } } } diff --git a/src/gui/wizard/qml/SyncOptionsPage.qml b/src/gui/wizard/qml/SyncOptionsPage.qml index 911c342f3c9c5..19dbf0bc631b9 100644 --- a/src/gui/wizard/qml/SyncOptionsPage.qml +++ b/src/gui/wizard/qml/SyncOptionsPage.qml @@ -16,12 +16,15 @@ Item { ColumnLayout { anchors.fill: parent - anchors.margins: 32 + anchors.leftMargin: 36 + anchors.rightMargin: 36 + anchors.topMargin: 38 + anchors.bottomMargin: 12 spacing: 16 Label { text: qsTr("Choose what to sync") - font.pixelSize: Style.pixelSize + 8 + font.pixelSize: Style.pixelSize + 6 font.bold: true Layout.fillWidth: true } diff --git a/src/gui/wizard/qml/WizardButton.qml b/src/gui/wizard/qml/WizardButton.qml index b05718d4c882e..a8d7e6e67ebf8 100644 --- a/src/gui/wizard/qml/WizardButton.qml +++ b/src/gui/wizard/qml/WizardButton.qml @@ -12,7 +12,7 @@ Button { property bool primary: false - implicitHeight: 40 + implicitHeight: 36 leftPadding: 18 rightPadding: 18 @@ -36,9 +36,9 @@ Button { if (root.primary) { return root.down ? Qt.darker(Style.ncBlue, 1.25) : Style.ncBlue } - return root.down ? root.palette.midlight : root.palette.alternateBase + return root.down + ? Qt.darker(Style.infoBoxBackgroundColor, 1.12) + : Style.infoBoxBackgroundColor } - border.width: root.primary ? 0 : 1 - border.color: root.palette.mid } } diff --git a/src/gui/wizard/qml/WizardDialogFrame.qml b/src/gui/wizard/qml/WizardDialogFrame.qml index d4c467ccdb009..7bdfcd9e1eb59 100644 --- a/src/gui/wizard/qml/WizardDialogFrame.qml +++ b/src/gui/wizard/qml/WizardDialogFrame.qml @@ -16,10 +16,7 @@ Pane { padding: 0 background: Rectangle { - radius: 12 - color: palette.window - border.width: 1 - border.color: palette.mid + color: "transparent" } ColumnLayout { @@ -34,22 +31,16 @@ Pane { Item { Layout.fillWidth: true - Layout.preferredHeight: 72 - Rectangle { - anchors.top: parent.top - width: parent.width - height: 1 - color: palette.mid - } + Layout.preferredHeight: 68 RowLayout { id: footerLayout anchors.fill: parent - anchors.leftMargin: 32 - anchors.rightMargin: 32 - anchors.topMargin: 16 - anchors.bottomMargin: 16 - spacing: 12 + anchors.leftMargin: 24 + anchors.rightMargin: 24 + anchors.topMargin: 10 + anchors.bottomMargin: 8 + spacing: 6 } } } From c955edc2eaee358483070ca535aaa977ca039719 Mon Sep 17 00:00:00 2001 From: Rello Date: Thu, 14 May 2026 09:52:42 +0200 Subject: [PATCH 04/14] fix: layout Signed-off-by: Rello --- src/gui/owncloudsetupwizard.cpp | 5 +++++ src/gui/wizard/qml/AccountWizardWindow.qml | 6 ++---- src/gui/wizard/qml/AdvancedOptionsDialog.qml | 2 +- src/gui/wizard/qml/ServerPage.qml | 4 ++-- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/gui/owncloudsetupwizard.cpp b/src/gui/owncloudsetupwizard.cpp index 1879e950dea5a..d63cfc06a88d6 100644 --- a/src/gui/owncloudsetupwizard.cpp +++ b/src/gui/owncloudsetupwizard.cpp @@ -201,6 +201,11 @@ bool OwncloudSetupWizard::startQmlWizard() return false; } +#if defined(Q_OS_MACOS) && QT_VERSION >= QT_VERSION_CHECK(6, 9, 0) + _qmlWizardWindow->setFlag(Qt::ExpandedClientAreaHint, true); + _qmlWizardWindow->setFlag(Qt::NoTitleBarBackgroundHint, true); +#endif + connect(_qmlController, &AccountWizardController::finished, this, [this](int result) { emit ownCloudWizardDone(result); if (_qmlWizardWindow) { diff --git a/src/gui/wizard/qml/AccountWizardWindow.qml b/src/gui/wizard/qml/AccountWizardWindow.qml index cf10f592fb6f2..4dd7606df144c 100644 --- a/src/gui/wizard/qml/AccountWizardWindow.qml +++ b/src/gui/wizard/qml/AccountWizardWindow.qml @@ -24,12 +24,10 @@ ApplicationWindow { minimumHeight: 420 title: qsTr("Add %1 account").arg(controller ? controller.appName : "") visible: true - flags: Qt.Dialog | Qt.FramelessWindowHint - color: "transparent" + color: "white" background: Rectangle { - radius: 14 - color: palette.window + color: "white" } onClosing: function(close) { diff --git a/src/gui/wizard/qml/AdvancedOptionsDialog.qml b/src/gui/wizard/qml/AdvancedOptionsDialog.qml index 657c3593761c1..6b388ca0caea3 100644 --- a/src/gui/wizard/qml/AdvancedOptionsDialog.qml +++ b/src/gui/wizard/qml/AdvancedOptionsDialog.qml @@ -21,7 +21,7 @@ Dialog { background: Rectangle { radius: 12 - color: palette.window + color: "white" } contentItem: ColumnLayout { diff --git a/src/gui/wizard/qml/ServerPage.qml b/src/gui/wizard/qml/ServerPage.qml index 1510acceae07e..334ddde10f948 100644 --- a/src/gui/wizard/qml/ServerPage.qml +++ b/src/gui/wizard/qml/ServerPage.qml @@ -62,7 +62,7 @@ Item { WizardButton { primary: true - Layout.preferredWidth: 72 + Layout.preferredWidth: 88 enabled: !root.controller.busy text: qsTr("Log in") onClicked: root.controller.submitServerUrl() @@ -74,7 +74,7 @@ Item { y: 0 width: serverAddressLabel.implicitWidth + 8 height: serverAddressLabel.implicitHeight - color: palette.window + color: "white" Label { id: serverAddressLabel From e7c0dc91859a761849e1a20a335e7baa2cb4dd31 Mon Sep 17 00:00:00 2001 From: Rello Date: Thu, 14 May 2026 13:32:34 +0200 Subject: [PATCH 05/14] fix: font sizes and input fields Signed-off-by: Rello --- resources.qrc | 1 + src/gui/wizard/qml/AccountWizardWindow.qml | 34 ++++- src/gui/wizard/qml/AdvancedOptionsDialog.qml | 5 +- src/gui/wizard/qml/BrowserAuthPage.qml | 69 +++++------ src/gui/wizard/qml/OptionRow.qml | 2 + src/gui/wizard/qml/ServerPage.qml | 10 +- src/gui/wizard/qml/SyncOptionsPage.qml | 3 +- src/gui/wizard/qml/WizardButton.qml | 2 + src/gui/wizard/qml/WizardTextField.qml | 31 +++++ src/gui/wizard/wizardproxysettingsdialog.cpp | 123 +++++++++++++++++-- src/gui/wizard/wizardproxysettingsdialog.h | 12 ++ 11 files changed, 234 insertions(+), 58 deletions(-) create mode 100644 src/gui/wizard/qml/WizardTextField.qml diff --git a/resources.qrc b/resources.qrc index 02cc1abae64d7..057feb7f1d695 100644 --- a/resources.qrc +++ b/resources.qrc @@ -67,6 +67,7 @@ src/gui/wizard/qml/SyncOptionsPage.qml src/gui/wizard/qml/WizardButton.qml src/gui/wizard/qml/WizardDialogFrame.qml + src/gui/wizard/qml/WizardTextField.qml src/gui/macOS/ui/FileProviderSettings.qml src/gui/macOS/ui/FileProviderFileDelegate.qml src/gui/integration/FileActionsWindow.qml diff --git a/src/gui/wizard/qml/AccountWizardWindow.qml b/src/gui/wizard/qml/AccountWizardWindow.qml index 4dd7606df144c..092edbf8e29a9 100644 --- a/src/gui/wizard/qml/AccountWizardWindow.qml +++ b/src/gui/wizard/qml/AccountWizardWindow.qml @@ -8,6 +8,7 @@ import QtQuick.Controls import QtQuick.Layouts import QtQuick.Window import com.nextcloud.desktopclient +import Style ApplicationWindow { id: root @@ -45,6 +46,17 @@ ApplicationWindow { } } + MouseArea { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: Qt.platform.os === "osx" ? 72 : 0 + height: 32 + z: 10 + acceptedButtons: Qt.LeftButton + onPressed: root.startSystemMove() + } + Connections { target: controller @@ -89,8 +101,17 @@ ApplicationWindow { WizardButton { visible: root.controller && root.controller.currentStep !== AccountWizardController.ServerStep enabled: root.controller && !root.controller.busy - text: qsTr("Back") - onClicked: root.controller.goBack() + Layout.preferredWidth: root.controller && root.controller.currentStep === AccountWizardController.BrowserAuthStep ? 146 : implicitWidth + text: root.controller && root.controller.currentStep === AccountWizardController.BrowserAuthStep + ? qsTr("Cancel") + : qsTr("Back") + onClicked: { + if (root.controller.currentStep === AccountWizardController.BrowserAuthStep) { + root.controller.cancel() + } else { + root.controller.goBack() + } + } }, WizardButton { @@ -119,17 +140,21 @@ ApplicationWindow { enabled: root.controller && !root.controller.busy flat: true text: qsTr("Proxy settings") + font.bold: true + font.pixelSize: Style.pixelSize + 1 onClicked: root.controller.openProxySettings() }, Item { - Layout.fillWidth: true + visible: root.controller && root.controller.currentStep !== AccountWizardController.BrowserAuthStep + 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") + Layout.preferredWidth: 146 onClicked: root.controller.copyLoginLink() }, @@ -137,13 +162,14 @@ ApplicationWindow { visible: root.controller && root.controller.currentStep !== AccountWizardController.ServerStep primary: true enabled: root.controller && !root.controller.busy + Layout.preferredWidth: root.controller && root.controller.currentStep === AccountWizardController.BrowserAuthStep ? 146 : implicitWidth text: { if (!root.controller) { return "" } switch (root.controller.currentStep) { case AccountWizardController.BrowserAuthStep: - return qsTr("Open browser") + return qsTr("Open") case AccountWizardController.SyncOptionsStep: return qsTr("Done") default: diff --git a/src/gui/wizard/qml/AdvancedOptionsDialog.qml b/src/gui/wizard/qml/AdvancedOptionsDialog.qml index 6b388ca0caea3..d795785a1ebdc 100644 --- a/src/gui/wizard/qml/AdvancedOptionsDialog.qml +++ b/src/gui/wizard/qml/AdvancedOptionsDialog.qml @@ -29,7 +29,7 @@ Dialog { Label { text: qsTr("Advanced options") - font.pixelSize: Style.pixelSize + 4 + font.pixelSize: Style.pixelSize + 8 font.bold: true Layout.fillWidth: true } @@ -37,6 +37,7 @@ Dialog { 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 } @@ -47,6 +48,7 @@ Dialog { 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) } @@ -56,6 +58,7 @@ Dialog { 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 } diff --git a/src/gui/wizard/qml/BrowserAuthPage.qml b/src/gui/wizard/qml/BrowserAuthPage.qml index 588249143132b..7ea0017a6653c 100644 --- a/src/gui/wizard/qml/BrowserAuthPage.qml +++ b/src/gui/wizard/qml/BrowserAuthPage.qml @@ -15,61 +15,48 @@ Item { ColumnLayout { anchors.fill: parent - anchors.leftMargin: 36 - anchors.rightMargin: 36 - anchors.topMargin: 38 - anchors.bottomMargin: 12 - spacing: 12 - - Label { - text: qsTr("Log in using your browser") - font.pixelSize: Style.pixelSize + 6 - font.bold: true - Layout.fillWidth: true - wrapMode: Text.WordWrap - } - - Label { - text: qsTr("Authorize this device in the browser window that opened.") - color: palette.mid - Layout.fillWidth: true - wrapMode: Text.WordWrap - } + anchors.margins: 36 + spacing: 14 Item { - Layout.fillWidth: true - Layout.preferredHeight: 126 + Layout.fillHeight: true + } - Image { - anchors.centerIn: parent - source: "image://svgimage-custom-color/external.svg/" + Style.ncBlue - sourceSize.width: 82 - sourceSize.height: 82 - fillMode: Image.PreserveAspectFit - } + Image { + Layout.alignment: Qt.AlignHCenter + source: "image://svgimage-custom-color/external.svg/" + Style.ncBlue + sourceSize.width: 82 + sourceSize.height: 82 + fillMode: Image.PreserveAspectFit } Label { - visible: root.controller.authStatusText !== "" - text: root.controller.authStatusText - color: palette.mid + text: qsTr("Switch to your browser") + font.pixelSize: Style.pixelSize + 8 + font.bold: true + horizontalAlignment: Text.AlignHCenter Layout.fillWidth: true wrapMode: Text.WordWrap } Label { - visible: root.controller.errorText !== "" - text: root.controller.errorText - color: Style.errorBoxBackgroundColor + text: qsTr("Authorize this device in the browser window that opened.") + color: palette.mid + font.pixelSize: Style.pixelSize + 2 + horizontalAlignment: Text.AlignHCenter Layout.fillWidth: true wrapMode: Text.WordWrap } RowLayout { - visible: root.controller.busy + visible: root.controller.busy && root.controller.authStatusText !== "" Layout.fillWidth: true spacing: 8 + Item { + Layout.fillWidth: true + } + BusyIndicator { running: root.controller.busy visible: running @@ -82,6 +69,16 @@ Item { } } + Label { + 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 index df2e25be91209..5cd93ab638e54 100644 --- a/src/gui/wizard/qml/OptionRow.qml +++ b/src/gui/wizard/qml/OptionRow.qml @@ -48,6 +48,7 @@ Control { id: titleLabel Layout.fillWidth: true font.bold: true + font.pixelSize: Style.pixelSize + 2 elide: Text.ElideRight } @@ -55,6 +56,7 @@ Control { id: descriptionLabel Layout.fillWidth: true color: palette.mid + font.pixelSize: Style.pixelSize + 1 wrapMode: Text.WordWrap maximumLineCount: 2 } diff --git a/src/gui/wizard/qml/ServerPage.qml b/src/gui/wizard/qml/ServerPage.qml index 334ddde10f948..e0266b99e01ce 100644 --- a/src/gui/wizard/qml/ServerPage.qml +++ b/src/gui/wizard/qml/ServerPage.qml @@ -24,7 +24,7 @@ Item { Label { text: qsTr("Log in to %1").arg(root.controller.appName) - font.pixelSize: Style.pixelSize + 6 + font.pixelSize: Style.pixelSize + 8 font.bold: true Layout.fillWidth: true wrapMode: Text.WordWrap @@ -33,6 +33,7 @@ Item { Label { 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: palette.mid + font.pixelSize: Style.pixelSize + 2 Layout.fillWidth: true wrapMode: Text.WordWrap } @@ -46,10 +47,9 @@ Item { anchors.topMargin: 6 spacing: 6 - TextField { + WizardTextField { id: serverUrlField Layout.fillWidth: true - Layout.preferredHeight: 38 text: root.controller.serverUrl enabled: !root.controller.busy readOnly: !root.controller.serverUrlEditable @@ -81,7 +81,7 @@ Item { anchors.centerIn: parent text: qsTr("Server address") color: palette.mid - font.pixelSize: Math.max(Style.pixelSize - 2, 10) + font.pixelSize: Style.pixelSize } } } @@ -91,6 +91,7 @@ Item { text: root.controller.errorText color: Style.errorBoxBackgroundColor textFormat: Text.RichText + font.pixelSize: Style.pixelSize + 1 Layout.fillWidth: true wrapMode: Text.WordWrap } @@ -110,6 +111,7 @@ Item { Label { text: root.controller.authStatusText color: palette.mid + font.pixelSize: Style.pixelSize + 1 Layout.fillWidth: true wrapMode: Text.WordWrap } diff --git a/src/gui/wizard/qml/SyncOptionsPage.qml b/src/gui/wizard/qml/SyncOptionsPage.qml index 19dbf0bc631b9..07a4dc2523585 100644 --- a/src/gui/wizard/qml/SyncOptionsPage.qml +++ b/src/gui/wizard/qml/SyncOptionsPage.qml @@ -24,7 +24,7 @@ Item { Label { text: qsTr("Choose what to sync") - font.pixelSize: Style.pixelSize + 6 + font.pixelSize: Style.pixelSize + 8 font.bold: true Layout.fillWidth: true } @@ -34,6 +34,7 @@ Item { ? qsTr("Connected as %1.").arg(root.controller.userDisplayName) : qsTr("Your account is connected.") color: palette.mid + font.pixelSize: Style.pixelSize + 2 Layout.fillWidth: true wrapMode: Text.WordWrap } diff --git a/src/gui/wizard/qml/WizardButton.qml b/src/gui/wizard/qml/WizardButton.qml index a8d7e6e67ebf8..684440aa72cb7 100644 --- a/src/gui/wizard/qml/WizardButton.qml +++ b/src/gui/wizard/qml/WizardButton.qml @@ -15,6 +15,8 @@ Button { implicitHeight: 36 leftPadding: 18 rightPadding: 18 + font.bold: true + font.pixelSize: Style.pixelSize + 1 contentItem: Text { text: root.text diff --git a/src/gui/wizard/qml/WizardTextField.qml b/src/gui/wizard/qml/WizardTextField.qml new file mode 100644 index 0000000000000..4d92586e82868 --- /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: 46 + 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.28) + 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..c472ab82f0c4a 100644 --- a/src/gui/wizard/wizardproxysettingsdialog.cpp +++ b/src/gui/wizard/wizardproxysettingsdialog.cpp @@ -5,22 +5,70 @@ #include "wizardproxysettingsdialog.h" -#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: 34px;" + "border: 1px solid red;" + "border-radius: 8px;" + "padding: 4px 10px;" + "background: white;" + "font-size: 15px;" +); + +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) { +#if defined(Q_OS_MACOS) && QT_VERSION >= QT_VERSION_CHECK(6, 9, 0) + setWindowFlag(Qt::ExpandedClientAreaHint, true); + setWindowFlag(Qt::NoTitleBarBackgroundHint, true); +#endif + _ui.setupUi(this); setWindowModality(Qt::WindowModal); setWindowTitle(tr("Proxy Settings", "Dialog window title for proxy settings")); + setMinimumSize(500, 448); + resize(500, 448); + + if (layout()) { + layout()->setContentsMargins(36, 38, 36, 16); + layout()->setSpacing(14); + } _ui.hostLineEdit->setPlaceholderText(tr("Hostname of proxy server")); _ui.userLineEdit->setPlaceholderText(tr("Username for proxy server")); @@ -58,6 +106,56 @@ WizardProxySettingsDialog::WizardProxySettingsDialog(QUrl serverURL, setServerUrl(std::move(serverURL)); setProxySettings(std::move(proxySettings)); + customizeStyle(); + +#if defined(Q_OS_MACOS) && QT_VERSION >= QT_VERSION_CHECK(6, 9, 0) + _windowDragHandle = new WindowDragHandle(this); + _windowDragHandle->setFixedHeight(32); + _windowDragHandle->setGeometry(0, 0, width(), _windowDragHandle->height()); + _windowDragHandle->raise(); +#endif +} + +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: 0px; padding-top: 0px; font-size: 18px; font-weight: bold; }" + "QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; padding: 0px; }" + "QRadioButton, QCheckBox, QLabel { font-size: 15px; }" + "QLineEdit, QSpinBox, QComboBox {" + " min-height: 34px;" + " border: 1px solid rgb(196, 196, 196);" + " border-radius: 8px;" + " padding: 4px 10px;" + " background: white;" + " font-size: 15px;" + " }" + "QLineEdit:focus, QSpinBox:focus, QComboBox:focus { border: 1px solid palette(highlight); }" + "QPushButton {" + " min-height: 34px;" + " border: none;" + " border-radius: 8px;" + " padding: 4px 18px;" + " background: rgb(230, 242, 249);" + " font-size: 15px;" + " font-weight: bold;" + " }" + "QPushButton:default { background: rgb(0, 130, 201); color: white; }" + "QPushButton:disabled { color: rgb(150, 150, 150); background: rgb(242, 242, 242); }" + )); } void WizardProxySettingsDialog::setServerUrl(QUrl serverUrl) @@ -119,7 +217,7 @@ void WizardProxySettingsDialog::setProxySettings(WizardProxySettings proxySettin void WizardProxySettingsDialog::checkEmptyProxyHost() { if (_ui.hostLineEdit->isEnabled() && _ui.hostLineEdit->text().isEmpty()) { - _ui.hostLineEdit->setStyleSheet("border: 1px solid red"); + _ui.hostLineEdit->setStyleSheet(invalidInputStyle); } else { _ui.hostLineEdit->setStyleSheet(QString()); } @@ -134,16 +232,16 @@ void WizardProxySettingsDialog::checkEmptyProxyCredentials() } 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()); - } + _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() @@ -196,6 +294,7 @@ void WizardProxySettingsDialog::validateProxySettings() 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..bf67fde319c31 100644 --- a/src/gui/wizard/wizardproxysettingsdialog.h +++ b/src/gui/wizard/wizardproxysettingsdialog.h @@ -10,6 +10,9 @@ #include "ui_proxysettings.h" +class QResizeEvent; +class QWidget; + namespace OCC { class WizardProxySettingsDialog : public QDialog @@ -56,7 +59,12 @@ private Q_SLOTS: void settingsDone(); +protected: + void resizeEvent(QResizeEvent *event) override; + private: + void customizeStyle(); + Ui_ProxySettings _ui{}; QUrl _serverURL; @@ -64,6 +72,10 @@ private Q_SLOTS: bool _valid = false; WizardProxySettings _settings; + +#if defined(Q_OS_MACOS) && QT_VERSION >= QT_VERSION_CHECK(6, 9, 0) + QWidget *_windowDragHandle = nullptr; +#endif }; } From 4b9351f94a5ab499bdb9009abf8b59d28182e17b Mon Sep 17 00:00:00 2001 From: Rello Date: Thu, 14 May 2026 14:42:52 +0200 Subject: [PATCH 06/14] fix: language strings and labels Signed-off-by: Rello --- src/gui/wizard/accountwizardcontroller.cpp | 22 ++++++++++---------- src/gui/wizard/qml/AdvancedOptionsDialog.qml | 3 ++- src/gui/wizard/qml/BrowserAuthPage.qml | 7 ++++--- src/gui/wizard/qml/OptionRow.qml | 5 +++-- src/gui/wizard/qml/ServerPage.qml | 12 +++++------ src/gui/wizard/qml/SyncOptionsPage.qml | 5 +++-- 6 files changed, 29 insertions(+), 25 deletions(-) diff --git a/src/gui/wizard/accountwizardcontroller.cpp b/src/gui/wizard/accountwizardcontroller.cpp index 2a7564d4c6238..e89365ae23975 100644 --- a/src/gui/wizard/accountwizardcontroller.cpp +++ b/src/gui/wizard/accountwizardcontroller.cpp @@ -262,7 +262,7 @@ void AccountWizardController::startServerCheck(const QUrl &serverUrl) setErrorText({}); setBusy(true); - setAuthStatusText(tr("Checking server address...")); + setAuthStatusText(tr("Checking server address") + QStringLiteral("…")); if (ClientProxy::isUsingSystemDefault() || _account->proxyType() == QNetworkProxy::DefaultProxy) { ClientProxy::lookupSystemProxyAsync(_account->url(), this, SLOT(slotSystemProxyLookupDone(QNetworkProxy))); @@ -327,7 +327,7 @@ void AccountWizardController::slotFoundServer(const QUrl &url, const QJsonObject } setServerDisplayName(url.host()); - setAuthStatusText(tr("Preparing browser login...")); + setAuthStatusText(tr("Preparing browser login") + QStringLiteral("…")); if (_account->isPublicShareLink()) { setBusy(false); @@ -346,10 +346,10 @@ void AccountWizardController::slotNoServerFound(QNetworkReply *reply) if (!_account->url().isValid()) { message = tr("Invalid URL"); } else { - message = tr("Failed to connect to %1 at %2:
%3") - .arg(Utility::escape(Theme::instance()->appNameGUI()), - Utility::escape(_account->url().toString()), - Utility::escape(job ? job->errorString() : QString{})); + message = tr("Failed to connect to %1 at %2:\n%3") + .arg(Theme::instance()->appNameGUI(), + _account->url().toString(), + job ? job->errorString() : QString{}); } setBusy(false); @@ -469,7 +469,7 @@ void AccountWizardController::slotFlow2AuthResult(Flow2Auth::Result result, cons { switch (result) { case Flow2Auth::NotSupported: - setErrorText(tr("Unable to open the browser. Copy the login link and open it in your browser.")); + setErrorText(tr("Unable to open the Browser, please copy the link to your Browser.")); break; case Flow2Auth::Error: setBusy(false); @@ -490,15 +490,15 @@ void AccountWizardController::slotFlow2StatusChanged(Flow2Auth::PollStatus statu switch (status) { case Flow2Auth::statusPollCountdown: setBusy(false); - setAuthStatusText(tr("Waiting for authorization... (%1)").arg(secondsLeft)); + setAuthStatusText(tr("Waiting for authorization") + QStringLiteral("… (%1)").arg(secondsLeft)); break; case Flow2Auth::statusPollNow: setBusy(true); - setAuthStatusText(tr("Polling for authorization...")); + setAuthStatusText(tr("Polling for authorization") + QStringLiteral("…")); break; case Flow2Auth::statusFetchToken: setBusy(true); - setAuthStatusText(tr("Starting authorization...")); + setAuthStatusText(tr("Starting authorization") + QStringLiteral("…")); break; case Flow2Auth::statusCopyLinkToClipboard: setBusy(false); @@ -511,7 +511,7 @@ void AccountWizardController::connectToAuthenticatedAccount(const QString &url, { setBusy(true); setErrorText({}); - setAuthStatusText(tr("Checking account access...")); + setAuthStatusText(tr("Checking account access") + QStringLiteral("…")); auto *credentials = new WebFlowCredentials(user, appPassword); _account->setCredentials(credentials); diff --git a/src/gui/wizard/qml/AdvancedOptionsDialog.qml b/src/gui/wizard/qml/AdvancedOptionsDialog.qml index d795785a1ebdc..ee7232d66d557 100644 --- a/src/gui/wizard/qml/AdvancedOptionsDialog.qml +++ b/src/gui/wizard/qml/AdvancedOptionsDialog.qml @@ -7,6 +7,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import Style +import "../../tray" Dialog { id: root @@ -27,7 +28,7 @@ Dialog { contentItem: ColumnLayout { spacing: 14 - Label { + EnforcedPlainTextLabel { text: qsTr("Advanced options") font.pixelSize: Style.pixelSize + 8 font.bold: true diff --git a/src/gui/wizard/qml/BrowserAuthPage.qml b/src/gui/wizard/qml/BrowserAuthPage.qml index 7ea0017a6653c..baf1afba6b66c 100644 --- a/src/gui/wizard/qml/BrowserAuthPage.qml +++ b/src/gui/wizard/qml/BrowserAuthPage.qml @@ -7,6 +7,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import Style +import "../../tray" Item { id: root @@ -30,7 +31,7 @@ Item { fillMode: Image.PreserveAspectFit } - Label { + EnforcedPlainTextLabel { text: qsTr("Switch to your browser") font.pixelSize: Style.pixelSize + 8 font.bold: true @@ -39,7 +40,7 @@ Item { wrapMode: Text.WordWrap } - Label { + EnforcedPlainTextLabel { text: qsTr("Authorize this device in the browser window that opened.") color: palette.mid font.pixelSize: Style.pixelSize + 2 @@ -69,7 +70,7 @@ Item { } } - Label { + EnforcedPlainTextLabel { visible: root.controller.errorText !== "" text: root.controller.errorText color: Style.errorBoxBackgroundColor diff --git a/src/gui/wizard/qml/OptionRow.qml b/src/gui/wizard/qml/OptionRow.qml index 5cd93ab638e54..bf9841ff8a400 100644 --- a/src/gui/wizard/qml/OptionRow.qml +++ b/src/gui/wizard/qml/OptionRow.qml @@ -7,6 +7,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import Style +import "../../tray" Control { id: root @@ -44,7 +45,7 @@ Control { spacing: 2 Layout.fillWidth: true - Label { + EnforcedPlainTextLabel { id: titleLabel Layout.fillWidth: true font.bold: true @@ -52,7 +53,7 @@ Control { elide: Text.ElideRight } - Label { + EnforcedPlainTextLabel { id: descriptionLabel Layout.fillWidth: true color: palette.mid diff --git a/src/gui/wizard/qml/ServerPage.qml b/src/gui/wizard/qml/ServerPage.qml index e0266b99e01ce..0359326dfbd99 100644 --- a/src/gui/wizard/qml/ServerPage.qml +++ b/src/gui/wizard/qml/ServerPage.qml @@ -8,6 +8,7 @@ import QtQuick.Controls import QtQuick.Layouts import com.nextcloud.desktopclient import Style +import "../../tray" Item { id: root @@ -22,7 +23,7 @@ Item { anchors.bottomMargin: 12 spacing: 10 - Label { + EnforcedPlainTextLabel { text: qsTr("Log in to %1").arg(root.controller.appName) font.pixelSize: Style.pixelSize + 8 font.bold: true @@ -30,7 +31,7 @@ Item { wrapMode: Text.WordWrap } - Label { + 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: palette.mid font.pixelSize: Style.pixelSize + 2 @@ -76,7 +77,7 @@ Item { height: serverAddressLabel.implicitHeight color: "white" - Label { + EnforcedPlainTextLabel { id: serverAddressLabel anchors.centerIn: parent text: qsTr("Server address") @@ -86,11 +87,10 @@ Item { } } - Label { + EnforcedPlainTextLabel { visible: root.controller.errorText !== "" text: root.controller.errorText color: Style.errorBoxBackgroundColor - textFormat: Text.RichText font.pixelSize: Style.pixelSize + 1 Layout.fillWidth: true wrapMode: Text.WordWrap @@ -108,7 +108,7 @@ Item { Layout.preferredHeight: 20 } - Label { + EnforcedPlainTextLabel { text: root.controller.authStatusText color: palette.mid font.pixelSize: Style.pixelSize + 1 diff --git a/src/gui/wizard/qml/SyncOptionsPage.qml b/src/gui/wizard/qml/SyncOptionsPage.qml index 07a4dc2523585..2e22382a633dd 100644 --- a/src/gui/wizard/qml/SyncOptionsPage.qml +++ b/src/gui/wizard/qml/SyncOptionsPage.qml @@ -8,6 +8,7 @@ import QtQuick.Controls import QtQuick.Layouts import Style import com.nextcloud.desktopclient +import "../../tray" Item { id: root @@ -22,14 +23,14 @@ Item { anchors.bottomMargin: 12 spacing: 16 - Label { + EnforcedPlainTextLabel { text: qsTr("Choose what to sync") font.pixelSize: Style.pixelSize + 8 font.bold: true Layout.fillWidth: true } - Label { + EnforcedPlainTextLabel { text: root.controller.userDisplayName !== "" ? qsTr("Connected as %1.").arg(root.controller.userDisplayName) : qsTr("Your account is connected.") From cb72c61ffdba9bd1c861c236c1e6caa43d38bc4c Mon Sep 17 00:00:00 2001 From: Rello Date: Thu, 14 May 2026 15:01:24 +0200 Subject: [PATCH 07/14] fix: testing error Signed-off-by: Rello --- src/gui/wizard/qml/AccountWizardWindow.qml | 3 +- src/gui/wizard/qml/WizardButton.qml | 41 ++++-- src/gui/wizard/wizardproxysettingsdialog.cpp | 140 +++++++++---------- src/gui/wizard/wizardproxysettingsdialog.h | 9 +- 4 files changed, 110 insertions(+), 83 deletions(-) diff --git a/src/gui/wizard/qml/AccountWizardWindow.qml b/src/gui/wizard/qml/AccountWizardWindow.qml index 092edbf8e29a9..a515049e8f7af 100644 --- a/src/gui/wizard/qml/AccountWizardWindow.qml +++ b/src/gui/wizard/qml/AccountWizardWindow.qml @@ -24,7 +24,6 @@ ApplicationWindow { minimumWidth: 480 minimumHeight: 420 title: qsTr("Add %1 account").arg(controller ? controller.appName : "") - visible: true color: "white" background: Rectangle { @@ -125,6 +124,7 @@ ApplicationWindow { visible: root.controller && root.controller.currentStep === AccountWizardController.ServerStep enabled: root.controller && !root.controller.busy text: qsTr("Sign up") + iconSource: "image://svgimage-custom-color/external.svg/" + palette.buttonText onClicked: root.controller.openSignup() }, @@ -132,6 +132,7 @@ ApplicationWindow { visible: root.controller && root.controller.currentStep === AccountWizardController.ServerStep enabled: root.controller && !root.controller.busy text: qsTr("Self-host") + iconSource: "image://svgimage-custom-color/external.svg/" + palette.buttonText onClicked: root.controller.openSelfHostedServerGuide() }, diff --git a/src/gui/wizard/qml/WizardButton.qml b/src/gui/wizard/qml/WizardButton.qml index 684440aa72cb7..e67a82d10e68d 100644 --- a/src/gui/wizard/qml/WizardButton.qml +++ b/src/gui/wizard/qml/WizardButton.qml @@ -11,6 +11,7 @@ Button { id: root property bool primary: false + property string iconSource: "" implicitHeight: 36 leftPadding: 18 @@ -18,15 +19,37 @@ Button { font.bold: true font.pixelSize: Style.pixelSize + 1 - contentItem: Text { - text: root.text - font: root.font - color: root.enabled - ? (root.primary ? "white" : root.palette.buttonText) - : root.palette.mid - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight + contentItem: Item { + implicitWidth: contentRow.implicitWidth + implicitHeight: contentRow.implicitHeight + + Row { + id: contentRow + + anchors.centerIn: parent + spacing: 6 + + Text { + text: root.text + font: root.font + color: root.enabled + ? (root.primary ? "white" : root.palette.buttonText) + : root.palette.mid + anchors.verticalCenter: parent.verticalCenter + elide: Text.ElideRight + } + + Image { + visible: root.iconSource !== "" + source: root.iconSource + sourceSize.width: 16 + sourceSize.height: 16 + width: visible ? 16 : 0 + height: 16 + anchors.verticalCenter: parent.verticalCenter + fillMode: Image.PreserveAspectFit + } + } } background: Rectangle { diff --git a/src/gui/wizard/wizardproxysettingsdialog.cpp b/src/gui/wizard/wizardproxysettingsdialog.cpp index c472ab82f0c4a..0ad5124d190c0 100644 --- a/src/gui/wizard/wizardproxysettingsdialog.cpp +++ b/src/gui/wizard/wizardproxysettingsdialog.cpp @@ -5,6 +5,8 @@ #include "wizardproxysettingsdialog.h" +#include "ui_proxysettings.h" + #include #include #include @@ -52,13 +54,14 @@ WizardProxySettingsDialog::WizardProxySettingsDialog(QUrl serverURL, WizardProxySettings proxySettings, QWidget *parent) : QDialog(parent) + , _ui(std::make_unique()) { #if defined(Q_OS_MACOS) && QT_VERSION >= QT_VERSION_CHECK(6, 9, 0) setWindowFlag(Qt::ExpandedClientAreaHint, true); setWindowFlag(Qt::NoTitleBarBackgroundHint, true); #endif - _ui.setupUi(this); + _ui->setupUi(this); setWindowModality(Qt::WindowModal); setWindowTitle(tr("Proxy Settings", "Dialog window title for proxy settings")); @@ -70,38 +73,39 @@ WizardProxySettingsDialog::WizardProxySettingsDialog(QUrl serverURL, layout()->setSpacing(14); } - _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->proxyGroupBox->setTitle({}); + _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->addItem(tr("HTTP(S) proxy"), QNetworkProxy::HttpProxy); - _ui.typeComboBox->addItem(tr("SOCKS5 proxy"), QNetworkProxy::Socks5Proxy); + _ui->typeComboBox->addItem(tr("HTTP(S) proxy"), QNetworkProxy::HttpProxy); + _ui->typeComboBox->addItem(tr("SOCKS5 proxy"), QNetworkProxy::Socks5Proxy); - _ui.authRequiredcheckBox->setEnabled(true); + _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)); @@ -110,12 +114,15 @@ WizardProxySettingsDialog::WizardProxySettingsDialog(QUrl serverURL, #if defined(Q_OS_MACOS) && QT_VERSION >= QT_VERSION_CHECK(6, 9, 0) _windowDragHandle = new WindowDragHandle(this); - _windowDragHandle->setFixedHeight(32); + _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); @@ -170,41 +177,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: @@ -216,45 +216,45 @@ void WizardProxySettingsDialog::setProxySettings(WizardProxySettings proxySettin void WizardProxySettingsDialog::checkEmptyProxyHost() { - if (_ui.hostLineEdit->isEnabled() && _ui.hostLineEdit->text().isEmpty()) { - _ui.hostLineEdit->setStyleSheet(invalidInputStyle); + 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(invalidInputStyle); + if (_ui->userLineEdit->text().isEmpty()) { + _ui->userLineEdit->setStyleSheet(invalidInputStyle); } else { - _ui.userLineEdit->setStyleSheet(QString()); + _ui->userLineEdit->setStyleSheet(QString()); } - if (_ui.passwordLineEdit->text().isEmpty()) { - _ui.passwordLineEdit->setStyleSheet(invalidInputStyle); + if (_ui->passwordLineEdit->text().isEmpty()) { + _ui->passwordLineEdit->setStyleSheet(invalidInputStyle); } else { - _ui.passwordLineEdit->setStyleSheet(QString()); + _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() @@ -263,35 +263,35 @@ 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 bf67fde319c31..0bd08fdb97fe1 100644 --- a/src/gui/wizard/wizardproxysettingsdialog.h +++ b/src/gui/wizard/wizardproxysettingsdialog.h @@ -7,11 +7,13 @@ #include #include +#include -#include "ui_proxysettings.h" +#include class QResizeEvent; class QWidget; +class Ui_ProxySettings; namespace OCC { @@ -29,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; @@ -39,6 +41,7 @@ class WizardProxySettingsDialog : public QDialog explicit WizardProxySettingsDialog(QUrl serverURL, WizardProxySettings proxySettings, QWidget *parent = nullptr); + ~WizardProxySettingsDialog() override; void setServerUrl(QUrl serverUrl); @@ -65,7 +68,7 @@ private Q_SLOTS: private: void customizeStyle(); - Ui_ProxySettings _ui{}; + std::unique_ptr _ui; QUrl _serverURL; From e3e44e2c639ce31cf4fbb819114058a5ca13b633 Mon Sep 17 00:00:00 2001 From: Rello Date: Thu, 14 May 2026 16:04:24 +0200 Subject: [PATCH 08/14] fix: testing error Signed-off-by: Rello --- src/gui/wizard/accountwizardcontroller.cpp | 23 +++++++++++++++------- src/gui/wizard/accountwizardcontroller.h | 1 + 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/gui/wizard/accountwizardcontroller.cpp b/src/gui/wizard/accountwizardcontroller.cpp index e89365ae23975..593abdbc74f18 100644 --- a/src/gui/wizard/accountwizardcontroller.cpp +++ b/src/gui/wizard/accountwizardcontroller.cpp @@ -69,12 +69,8 @@ void AccountWizardController::initialiseAccount() Theme::instance()->setStartLoginFlowAutomatically(true); } - _account = AccountManager::createAccount(); - _account->setCredentials(CredentialsFactory::create("dummy")); - const auto defaultUrl = Theme::instance()->multipleOverrideServers() ? QString{} : Theme::instance()->overrideServerUrl(); - _account->setUrl(defaultUrl); _remoteFolder = Theme::instance()->defaultServerFolder(); setServerUrl(defaultUrl); @@ -83,6 +79,16 @@ void AccountWizardController::initialiseAccount() setServerUrlEditable(!hasForcedConcreteServerUrl); } +void AccountWizardController::ensureAccount() +{ + if (_account) { + return; + } + + _account = AccountManager::createAccount(); + _account->setCredentials(CredentialsFactory::create("dummy")); +} + AccountWizardController::Step AccountWizardController::currentStep() const { return _currentStep; @@ -221,15 +227,18 @@ void AccountWizardController::submitServerUrl() return; } - const auto normalizedServerUrl = normalizeServerUrlInput(_serverUrl, _account ? _account->davPath() : QString{}); + 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; } - setServerUrl(url.toString()); - startServerCheck(url); + ensureAccount(); + normalizedServerUrl = normalizeServerUrlInput(_serverUrl, _account->davPath()); + const auto accountUrl = QUrl::fromUserInput(normalizedServerUrl); + setServerUrl(accountUrl.toString()); + startServerCheck(accountUrl); } void AccountWizardController::startServerCheck(const QUrl &serverUrl) diff --git a/src/gui/wizard/accountwizardcontroller.h b/src/gui/wizard/accountwizardcontroller.h index eddd397234bc9..425463f7b2027 100644 --- a/src/gui/wizard/accountwizardcontroller.h +++ b/src/gui/wizard/accountwizardcontroller.h @@ -147,6 +147,7 @@ private slots: private: void initialiseAccount(); + void ensureAccount(); void startServerCheck(const QUrl &serverUrl); void startFlow2Auth(); void connectToAuthenticatedAccount(const QString &url, const QString &user, const QString &appPassword); From d7699086f243239d1f31973d9e925de37efd66b1 Mon Sep 17 00:00:00 2001 From: Rello Date: Thu, 14 May 2026 22:23:34 +0200 Subject: [PATCH 09/14] fix: button layouts Signed-off-by: Rello --- src/gui/wizard/qml/AccountWizardWindow.qml | 16 +++++----------- src/gui/wizard/qml/WizardButton.qml | 14 +++++++++++++- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/gui/wizard/qml/AccountWizardWindow.qml b/src/gui/wizard/qml/AccountWizardWindow.qml index a515049e8f7af..1761a8d680ef6 100644 --- a/src/gui/wizard/qml/AccountWizardWindow.qml +++ b/src/gui/wizard/qml/AccountWizardWindow.qml @@ -45,17 +45,6 @@ ApplicationWindow { } } - MouseArea { - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.leftMargin: Qt.platform.os === "osx" ? 72 : 0 - height: 32 - z: 10 - acceptedButtons: Qt.LeftButton - onPressed: root.startSystemMove() - } - Connections { target: controller @@ -155,6 +144,8 @@ ApplicationWindow { 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.preferredWidth: 146 onClicked: root.controller.copyLoginLink() }, @@ -177,6 +168,9 @@ ApplicationWindow { return qsTr("Log in") } } + iconSource: root.controller && root.controller.currentStep === AccountWizardController.BrowserAuthStep + ? "image://svgimage-custom-color/external.svg/white" + : "" onClicked: { switch (root.controller.currentStep) { case AccountWizardController.BrowserAuthStep: diff --git a/src/gui/wizard/qml/WizardButton.qml b/src/gui/wizard/qml/WizardButton.qml index e67a82d10e68d..6b1efa9fc88dc 100644 --- a/src/gui/wizard/qml/WizardButton.qml +++ b/src/gui/wizard/qml/WizardButton.qml @@ -12,6 +12,7 @@ Button { property bool primary: false property string iconSource: "" + property bool iconBeforeText: false implicitHeight: 36 leftPadding: 18 @@ -29,6 +30,17 @@ Button { 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.text font: root.font @@ -40,7 +52,7 @@ Button { } Image { - visible: root.iconSource !== "" + visible: root.iconSource !== "" && !root.iconBeforeText source: root.iconSource sourceSize.width: 16 sourceSize.height: 16 From d2902c70c7add305af244f64fc74c3572907ccd3 Mon Sep 17 00:00:00 2001 From: Rello Date: Fri, 15 May 2026 12:03:03 +0200 Subject: [PATCH 10/14] fix: icon and padding --- src/gui/wizard/qml/AccountWizardWindow.qml | 34 +++++++++++++------- src/gui/wizard/qml/AdvancedOptionsDialog.qml | 2 +- src/gui/wizard/qml/BrowserAuthPage.qml | 17 +++------- src/gui/wizard/qml/OptionRow.qml | 4 ++- src/gui/wizard/qml/ServerPage.qml | 13 ++++---- src/gui/wizard/qml/SyncOptionsPage.qml | 6 ++-- src/gui/wizard/qml/WizardButton.qml | 17 ++++++---- src/gui/wizard/qml/WizardDialogFrame.qml | 12 +++---- src/gui/wizard/qml/WizardTextField.qml | 2 +- src/gui/wizard/wizardproxysettingsdialog.cpp | 17 +++++++--- theme.qrc.in | 1 + theme/globe.svg | 5 +++ 12 files changed, 77 insertions(+), 53 deletions(-) create mode 100644 theme/globe.svg diff --git a/src/gui/wizard/qml/AccountWizardWindow.qml b/src/gui/wizard/qml/AccountWizardWindow.qml index 1761a8d680ef6..32f5a17cff79d 100644 --- a/src/gui/wizard/qml/AccountWizardWindow.qml +++ b/src/gui/wizard/qml/AccountWizardWindow.qml @@ -23,7 +23,7 @@ ApplicationWindow { height: 448 minimumWidth: 480 minimumHeight: 420 - title: qsTr("Add %1 account").arg(controller ? controller.appName : "") + title: "" color: "white" background: Rectangle { @@ -89,7 +89,8 @@ ApplicationWindow { WizardButton { visible: root.controller && root.controller.currentStep !== AccountWizardController.ServerStep enabled: root.controller && !root.controller.busy - Layout.preferredWidth: root.controller && root.controller.currentStep === AccountWizardController.BrowserAuthStep ? 146 : implicitWidth + Layout.fillWidth: root.controller && root.controller.currentStep === AccountWizardController.BrowserAuthStep + Layout.preferredWidth: root.controller && root.controller.currentStep === AccountWizardController.BrowserAuthStep ? 1 : implicitWidth text: root.controller && root.controller.currentStep === AccountWizardController.BrowserAuthStep ? qsTr("Cancel") : qsTr("Back") @@ -113,7 +114,9 @@ ApplicationWindow { visible: root.controller && root.controller.currentStep === AccountWizardController.ServerStep enabled: root.controller && !root.controller.busy text: qsTr("Sign up") - iconSource: "image://svgimage-custom-color/external.svg/" + palette.buttonText + textSuffix: "\u2197" + Layout.fillWidth: true + Layout.preferredWidth: 1 onClicked: root.controller.openSignup() }, @@ -121,7 +124,9 @@ ApplicationWindow { visible: root.controller && root.controller.currentStep === AccountWizardController.ServerStep enabled: root.controller && !root.controller.busy text: qsTr("Self-host") - iconSource: "image://svgimage-custom-color/external.svg/" + palette.buttonText + textSuffix: "\u2197" + Layout.fillWidth: true + Layout.preferredWidth: 1 onClicked: root.controller.openSelfHostedServerGuide() }, @@ -130,13 +135,18 @@ ApplicationWindow { enabled: root.controller && !root.controller.busy flat: true text: qsTr("Proxy settings") - font.bold: true - font.pixelSize: Style.pixelSize + 1 + font.pointSize: 15 + font.weight: Font.Medium + Layout.fillWidth: true + Layout.preferredWidth: 1 + Layout.preferredHeight: Style.standardPrimaryButtonHeight onClicked: root.controller.openProxySettings() }, Item { - visible: root.controller && root.controller.currentStep !== AccountWizardController.BrowserAuthStep + visible: root.controller + && root.controller.currentStep !== AccountWizardController.ServerStep + && root.controller.currentStep !== AccountWizardController.BrowserAuthStep Layout.fillWidth: visible }, @@ -146,7 +156,8 @@ ApplicationWindow { text: qsTr("Copy link") iconSource: "image://svgimage-custom-color/copy.svg/" + palette.buttonText iconBeforeText: true - Layout.preferredWidth: 146 + Layout.fillWidth: true + Layout.preferredWidth: 1 onClicked: root.controller.copyLoginLink() }, @@ -154,7 +165,8 @@ ApplicationWindow { visible: root.controller && root.controller.currentStep !== AccountWizardController.ServerStep primary: true enabled: root.controller && !root.controller.busy - Layout.preferredWidth: root.controller && root.controller.currentStep === AccountWizardController.BrowserAuthStep ? 146 : implicitWidth + Layout.fillWidth: root.controller && root.controller.currentStep === AccountWizardController.BrowserAuthStep + Layout.preferredWidth: root.controller && root.controller.currentStep === AccountWizardController.BrowserAuthStep ? 1 : implicitWidth text: { if (!root.controller) { return "" @@ -168,8 +180,8 @@ ApplicationWindow { return qsTr("Log in") } } - iconSource: root.controller && root.controller.currentStep === AccountWizardController.BrowserAuthStep - ? "image://svgimage-custom-color/external.svg/white" + textSuffix: root.controller && root.controller.currentStep === AccountWizardController.BrowserAuthStep + ? "\u2197" : "" onClicked: { switch (root.controller.currentStep) { diff --git a/src/gui/wizard/qml/AdvancedOptionsDialog.qml b/src/gui/wizard/qml/AdvancedOptionsDialog.qml index ee7232d66d557..63e37c45ae3c4 100644 --- a/src/gui/wizard/qml/AdvancedOptionsDialog.qml +++ b/src/gui/wizard/qml/AdvancedOptionsDialog.qml @@ -16,7 +16,7 @@ Dialog { modal: true width: 360 - padding: 22 + padding: 36 header: null footer: null diff --git a/src/gui/wizard/qml/BrowserAuthPage.qml b/src/gui/wizard/qml/BrowserAuthPage.qml index baf1afba6b66c..2604f8a48c037 100644 --- a/src/gui/wizard/qml/BrowserAuthPage.qml +++ b/src/gui/wizard/qml/BrowserAuthPage.qml @@ -25,9 +25,11 @@ Item { Image { Layout.alignment: Qt.AlignHCenter - source: "image://svgimage-custom-color/external.svg/" + Style.ncBlue - sourceSize.width: 82 - sourceSize.height: 82 + source: "image://svgimage-custom-color/globe.svg/" + Style.ncBlue + sourceSize.width: 72 + sourceSize.height: 72 + Layout.preferredWidth: 72 + Layout.preferredHeight: 72 fillMode: Image.PreserveAspectFit } @@ -40,15 +42,6 @@ Item { wrapMode: Text.WordWrap } - EnforcedPlainTextLabel { - text: qsTr("Authorize this device in the browser window that opened.") - color: palette.mid - font.pixelSize: Style.pixelSize + 2 - horizontalAlignment: Text.AlignHCenter - Layout.fillWidth: true - wrapMode: Text.WordWrap - } - RowLayout { visible: root.controller.busy && root.controller.authStatusText !== "" Layout.fillWidth: true diff --git a/src/gui/wizard/qml/OptionRow.qml b/src/gui/wizard/qml/OptionRow.qml index bf9841ff8a400..81e82a611fd57 100644 --- a/src/gui/wizard/qml/OptionRow.qml +++ b/src/gui/wizard/qml/OptionRow.qml @@ -24,8 +24,10 @@ Control { background: Rectangle { radius: 8 + border.width: 1 + border.color: root.selected ? "#d5e8f2" : "transparent" color: root.selected - ? Style.infoBoxBackgroundColor + ? "#e7f3fa" : (Style.darkMode ? Qt.rgba(1, 1, 1, 0.05) : Qt.rgba(0, 0, 0, 0.035)) } diff --git a/src/gui/wizard/qml/ServerPage.qml b/src/gui/wizard/qml/ServerPage.qml index 0359326dfbd99..ed5f1600f839d 100644 --- a/src/gui/wizard/qml/ServerPage.qml +++ b/src/gui/wizard/qml/ServerPage.qml @@ -19,9 +19,9 @@ Item { anchors.fill: parent anchors.leftMargin: 36 anchors.rightMargin: 36 - anchors.topMargin: 38 - anchors.bottomMargin: 12 - spacing: 10 + anchors.topMargin: 36 + anchors.bottomMargin: 36 + spacing: 4 EnforcedPlainTextLabel { text: qsTr("Log in to %1").arg(root.controller.appName) @@ -41,12 +41,13 @@ Item { Item { Layout.fillWidth: true - Layout.preferredHeight: 64 + Layout.preferredHeight: 60 + Layout.topMargin: 22 RowLayout { anchors.fill: parent anchors.topMargin: 6 - spacing: 6 + spacing: 8 WizardTextField { id: serverUrlField @@ -63,7 +64,7 @@ Item { WizardButton { primary: true - Layout.preferredWidth: 88 + Layout.preferredWidth: 76 enabled: !root.controller.busy text: qsTr("Log in") onClicked: root.controller.submitServerUrl() diff --git a/src/gui/wizard/qml/SyncOptionsPage.qml b/src/gui/wizard/qml/SyncOptionsPage.qml index 2e22382a633dd..e1b9bbff13855 100644 --- a/src/gui/wizard/qml/SyncOptionsPage.qml +++ b/src/gui/wizard/qml/SyncOptionsPage.qml @@ -19,9 +19,9 @@ Item { anchors.fill: parent anchors.leftMargin: 36 anchors.rightMargin: 36 - anchors.topMargin: 38 - anchors.bottomMargin: 12 - spacing: 16 + anchors.topMargin: 36 + anchors.bottomMargin: 36 + spacing: 14 EnforcedPlainTextLabel { text: qsTr("Choose what to sync") diff --git a/src/gui/wizard/qml/WizardButton.qml b/src/gui/wizard/qml/WizardButton.qml index 6b1efa9fc88dc..83c593106ff33 100644 --- a/src/gui/wizard/qml/WizardButton.qml +++ b/src/gui/wizard/qml/WizardButton.qml @@ -13,12 +13,13 @@ Button { property bool primary: false property string iconSource: "" property bool iconBeforeText: false + property string textSuffix: "" - implicitHeight: 36 + implicitHeight: Style.standardPrimaryButtonHeight leftPadding: 18 rightPadding: 18 - font.bold: true - font.pixelSize: Style.pixelSize + 1 + font.pointSize: 15 + font.weight: Font.Medium contentItem: Item { implicitWidth: contentRow.implicitWidth @@ -42,7 +43,7 @@ Button { } Text { - text: root.text + text: root.textSuffix === "" ? root.text : root.text + " " + root.textSuffix font: root.font color: root.enabled ? (root.primary ? "white" : root.palette.buttonText) @@ -66,16 +67,18 @@ Button { background: Rectangle { radius: 8 + border.width: root.primary ? 0 : 1 + border.color: "#d5e8f2" color: { if (!root.enabled) { return root.palette.button } if (root.primary) { - return root.down ? Qt.darker(Style.ncBlue, 1.25) : Style.ncBlue + return root.down ? "#00679a" : "#0076ad" } return root.down - ? Qt.darker(Style.infoBoxBackgroundColor, 1.12) - : Style.infoBoxBackgroundColor + ? "#d8edf8" + : "#e7f3fa" } } } diff --git a/src/gui/wizard/qml/WizardDialogFrame.qml b/src/gui/wizard/qml/WizardDialogFrame.qml index 7bdfcd9e1eb59..b3009988ddfec 100644 --- a/src/gui/wizard/qml/WizardDialogFrame.qml +++ b/src/gui/wizard/qml/WizardDialogFrame.qml @@ -31,16 +31,16 @@ Pane { Item { Layout.fillWidth: true - Layout.preferredHeight: 68 + Layout.preferredHeight: 76 RowLayout { id: footerLayout anchors.fill: parent - anchors.leftMargin: 24 - anchors.rightMargin: 24 - anchors.topMargin: 10 - anchors.bottomMargin: 8 - spacing: 6 + anchors.leftMargin: 36 + anchors.rightMargin: 36 + anchors.topMargin: 0 + anchors.bottomMargin: 36 + spacing: 8 } } } diff --git a/src/gui/wizard/qml/WizardTextField.qml b/src/gui/wizard/qml/WizardTextField.qml index 4d92586e82868..f41adeefba356 100644 --- a/src/gui/wizard/qml/WizardTextField.qml +++ b/src/gui/wizard/qml/WizardTextField.qml @@ -10,7 +10,7 @@ import Style TextField { id: root - implicitHeight: 46 + implicitHeight: Style.standardPrimaryButtonHeight leftPadding: 12 rightPadding: 12 topPadding: 0 diff --git a/src/gui/wizard/wizardproxysettingsdialog.cpp b/src/gui/wizard/wizardproxysettingsdialog.cpp index 0ad5124d190c0..ac1a1b9bd10bd 100644 --- a/src/gui/wizard/wizardproxysettingsdialog.cpp +++ b/src/gui/wizard/wizardproxysettingsdialog.cpp @@ -8,6 +8,7 @@ #include "ui_proxysettings.h" #include +#include #include #include #include @@ -111,6 +112,12 @@ WizardProxySettingsDialog::WizardProxySettingsDialog(QUrl serverURL, setServerUrl(std::move(serverURL)); setProxySettings(std::move(proxySettings)); customizeStyle(); + for (auto *button : _ui->buttonBox->buttons()) { + auto buttonFont = button->font(); + buttonFont.setPointSize(15); + buttonFont.setWeight(QFont::Medium); + button->setFont(buttonFont); + } #if defined(Q_OS_MACOS) && QT_VERSION >= QT_VERSION_CHECK(6, 9, 0) _windowDragHandle = new WindowDragHandle(this); @@ -152,15 +159,15 @@ void WizardProxySettingsDialog::customizeStyle() " }" "QLineEdit:focus, QSpinBox:focus, QComboBox:focus { border: 1px solid palette(highlight); }" "QPushButton {" - " min-height: 34px;" + " min-height: 40px;" " border: none;" " border-radius: 8px;" " padding: 4px 18px;" - " background: rgb(230, 242, 249);" - " font-size: 15px;" - " font-weight: bold;" + " background: #e7f3fa;" + " font-size: 15pt;" + " font-weight: 500;" " }" - "QPushButton:default { background: rgb(0, 130, 201); color: white; }" + "QPushButton:default { background: #0076ad; color: white; }" "QPushButton:disabled { color: rgb(150, 150, 150); background: rgb(242, 242, 242); }" )); } diff --git a/theme.qrc.in b/theme.qrc.in index 8ebb6e3d7b590..f13bb68bd50fa 100644 --- a/theme.qrc.in +++ b/theme.qrc.in @@ -286,6 +286,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 @@ + + From 108d2d37f1bc6c1d95f485d0fa76ee9d3bff496c Mon Sep 17 00:00:00 2001 From: Rello Date: Fri, 15 May 2026 14:59:04 +0200 Subject: [PATCH 11/14] fix: proxy dialog and button colors --- src/gui/wizard/qml/OptionRow.qml | 4 +- src/gui/wizard/qml/WizardButton.qml | 13 +++-- src/gui/wizard/wizardproxysettingsdialog.cpp | 61 +++++++++++++++----- 3 files changed, 58 insertions(+), 20 deletions(-) diff --git a/src/gui/wizard/qml/OptionRow.qml b/src/gui/wizard/qml/OptionRow.qml index 81e82a611fd57..fc5529a005265 100644 --- a/src/gui/wizard/qml/OptionRow.qml +++ b/src/gui/wizard/qml/OptionRow.qml @@ -25,9 +25,9 @@ Control { background: Rectangle { radius: 8 border.width: 1 - border.color: root.selected ? "#d5e8f2" : "transparent" + border.color: root.selected ? "#d5e0e7" : "transparent" color: root.selected - ? "#e7f3fa" + ? "#e7eef4" : (Style.darkMode ? Qt.rgba(1, 1, 1, 0.05) : Qt.rgba(0, 0, 0, 0.035)) } diff --git a/src/gui/wizard/qml/WizardButton.qml b/src/gui/wizard/qml/WizardButton.qml index 83c593106ff33..8cad5f0ac4415 100644 --- a/src/gui/wizard/qml/WizardButton.qml +++ b/src/gui/wizard/qml/WizardButton.qml @@ -14,6 +14,11 @@ Button { 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" implicitHeight: Style.standardPrimaryButtonHeight leftPadding: 18 @@ -68,17 +73,17 @@ Button { background: Rectangle { radius: 8 border.width: root.primary ? 0 : 1 - border.color: "#d5e8f2" + border.color: root.secondaryBorderColor color: { if (!root.enabled) { return root.palette.button } if (root.primary) { - return root.down ? "#00679a" : "#0076ad" + return root.down ? root.primaryPressedColor : root.primaryColor } return root.down - ? "#d8edf8" - : "#e7f3fa" + ? root.secondaryPressedColor + : root.secondaryColor } } } diff --git a/src/gui/wizard/wizardproxysettingsdialog.cpp b/src/gui/wizard/wizardproxysettingsdialog.cpp index ac1a1b9bd10bd..3339f68cb402b 100644 --- a/src/gui/wizard/wizardproxysettingsdialog.cpp +++ b/src/gui/wizard/wizardproxysettingsdialog.cpp @@ -9,10 +9,12 @@ #include #include +#include #include #include #include #include +#include #include #include @@ -22,12 +24,13 @@ Q_LOGGING_CATEGORY(lcWizardProxySettings, "nextcloud.gui.wizard.proxysettings", namespace { const QString invalidInputStyle = QStringLiteral( - "min-height: 34px;" + "min-height: 40px;" + "max-height: 40px;" "border: 1px solid red;" "border-radius: 8px;" - "padding: 4px 10px;" + "padding: 0px 12px;" "background: white;" - "font-size: 15px;" + "font-size: 15pt;" ); class WindowDragHandle : public QWidget @@ -65,19 +68,35 @@ WizardProxySettingsDialog::WizardProxySettingsDialog(QUrl serverURL, _ui->setupUi(this); setWindowModality(Qt::WindowModal); - setWindowTitle(tr("Proxy Settings", "Dialog window title for proxy settings")); + setWindowTitle({}); setMinimumSize(500, 448); resize(500, 448); if (layout()) { - layout()->setContentsMargins(36, 38, 36, 16); + layout()->setContentsMargins(36, 36, 36, 36); layout()->setSpacing(14); } + _ui->gridLayout_2->setContentsMargins(0, 30, 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->proxyGroupBox->setTitle({}); _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); @@ -112,11 +131,18 @@ WizardProxySettingsDialog::WizardProxySettingsDialog(QUrl serverURL, setServerUrl(std::move(serverURL)); setProxySettings(std::move(proxySettings)); customizeStyle(); + _ui->buttonBox->setCenterButtons(true); + if (_ui->buttonBox->layout()) { + _ui->buttonBox->layout()->setSpacing(8); + } + for (auto *button : _ui->buttonBox->buttons()) { auto buttonFont = button->font(); buttonFont.setPointSize(15); buttonFont.setWeight(QFont::Medium); button->setFont(buttonFont); + button->setMinimumHeight(40); + button->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); } #if defined(Q_OS_MACOS) && QT_VERSION >= QT_VERSION_CHECK(6, 9, 0) @@ -146,28 +172,35 @@ void WizardProxySettingsDialog::customizeStyle() { setStyleSheet(QStringLiteral( "QDialog, QWidget#ProxySettings { background: white; }" - "QGroupBox { border: none; margin-top: 0px; padding-top: 0px; font-size: 18px; font-weight: bold; }" + "QGroupBox { border: none; margin-top: 0px; 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: 15px; }" + "QRadioButton, QCheckBox, QLabel { font-size: 15pt; }" "QLineEdit, QSpinBox, QComboBox {" - " min-height: 34px;" + " min-height: 40px;" + " max-height: 40px;" " border: 1px solid rgb(196, 196, 196);" " border-radius: 8px;" - " padding: 4px 10px;" + " padding: 0px 12px;" " background: white;" - " font-size: 15px;" + " font-size: 15pt;" " }" "QLineEdit:focus, QSpinBox:focus, QComboBox:focus { border: 1px solid palette(highlight); }" + "QDialogButtonBox { border: none; padding: 0px; }" "QPushButton {" " min-height: 40px;" + " max-height: 40px;" " border: none;" " border-radius: 8px;" - " padding: 4px 18px;" - " background: #e7f3fa;" + " padding: 0px 18px;" + " background-color: #e7eef4;" " font-size: 15pt;" " font-weight: 500;" " }" - "QPushButton:default { background: #0076ad; color: white; }" + "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); }" )); } From 51c95e84032035ac859d7986fcd282545098d378 Mon Sep 17 00:00:00 2001 From: Rello Date: Fri, 15 May 2026 17:16:05 +0200 Subject: [PATCH 12/14] fix: UI polishing --- .../project.pbxproj | 32 +++++++++++++++++ src/gui/wizard/qml/AccountWizardWindow.qml | 13 +++++-- src/gui/wizard/qml/AdvancedOptionsDialog.qml | 4 ++- src/gui/wizard/qml/BrowserAuthPage.qml | 7 ++-- src/gui/wizard/qml/OptionRow.qml | 7 ++-- src/gui/wizard/qml/ServerPage.qml | 17 ++++++---- src/gui/wizard/qml/SyncOptionsPage.qml | 19 ++++++----- src/gui/wizard/qml/WizardButton.qml | 14 ++++---- src/gui/wizard/qml/WizardDialogFrame.qml | 10 +++--- src/gui/wizard/qml/WizardTextField.qml | 2 +- src/gui/wizard/wizardproxysettingsdialog.cpp | 34 +++++++++++++------ 11 files changed, 115 insertions(+), 44 deletions(-) diff --git a/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj/project.pbxproj b/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj/project.pbxproj index 9f483ddc071a9..79d1bc9154ab8 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj/project.pbxproj +++ b/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj/project.pbxproj @@ -746,9 +746,13 @@ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)"; DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = 649J6ZGB3X; ENABLE_TESTING_SEARCH_PATHS = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; @@ -795,9 +799,13 @@ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)"; DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = 649J6ZGB3X; ENABLE_NS_ASSERTIONS = NO; ENABLE_TESTING_SEARCH_PATHS = YES; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -839,9 +847,13 @@ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)"; DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = 649J6ZGB3X; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GCC_DYNAMIC_NO_PIC = NO; @@ -900,9 +912,13 @@ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)"; DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = 649J6ZGB3X; ENABLE_NS_ASSERTIONS = NO; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -1115,9 +1131,13 @@ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = 649J6ZGB3X; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; @@ -1169,9 +1189,13 @@ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = 649J6ZGB3X; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; @@ -1214,9 +1238,13 @@ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = 649J6ZGB3X; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; @@ -1267,9 +1295,13 @@ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = 649J6ZGB3X; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; diff --git a/src/gui/wizard/qml/AccountWizardWindow.qml b/src/gui/wizard/qml/AccountWizardWindow.qml index 32f5a17cff79d..d61e57a6d9084 100644 --- a/src/gui/wizard/qml/AccountWizardWindow.qml +++ b/src/gui/wizard/qml/AccountWizardWindow.qml @@ -8,7 +8,6 @@ import QtQuick.Controls import QtQuick.Layouts import QtQuick.Window import com.nextcloud.desktopclient -import Style ApplicationWindow { id: root @@ -25,6 +24,14 @@ ApplicationWindow { 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" @@ -135,11 +142,11 @@ ApplicationWindow { enabled: root.controller && !root.controller.busy flat: true text: qsTr("Proxy settings") - font.pointSize: 15 + font.pointSize: 16 font.weight: Font.Medium Layout.fillWidth: true Layout.preferredWidth: 1 - Layout.preferredHeight: Style.standardPrimaryButtonHeight + Layout.preferredHeight: 36 onClicked: root.controller.openProxySettings() }, diff --git a/src/gui/wizard/qml/AdvancedOptionsDialog.qml b/src/gui/wizard/qml/AdvancedOptionsDialog.qml index 63e37c45ae3c4..c287eec6d9501 100644 --- a/src/gui/wizard/qml/AdvancedOptionsDialog.qml +++ b/src/gui/wizard/qml/AdvancedOptionsDialog.qml @@ -13,10 +13,11 @@ Dialog { id: root required property var controller + readonly property color primaryTextColor: "#111111" modal: true width: 360 - padding: 36 + padding: 24 header: null footer: null @@ -30,6 +31,7 @@ Dialog { EnforcedPlainTextLabel { text: qsTr("Advanced options") + color: root.primaryTextColor font.pixelSize: Style.pixelSize + 8 font.bold: true Layout.fillWidth: true diff --git a/src/gui/wizard/qml/BrowserAuthPage.qml b/src/gui/wizard/qml/BrowserAuthPage.qml index 2604f8a48c037..2e24b85dd98d0 100644 --- a/src/gui/wizard/qml/BrowserAuthPage.qml +++ b/src/gui/wizard/qml/BrowserAuthPage.qml @@ -13,10 +13,12 @@ Item { id: root required property var controller + readonly property color primaryTextColor: "#111111" + readonly property color primaryButtonColor: "#2B659A" ColumnLayout { anchors.fill: parent - anchors.margins: 36 + anchors.margins: 24 spacing: 14 Item { @@ -25,7 +27,7 @@ Item { Image { Layout.alignment: Qt.AlignHCenter - source: "image://svgimage-custom-color/globe.svg/" + Style.ncBlue + source: "image://svgimage-custom-color/globe.svg/" + root.primaryButtonColor sourceSize.width: 72 sourceSize.height: 72 Layout.preferredWidth: 72 @@ -35,6 +37,7 @@ Item { EnforcedPlainTextLabel { text: qsTr("Switch to your browser") + color: root.primaryTextColor font.pixelSize: Style.pixelSize + 8 font.bold: true horizontalAlignment: Text.AlignHCenter diff --git a/src/gui/wizard/qml/OptionRow.qml b/src/gui/wizard/qml/OptionRow.qml index fc5529a005265..cfd05142b6b4f 100644 --- a/src/gui/wizard/qml/OptionRow.qml +++ b/src/gui/wizard/qml/OptionRow.qml @@ -16,6 +16,8 @@ Control { 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() @@ -28,7 +30,7 @@ Control { border.color: root.selected ? "#d5e0e7" : "transparent" color: root.selected ? "#e7eef4" - : (Style.darkMode ? Qt.rgba(1, 1, 1, 0.05) : Qt.rgba(0, 0, 0, 0.035)) + : Qt.rgba(0, 0, 0, 0.035) } contentItem: RowLayout { @@ -50,6 +52,7 @@ Control { EnforcedPlainTextLabel { id: titleLabel Layout.fillWidth: true + color: root.primaryTextColor font.bold: true font.pixelSize: Style.pixelSize + 2 elide: Text.ElideRight @@ -58,7 +61,7 @@ Control { EnforcedPlainTextLabel { id: descriptionLabel Layout.fillWidth: true - color: palette.mid + color: root.hintTextColor font.pixelSize: Style.pixelSize + 1 wrapMode: Text.WordWrap maximumLineCount: 2 diff --git a/src/gui/wizard/qml/ServerPage.qml b/src/gui/wizard/qml/ServerPage.qml index ed5f1600f839d..8512f366233de 100644 --- a/src/gui/wizard/qml/ServerPage.qml +++ b/src/gui/wizard/qml/ServerPage.qml @@ -14,17 +14,20 @@ 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: 36 - anchors.rightMargin: 36 - anchors.topMargin: 36 - anchors.bottomMargin: 36 + 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 @@ -33,7 +36,7 @@ Item { 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: palette.mid + color: root.hintTextColor font.pixelSize: Style.pixelSize + 2 Layout.fillWidth: true wrapMode: Text.WordWrap @@ -82,7 +85,7 @@ Item { id: serverAddressLabel anchors.centerIn: parent text: qsTr("Server address") - color: palette.mid + color: root.hintTextColor font.pixelSize: Style.pixelSize } } @@ -111,7 +114,7 @@ Item { EnforcedPlainTextLabel { text: root.controller.authStatusText - color: palette.mid + color: root.hintTextColor font.pixelSize: Style.pixelSize + 1 Layout.fillWidth: true wrapMode: Text.WordWrap diff --git a/src/gui/wizard/qml/SyncOptionsPage.qml b/src/gui/wizard/qml/SyncOptionsPage.qml index e1b9bbff13855..81c3776c3cbd4 100644 --- a/src/gui/wizard/qml/SyncOptionsPage.qml +++ b/src/gui/wizard/qml/SyncOptionsPage.qml @@ -14,17 +14,20 @@ 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: 36 - anchors.rightMargin: 36 - anchors.topMargin: 36 - anchors.bottomMargin: 36 + anchors.leftMargin: 24 + anchors.rightMargin: 24 + anchors.topMargin: 24 + anchors.bottomMargin: 24 spacing: 14 EnforcedPlainTextLabel { text: qsTr("Choose what to sync") + color: root.primaryTextColor font.pixelSize: Style.pixelSize + 8 font.bold: true Layout.fillWidth: true @@ -34,7 +37,7 @@ Item { text: root.controller.userDisplayName !== "" ? qsTr("Connected as %1.").arg(root.controller.userDisplayName) : qsTr("Your account is connected.") - color: palette.mid + color: root.hintTextColor font.pixelSize: Style.pixelSize + 2 Layout.fillWidth: true wrapMode: Text.WordWrap @@ -44,7 +47,7 @@ Item { Layout.fillWidth: true title: qsTr("Sync all files") description: qsTr("Download files to this device.") - iconSource: "image://svgimage-custom-color/folder.svg/" + palette.windowText + iconSource: "image://svgimage-custom-color/folder.svg/" + root.primaryTextColor selected: root.controller.syncMode === AccountWizardController.SyncEverything onClicked: root.controller.setSyncMode(AccountWizardController.SyncEverything) } @@ -53,7 +56,7 @@ Item { Layout.fillWidth: true title: qsTr("Choose folders") description: qsTr("Select folders before the first sync.") - iconSource: "image://svgimage-custom-color/sync.svg/" + palette.windowText + iconSource: "image://svgimage-custom-color/sync.svg/" + root.primaryTextColor selected: root.controller.syncMode === AccountWizardController.SelectiveSync onClicked: root.controller.openSelectiveSync() } @@ -63,7 +66,7 @@ Item { visible: root.controller.canUseVirtualFiles title: qsTr("Use virtual files") description: qsTr("Keep files online until opened.") - iconSource: "image://svgimage-custom-color/wizard-files.svg/" + palette.windowText + iconSource: "image://svgimage-custom-color/wizard-files.svg/" + root.primaryTextColor selected: root.controller.syncMode === AccountWizardController.VirtualFiles onClicked: root.controller.setSyncMode(AccountWizardController.VirtualFiles) } diff --git a/src/gui/wizard/qml/WizardButton.qml b/src/gui/wizard/qml/WizardButton.qml index 8cad5f0ac4415..0e3a5085807fb 100644 --- a/src/gui/wizard/qml/WizardButton.qml +++ b/src/gui/wizard/qml/WizardButton.qml @@ -14,16 +14,18 @@ Button { property string iconSource: "" property bool iconBeforeText: false property string textSuffix: "" - readonly property color primaryColor: "#2b659a" + 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: Style.standardPrimaryButtonHeight + implicitHeight: 36 leftPadding: 18 rightPadding: 18 - font.pointSize: 15 + font.pointSize: 16 font.weight: Font.Medium contentItem: Item { @@ -52,7 +54,7 @@ Button { font: root.font color: root.enabled ? (root.primary ? "white" : root.palette.buttonText) - : root.palette.mid + : "#8a949c" anchors.verticalCenter: parent.verticalCenter elide: Text.ElideRight } @@ -73,10 +75,10 @@ Button { background: Rectangle { radius: 8 border.width: root.primary ? 0 : 1 - border.color: root.secondaryBorderColor + border.color: root.enabled ? root.secondaryBorderColor : root.disabledBorderColor color: { if (!root.enabled) { - return root.palette.button + return root.disabledColor } if (root.primary) { return root.down ? root.primaryPressedColor : root.primaryColor diff --git a/src/gui/wizard/qml/WizardDialogFrame.qml b/src/gui/wizard/qml/WizardDialogFrame.qml index b3009988ddfec..77432179d8aea 100644 --- a/src/gui/wizard/qml/WizardDialogFrame.qml +++ b/src/gui/wizard/qml/WizardDialogFrame.qml @@ -12,6 +12,8 @@ Pane { default property alias contents: body.data property alias footer: footerLayout.data + readonly property int windowMargin: 24 + readonly property int footerButtonHeight: 36 padding: 0 @@ -31,15 +33,15 @@ Pane { Item { Layout.fillWidth: true - Layout.preferredHeight: 76 + Layout.preferredHeight: root.footerButtonHeight + root.windowMargin RowLayout { id: footerLayout anchors.fill: parent - anchors.leftMargin: 36 - anchors.rightMargin: 36 + anchors.leftMargin: root.windowMargin + anchors.rightMargin: root.windowMargin anchors.topMargin: 0 - anchors.bottomMargin: 36 + anchors.bottomMargin: root.windowMargin spacing: 8 } } diff --git a/src/gui/wizard/qml/WizardTextField.qml b/src/gui/wizard/qml/WizardTextField.qml index f41adeefba356..c23a76f75668a 100644 --- a/src/gui/wizard/qml/WizardTextField.qml +++ b/src/gui/wizard/qml/WizardTextField.qml @@ -18,7 +18,7 @@ TextField { verticalAlignment: TextInput.AlignVCenter font.pixelSize: Style.pixelSize + 3 color: "black" - placeholderTextColor: Qt.rgba(0, 0, 0, 0.28) + placeholderTextColor: Qt.rgba(0, 0, 0, 0.50) selectionColor: Style.ncBlue selectedTextColor: "white" diff --git a/src/gui/wizard/wizardproxysettingsdialog.cpp b/src/gui/wizard/wizardproxysettingsdialog.cpp index 3339f68cb402b..f07fbdbeeb876 100644 --- a/src/gui/wizard/wizardproxysettingsdialog.cpp +++ b/src/gui/wizard/wizardproxysettingsdialog.cpp @@ -73,10 +73,10 @@ WizardProxySettingsDialog::WizardProxySettingsDialog(QUrl serverURL, resize(500, 448); if (layout()) { - layout()->setContentsMargins(36, 36, 36, 36); + layout()->setContentsMargins(24, 24, 24, 24); layout()->setSpacing(14); } - _ui->gridLayout_2->setContentsMargins(0, 30, 0, 0); + _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); @@ -131,18 +131,20 @@ WizardProxySettingsDialog::WizardProxySettingsDialog(QUrl serverURL, setServerUrl(std::move(serverURL)); setProxySettings(std::move(proxySettings)); customizeStyle(); - _ui->buttonBox->setCenterButtons(true); + _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(15); + buttonFont.setPointSize(16); buttonFont.setWeight(QFont::Medium); button->setFont(buttonFont); - button->setMinimumHeight(40); - button->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + 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) @@ -172,7 +174,7 @@ void WizardProxySettingsDialog::customizeStyle() { setStyleSheet(QStringLiteral( "QDialog, QWidget#ProxySettings { background: white; }" - "QGroupBox { border: none; margin-top: 0px; padding-top: 0px; font-size: 20px; font-weight: bold; }" + "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 {" @@ -184,16 +186,28 @@ void WizardProxySettingsDialog::customizeStyle() " 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: 40px;" - " max-height: 40px;" + " min-height: 36px;" + " max-height: 36px;" " border: none;" " border-radius: 8px;" " padding: 0px 18px;" " background-color: #e7eef4;" - " font-size: 15pt;" + " font-size: 16pt;" " font-weight: 500;" " }" "QPushButton:hover { background-color: #e7eef4; }" From 083c0a01228a9e861ebb50aa3caa79aaff062418 Mon Sep 17 00:00:00 2001 From: Rello Date: Sat, 16 May 2026 15:55:29 +0200 Subject: [PATCH 13/14] fix: account wizard polishing --- src/gui/tray/usermodel.cpp | 110 ++++++- src/gui/wizard/accountwizardcontroller.cpp | 315 ++++++++++++++++++++- src/gui/wizard/accountwizardcontroller.h | 36 ++- src/gui/wizard/qml/AccountWizardWindow.qml | 41 ++- src/gui/wizard/qml/OptionRow.qml | 46 +-- src/gui/wizard/qml/SyncOptionsPage.qml | 160 ++++++++--- test/testiconutils.cpp | 7 + theme.qrc.in | 3 + 8 files changed, 643 insertions(+), 75 deletions(-) diff --git a/src/gui/tray/usermodel.cpp b/src/gui/tray/usermodel.cpp index 8ad55aa7f9af4..f96242f9bc2b6 100644 --- a/src/gui/tray/usermodel.cpp +++ b/src/gui/tray/usermodel.cpp @@ -34,12 +34,18 @@ #endif #include +#include #include +#include +#include +#include #include -#include +#include #include #include #include +#include +#include #include #include @@ -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 index 593abdbc74f18..ff157e0c3c86f 100644 --- a/src/gui/wizard/accountwizardcontroller.cpp +++ b/src/gui/wizard/accountwizardcontroller.cpp @@ -13,6 +13,8 @@ #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" @@ -24,8 +26,16 @@ #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 @@ -34,6 +44,7 @@ #include #include #include +#include using namespace Qt::StringLiterals; @@ -149,11 +160,47 @@ 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 @@ -541,6 +588,8 @@ void AccountWizardController::connectToAuthenticatedAccount(const QString &url, _account->setDavDisplayName(displayName); setUserDisplayName(displayName.isEmpty() ? userId : displayName); setServerUrl(url); + setAvatarUrl({}); + fetchUserAvatar(); testOwnCloudConnect(); }); @@ -616,6 +665,8 @@ void AccountWizardController::completeAuthentication() { setBusy(false); setAuthStatusText(tr("Account connected.")); + initialiseLocalSyncFolder(); + fetchRootFolderSize(); #if defined(Q_OS_WIN) || defined(Q_OS_MACOS) setNeedsSyncOptions(!canUseVirtualFiles()); @@ -630,6 +681,62 @@ void AccountWizardController::completeAuthentication() } } +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); @@ -656,34 +763,195 @@ void AccountWizardController::finish() 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); } -void AccountWizardController::applyAccountChanges() +AccountState *AccountWizardController::applyAccountChanges() { auto manager = AccountManager::instance(); + AccountState *accountState = nullptr; #ifdef BUILD_FILE_PROVIDER_MODULE if (_syncMode == VirtualFiles) { - const auto accountState = manager->addAccount(_account); + accountState = manager->addAccount(_account); const auto accountId = accountState->account()->userIdAtHostWithPort(); Mac::FileProviderSettingsController::instance()->setVfsEnabledForAccount(accountId, true, false); } else #endif { - manager->addAccount(_account); + 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) @@ -701,8 +969,40 @@ void AccountWizardController::setSyncMode(int syncMode) 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() @@ -834,6 +1134,15 @@ void AccountWizardController::setAvatarUrl(const QString &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) { diff --git a/src/gui/wizard/accountwizardcontroller.h b/src/gui/wizard/accountwizardcontroller.h index 425463f7b2027..5826f2d6fea5e 100644 --- a/src/gui/wizard/accountwizardcontroller.h +++ b/src/gui/wizard/accountwizardcontroller.h @@ -23,6 +23,7 @@ class QNetworkReply; namespace OCC { class SelectiveSyncDialog; +class AccountState; /** * Backend for the QML account wizard. @@ -44,7 +45,13 @@ class AccountWizardController : public QObject 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) @@ -84,7 +91,13 @@ class AccountWizardController : public QObject [[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; @@ -105,7 +118,9 @@ class AccountWizardController : public QObject 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); @@ -124,7 +139,12 @@ class AccountWizardController : public QObject void userDisplayNameChanged(); void serverDisplayNameChanged(); void avatarUrlChanged(); + void syncEverythingDescriptionChanged(); + void localSyncFolderChanged(); + void localSyncFolderErrorChanged(); + void localSyncFolderRequiredChanged(); void syncModeChanged(); + void canFinishChanged(); void needsSyncOptionsChanged(); void askBeforeLargeFoldersChanged(); void largeFolderThresholdMbChanged(); @@ -153,7 +173,15 @@ private slots: void connectToAuthenticatedAccount(const QString &url, const QString &user, const QString &appPassword); void testOwnCloudConnect(); void completeAuthentication(); - void applyAccountChanges(); + 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); @@ -162,6 +190,7 @@ private slots: 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; @@ -180,6 +209,11 @@ private slots: 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; diff --git a/src/gui/wizard/qml/AccountWizardWindow.qml b/src/gui/wizard/qml/AccountWizardWindow.qml index d61e57a6d9084..1a32aff73c9b2 100644 --- a/src/gui/wizard/qml/AccountWizardWindow.qml +++ b/src/gui/wizard/qml/AccountWizardWindow.qml @@ -19,7 +19,7 @@ ApplicationWindow { LayoutMirroring.childrenInherit: true width: 500 - height: 448 + height: 520 minimumWidth: 480 minimumHeight: 420 title: "" @@ -96,13 +96,22 @@ ApplicationWindow { WizardButton { visible: root.controller && root.controller.currentStep !== AccountWizardController.ServerStep enabled: root.controller && !root.controller.busy - Layout.fillWidth: root.controller && root.controller.currentStep === AccountWizardController.BrowserAuthStep - Layout.preferredWidth: root.controller && root.controller.currentStep === AccountWizardController.BrowserAuthStep ? 1 : implicitWidth + 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") - : qsTr("Back") + : root.controller && root.controller.currentStep === AccountWizardController.SyncOptionsStep + ? qsTr("Cancel") + : qsTr("Back") onClicked: { - if (root.controller.currentStep === AccountWizardController.BrowserAuthStep) { + if (root.controller.currentStep === AccountWizardController.BrowserAuthStep + || root.controller.currentStep === AccountWizardController.SyncOptionsStep) { root.controller.cancel() } else { root.controller.goBack() @@ -113,8 +122,10 @@ ApplicationWindow { WizardButton { visible: root.controller && root.controller.currentStep === AccountWizardController.SyncOptionsStep enabled: root.controller && !root.controller.busy - text: qsTr("Advanced") - onClicked: root.controller.openAdvancedOptions() + text: qsTr("Set up later") + Layout.fillWidth: true + Layout.preferredWidth: 1 + onClicked: root.controller.skipFolderConfiguration() }, WizardButton { @@ -154,6 +165,7 @@ ApplicationWindow { visible: root.controller && root.controller.currentStep !== AccountWizardController.ServerStep && root.controller.currentStep !== AccountWizardController.BrowserAuthStep + && root.controller.currentStep !== AccountWizardController.SyncOptionsStep Layout.fillWidth: visible }, @@ -171,9 +183,18 @@ ApplicationWindow { WizardButton { visible: root.controller && root.controller.currentStep !== AccountWizardController.ServerStep primary: true - enabled: root.controller && !root.controller.busy - Layout.fillWidth: root.controller && root.controller.currentStep === AccountWizardController.BrowserAuthStep - Layout.preferredWidth: root.controller && root.controller.currentStep === AccountWizardController.BrowserAuthStep ? 1 : implicitWidth + 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 "" diff --git a/src/gui/wizard/qml/OptionRow.qml b/src/gui/wizard/qml/OptionRow.qml index cfd05142b6b4f..f728f6b60faae 100644 --- a/src/gui/wizard/qml/OptionRow.qml +++ b/src/gui/wizard/qml/OptionRow.qml @@ -21,48 +21,60 @@ Control { signal clicked() - implicitHeight: 72 - padding: 14 + implicitHeight: descriptionLabel.text === "" ? 42 : 56 + padding: 10 background: Rectangle { - radius: 8 + radius: 6 border.width: 1 - border.color: root.selected ? "#d5e0e7" : "transparent" + border.color: root.selected ? "#d8e7f1" : "#e1e8ee" color: root.selected - ? "#e7eef4" - : Qt.rgba(0, 0, 0, 0.035) + ? "#eef5fb" + : "#f5f8fa" } contentItem: RowLayout { - spacing: 12 + spacing: 10 - Image { - source: root.iconSource - sourceSize.width: 24 - sourceSize.height: 24 - Layout.preferredWidth: 28 - Layout.preferredHeight: 28 - fillMode: Image.PreserveAspectFit + 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: 2 + 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 + 2 + font.pixelSize: Style.pixelSize + 1 elide: Text.ElideRight } EnforcedPlainTextLabel { id: descriptionLabel + visible: text !== "" Layout.fillWidth: true color: root.hintTextColor - font.pixelSize: Style.pixelSize + 1 + font.pixelSize: Style.pixelSize wrapMode: Text.WordWrap maximumLineCount: 2 } diff --git a/src/gui/wizard/qml/SyncOptionsPage.qml b/src/gui/wizard/qml/SyncOptionsPage.qml index 81c3776c3cbd4..944238be5b269 100644 --- a/src/gui/wizard/qml/SyncOptionsPage.qml +++ b/src/gui/wizard/qml/SyncOptionsPage.qml @@ -14,61 +14,157 @@ 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: 24 - anchors.rightMargin: 24 - anchors.topMargin: 24 - anchors.bottomMargin: 24 - spacing: 14 + 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: qsTr("Choose what to sync") + text: root.controller.userDisplayName color: root.primaryTextColor - font.pixelSize: Style.pixelSize + 8 + font.pixelSize: Style.pixelSize + 6 font.bold: true + horizontalAlignment: Text.AlignHCenter Layout.fillWidth: true + wrapMode: Text.WordWrap } EnforcedPlainTextLabel { - text: root.controller.userDisplayName !== "" - ? qsTr("Connected as %1.").arg(root.controller.userDisplayName) - : qsTr("Your account is connected.") + text: root.serverLabel color: root.hintTextColor font.pixelSize: Style.pixelSize + 2 + horizontalAlignment: Text.AlignHCenter Layout.fillWidth: true - wrapMode: Text.WordWrap + elide: Text.ElideMiddle } - OptionRow { + ColumnLayout { Layout.fillWidth: true - title: qsTr("Sync all files") - description: qsTr("Download files to this device.") - iconSource: "image://svgimage-custom-color/folder.svg/" + root.primaryTextColor - selected: root.controller.syncMode === AccountWizardController.SyncEverything - onClicked: root.controller.setSyncMode(AccountWizardController.SyncEverything) - } + Layout.topMargin: 24 + spacing: 8 - OptionRow { - Layout.fillWidth: true - title: qsTr("Choose folders") - description: qsTr("Select folders before the first sync.") - iconSource: "image://svgimage-custom-color/sync.svg/" + root.primaryTextColor - selected: root.controller.syncMode === AccountWizardController.SelectiveSync - onClicked: root.controller.openSelectiveSync() + 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() + } } - OptionRow { + ColumnLayout { Layout.fillWidth: true - visible: root.controller.canUseVirtualFiles - title: qsTr("Use virtual files") - description: qsTr("Keep files online until opened.") - iconSource: "image://svgimage-custom-color/wizard-files.svg/" + root.primaryTextColor - selected: root.controller.syncMode === AccountWizardController.VirtualFiles - onClicked: root.controller.setSyncMode(AccountWizardController.VirtualFiles) + 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 { 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 f13bb68bd50fa..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 From 301b422d4314436002910628bdb8493875dbf8db Mon Sep 17 00:00:00 2001 From: Rello Date: Mon, 18 May 2026 16:34:44 +0200 Subject: [PATCH 14/14] fix: add missing features to QML wizard --- src/gui/wizard/accountwizardcontroller.cpp | 196 ++++++++++++++++++- src/gui/wizard/accountwizardcontroller.h | 30 +++ src/gui/wizard/qml/AccountWizardWindow.qml | 11 ++ src/gui/wizard/qml/AdvancedOptionsDialog.qml | 3 + src/gui/wizard/qml/OptionRow.qml | 13 +- src/gui/wizard/qml/SyncOptionsPage.qml | 51 ++++- src/gui/wizard/qml/WizardButton.qml | 3 +- 7 files changed, 289 insertions(+), 18 deletions(-) diff --git a/src/gui/wizard/accountwizardcontroller.cpp b/src/gui/wizard/accountwizardcontroller.cpp index ff157e0c3c86f..325096bf601b1 100644 --- a/src/gui/wizard/accountwizardcontroller.cpp +++ b/src/gui/wizard/accountwizardcontroller.cpp @@ -23,6 +23,7 @@ #include "theme.h" #ifdef BUILD_FILE_PROVIDER_MODULE +#include "gui/macOS/fileprovider.h" #include "gui/macOS/fileprovidersettingscontroller.h" #endif @@ -44,6 +45,8 @@ #include #include #include +#include +#include #include using namespace Qt::StringLiterals; @@ -182,6 +185,26 @@ QString AccountWizardController::localSyncFolderError() const return _localSyncFolderError; } +bool AccountWizardController::localSyncFolderWarning() const +{ + return _localSyncFolderWarning; +} + +QString AccountWizardController::localSyncFolderFreeSpace() const +{ + return _localSyncFolderFreeSpace; +} + +bool AccountWizardController::localSyncFolderHasExistingData() const +{ + return _localSyncFolderHasExistingData; +} + +bool AccountWizardController::syncFromScratch() const +{ + return _syncFromScratch; +} + bool AccountWizardController::localSyncFolderRequired() const { #ifdef BUILD_FILE_PROVIDER_MODULE @@ -203,15 +226,24 @@ bool AccountWizardController::canFinish() const bool AccountWizardController::canUseVirtualFiles() const { + if (Theme::instance()->disableVirtualFilesSyncFolder()) { + return false; + } + #ifdef BUILD_FILE_PROVIDER_MODULE - return true; + return Mac::FileProvider::available(); #elif defined(Q_OS_WIN) - return bestAvailableVfsMode() == Vfs::WindowsCfApi; + return bestAvailableVfsMode() == Vfs::WindowsCfApi && Theme::instance()->showVirtualFilesOption(); #else return bestAvailableVfsMode() != Vfs::Off && Theme::instance()->showVirtualFilesOption(); #endif } +bool AccountWizardController::canUseClassicSync() const +{ + return !Theme::instance()->enforceVirtualFilesSyncFolder() || !canUseVirtualFiles(); +} + bool AccountWizardController::needsSyncOptions() const { return _needsSyncOptions; @@ -222,6 +254,16 @@ bool AccountWizardController::canSkipFolderConfiguration() const return true; } +bool AccountWizardController::hasAdvancedOptions() const +{ + return showLargeFolderConfirmation() || showExternalStorageConfirmation(); +} + +bool AccountWizardController::showLargeFolderConfirmation() const +{ + return !Theme::instance()->wizardHideFolderSizeLimitCheckbox(); +} + bool AccountWizardController::askBeforeLargeFolders() const { return _askBeforeLargeFolders; @@ -232,6 +274,11 @@ int AccountWizardController::largeFolderThresholdMb() const return _largeFolderThresholdMb; } +bool AccountWizardController::showExternalStorageConfirmation() const +{ + return !Theme::instance()->wizardHideExternalStorageConfirmationCheckbox(); +} + bool AccountWizardController::askBeforeExternalStorage() const { return _askBeforeExternalStorage; @@ -676,6 +723,10 @@ void AccountWizardController::completeAuthentication() if (needsSyncOptions()) { setCurrentStep(SyncOptionsStep); + if (Theme::instance()->wizardSelectiveSyncDefaultNothing() && canUseClassicSync()) { + _selectiveSyncBlacklist = QStringList(QStringLiteral("/")); + QTimer::singleShot(0, this, &AccountWizardController::openSelectiveSync); + } } else { finish(); } @@ -728,11 +779,15 @@ void AccountWizardController::fetchRootFolderSize() } if (size >= 0) { + _syncEverythingSize = size; setSyncEverythingDescription(tr("Will require %1 of storage").arg(Utility::octetsToString(size))); + validateLocalSyncFolder(); } }); connect(quotaJob, &PropfindJob::finishedWithError, this, [this](QNetworkReply *) { + _syncEverythingSize = -1; setSyncEverythingDescription({}); + validateLocalSyncFolder(); }); quotaJob->start(); } @@ -765,7 +820,7 @@ void AccountWizardController::finish() if (localSyncFolderRequired()) { validateLocalSyncFolder(); - if (!_localSyncFolderValid || !ensureLocalSyncFolder()) { + if (!_localSyncFolderValid || !ensureStartFromScratch() || !ensureLocalSyncFolder()) { return; } } @@ -860,20 +915,72 @@ void AccountWizardController::setLocalSyncFolder(const QString &localSyncFolder, void AccountWizardController::validateLocalSyncFolder() { const auto oldCanFinish = canFinish(); + auto localSyncFolderFreeSpace = QString{}; + auto localSyncFolderHasExistingData = false; + + if (localSyncFolderRequired()) { + const auto freeBytes = availableLocalSpace(); + if (freeBytes >= 0) { + localSyncFolderFreeSpace = tr("%1 free space", "%1 gets replaced with the size and a matching unit. Example: 3 MB or 5 GB") + .arg(Utility::octetsToString(freeBytes)); + } + + const QDir localFolder(_localSyncFolder); + localSyncFolderHasExistingData = localFolder.exists() + && !localFolder.entryList(QDir::AllEntries | QDir::NoDotAndDotDot).isEmpty(); + } + const auto pathValidity = FolderMan::instance()->checkPathValidityForNewFolder(_localSyncFolder, localFolderServerUrl()); auto localSyncFolderError = pathValidity.second; + auto localSyncFolderWarning = localSyncFolderRequired() + && !localSyncFolderError.isEmpty() + && pathValidity.first == FolderMan::PathValidityResult::ErrorNonEmptyFolder; + + const auto neededBytes = requiredLocalSpace(); + const auto freeBytes = availableLocalSpace(); + if (localSyncFolderRequired() && (localSyncFolderError.isEmpty() || localSyncFolderWarning) && neededBytes >= 0 && freeBytes >= 0 && freeBytes <= neededBytes) { + localSyncFolderError = tr("There isn't enough free space in the local folder!"); + localSyncFolderWarning = false; + } + +#ifndef BUILD_FILE_PROVIDER_MODULE + if (localSyncFolderRequired() && _syncMode == VirtualFiles && (localSyncFolderError.isEmpty() || localSyncFolderWarning)) { + const auto availability = Vfs::checkAvailability(FolderDefinition::prepareLocalPath(_localSyncFolder), bestAvailableVfsMode()); + if (!availability) { + localSyncFolderError = availability.error(); + localSyncFolderWarning = false; + } + } +#endif + #ifdef Q_OS_MACOS if (localSyncFolderRequired() && !_localSyncFolderSelected) { localSyncFolderError = tr("Please choose a local sync folder."); + localSyncFolderWarning = false; } #endif - const auto localSyncFolderValid = !localSyncFolderRequired() || localSyncFolderError.isEmpty(); + const auto localSyncFolderValid = !localSyncFolderRequired() || localSyncFolderError.isEmpty() || localSyncFolderWarning; + + if (_localSyncFolderFreeSpace != localSyncFolderFreeSpace) { + _localSyncFolderFreeSpace = localSyncFolderFreeSpace; + emit localSyncFolderFreeSpaceChanged(); + } + + if (_localSyncFolderHasExistingData != localSyncFolderHasExistingData) { + _localSyncFolderHasExistingData = localSyncFolderHasExistingData; + emit localSyncFolderHasExistingDataChanged(); + } if (_localSyncFolderError != localSyncFolderError) { _localSyncFolderError = localSyncFolderError; emit localSyncFolderErrorChanged(); } + if (_localSyncFolderWarning != localSyncFolderWarning) { + _localSyncFolderWarning = localSyncFolderWarning; + emit localSyncFolderWarningChanged(); + } + if (_localSyncFolderValid != localSyncFolderValid) { _localSyncFolderValid = localSyncFolderValid; if (oldCanFinish != canFinish()) { @@ -882,6 +989,34 @@ void AccountWizardController::validateLocalSyncFolder() } } +qint64 AccountWizardController::availableLocalSpace() const +{ + if (_localSyncFolder.isEmpty()) { + return -1; + } + + const auto localFolder = FolderDefinition::prepareLocalPath(_localSyncFolder); + const auto path = !QDir(localFolder).exists() && localFolder.contains(QDir::homePath()) + ? QDir::homePath() + : localFolder; + const QStorageInfo storage(QDir::toNativeSeparators(path)); + return storage.isValid() ? storage.bytesAvailable() : -1; +} + +qint64 AccountWizardController::requiredLocalSpace() const +{ + switch (_syncMode) { + case SyncEverything: + return _syncEverythingSize; + case SelectiveSync: + return _selectiveSyncSize; + case VirtualFiles: + return -1; + } + + return -1; +} + bool AccountWizardController::ensureLocalSyncFolder() { const auto localFolder = FolderDefinition::prepareLocalPath(_localSyncFolder); @@ -899,6 +1034,22 @@ bool AccountWizardController::ensureLocalSyncFolder() return true; } +bool AccountWizardController::ensureStartFromScratch() +{ + if (!_syncFromScratch || !_localSyncFolderHasExistingData) { + return true; + } + + const auto localFolder = FolderDefinition::prepareLocalPath(_localSyncFolder); + if (!FolderMan::instance()->startFromScratch(localFolder)) { + setErrorText(tr("Cannot remove and back up the folder because the folder or a file in it is open in another program. Please close the folder or file and try again.")); + return false; + } + + validateLocalSyncFolder(); + return true; +} + bool AccountWizardController::createSyncFolder(AccountState *accountState) { if (!accountState) { @@ -968,6 +1119,9 @@ void AccountWizardController::setSyncMode(int syncMode) if (newSyncMode == VirtualFiles && !canUseVirtualFiles()) { return; } + if (newSyncMode != VirtualFiles && !canUseClassicSync()) { + return; + } const auto oldLocalSyncFolderRequired = localSyncFolderRequired(); const auto oldCanFinish = canFinish(); @@ -1007,23 +1161,35 @@ void AccountWizardController::chooseLocalSyncFolder() void AccountWizardController::openSelectiveSync() { - if (!_account) { + if (!_account || !canUseClassicSync()) { return; } + const auto previousSyncMode = _syncMode; + const auto previousBlacklist = _selectiveSyncBlacklist; + const auto previousSelectiveSyncSize = _selectiveSyncSize; setSyncMode(SelectiveSync); if (!_selectiveSyncDialog) { _selectiveSyncDialog = new SelectiveSyncDialog(_account, _remoteFolder, _selectiveSyncBlacklist); _selectiveSyncDialog->setAttribute(Qt::WA_DeleteOnClose); - connect(_selectiveSyncDialog, &SelectiveSyncDialog::finished, this, [this] { + connect(_selectiveSyncDialog, &SelectiveSyncDialog::finished, this, [this, previousSyncMode, previousBlacklist, previousSelectiveSyncSize] { if (!_selectiveSyncDialog) { return; } if (_selectiveSyncDialog->result() == QDialog::Accepted) { _selectiveSyncBlacklist = _selectiveSyncDialog->createBlackList(); - } else if (_selectiveSyncBlacklist == QStringList("/")) { - _selectiveSyncBlacklist = _selectiveSyncDialog->oldBlackList(); + _selectiveSyncSize = _selectiveSyncDialog->estimatedSize(); + if (_selectiveSyncBlacklist.isEmpty()) { + setSyncMode(SyncEverything); + } else { + setSyncMode(SelectiveSync); + } + } else { + _selectiveSyncBlacklist = previousBlacklist; + _selectiveSyncSize = previousSelectiveSyncSize; + setSyncMode(previousSyncMode); } + validateLocalSyncFolder(); }); } _selectiveSyncDialog->open(); @@ -1031,7 +1197,19 @@ void AccountWizardController::openSelectiveSync() void AccountWizardController::openAdvancedOptions() { - emit advancedOptionsRequested(); + if (hasAdvancedOptions()) { + emit advancedOptionsRequested(); + } +} + +void AccountWizardController::setSyncFromScratch(bool syncFromScratch) +{ + if (_syncFromScratch == syncFromScratch) { + return; + } + + _syncFromScratch = syncFromScratch; + emit syncFromScratchChanged(); } void AccountWizardController::setAskBeforeLargeFolders(bool ask) diff --git a/src/gui/wizard/accountwizardcontroller.h b/src/gui/wizard/accountwizardcontroller.h index 5826f2d6fea5e..a05967f58e7fa 100644 --- a/src/gui/wizard/accountwizardcontroller.h +++ b/src/gui/wizard/accountwizardcontroller.h @@ -49,14 +49,22 @@ class AccountWizardController : public QObject 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 localSyncFolderWarning READ localSyncFolderWarning NOTIFY localSyncFolderWarningChanged) + Q_PROPERTY(QString localSyncFolderFreeSpace READ localSyncFolderFreeSpace NOTIFY localSyncFolderFreeSpaceChanged) + Q_PROPERTY(bool localSyncFolderHasExistingData READ localSyncFolderHasExistingData NOTIFY localSyncFolderHasExistingDataChanged) + Q_PROPERTY(bool syncFromScratch READ syncFromScratch WRITE setSyncFromScratch NOTIFY syncFromScratchChanged) 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 canUseClassicSync READ canUseClassicSync CONSTANT) Q_PROPERTY(bool needsSyncOptions READ needsSyncOptions NOTIFY needsSyncOptionsChanged) Q_PROPERTY(bool canSkipFolderConfiguration READ canSkipFolderConfiguration CONSTANT) + Q_PROPERTY(bool hasAdvancedOptions READ hasAdvancedOptions CONSTANT) + Q_PROPERTY(bool showLargeFolderConfirmation READ showLargeFolderConfirmation CONSTANT) Q_PROPERTY(bool askBeforeLargeFolders READ askBeforeLargeFolders NOTIFY askBeforeLargeFoldersChanged) Q_PROPERTY(int largeFolderThresholdMb READ largeFolderThresholdMb NOTIFY largeFolderThresholdMbChanged) + Q_PROPERTY(bool showExternalStorageConfirmation READ showExternalStorageConfirmation CONSTANT) Q_PROPERTY(bool askBeforeExternalStorage READ askBeforeExternalStorage NOTIFY askBeforeExternalStorageChanged) Q_PROPERTY(QString appName READ appName CONSTANT) Q_PROPERTY(QString serverUrlPlaceholder READ serverUrlPlaceholder CONSTANT) @@ -95,14 +103,22 @@ class AccountWizardController : public QObject [[nodiscard]] QString localSyncFolder() const; [[nodiscard]] QString localSyncFolderDisplay() const; [[nodiscard]] QString localSyncFolderError() const; + [[nodiscard]] bool localSyncFolderWarning() const; + [[nodiscard]] QString localSyncFolderFreeSpace() const; + [[nodiscard]] bool localSyncFolderHasExistingData() const; + [[nodiscard]] bool syncFromScratch() const; [[nodiscard]] bool localSyncFolderRequired() const; [[nodiscard]] SyncMode syncMode() const; [[nodiscard]] bool canFinish() const; [[nodiscard]] bool canUseVirtualFiles() const; + [[nodiscard]] bool canUseClassicSync() const; [[nodiscard]] bool needsSyncOptions() const; [[nodiscard]] bool canSkipFolderConfiguration() const; + [[nodiscard]] bool hasAdvancedOptions() const; + [[nodiscard]] bool showLargeFolderConfirmation() const; [[nodiscard]] bool askBeforeLargeFolders() const; [[nodiscard]] int largeFolderThresholdMb() const; + [[nodiscard]] bool showExternalStorageConfirmation() const; [[nodiscard]] bool askBeforeExternalStorage() const; [[nodiscard]] QString appName() const; [[nodiscard]] QString serverUrlPlaceholder() const; @@ -123,6 +139,7 @@ class AccountWizardController : public QObject Q_INVOKABLE void chooseLocalSyncFolder(); Q_INVOKABLE void openSelectiveSync(); Q_INVOKABLE void openAdvancedOptions(); + Q_INVOKABLE void setSyncFromScratch(bool syncFromScratch); Q_INVOKABLE void setAskBeforeLargeFolders(bool ask); Q_INVOKABLE void setLargeFolderThresholdMb(int thresholdMb); Q_INVOKABLE void setAskBeforeExternalStorage(bool ask); @@ -142,6 +159,10 @@ class AccountWizardController : public QObject void syncEverythingDescriptionChanged(); void localSyncFolderChanged(); void localSyncFolderErrorChanged(); + void localSyncFolderWarningChanged(); + void localSyncFolderFreeSpaceChanged(); + void localSyncFolderHasExistingDataChanged(); + void syncFromScratchChanged(); void localSyncFolderRequiredChanged(); void syncModeChanged(); void canFinishChanged(); @@ -179,7 +200,10 @@ private slots: void initialiseLocalSyncFolder(); void setLocalSyncFolder(const QString &localSyncFolder, bool selectedByUser = false); void validateLocalSyncFolder(); + [[nodiscard]] qint64 availableLocalSpace() const; + [[nodiscard]] qint64 requiredLocalSpace() const; [[nodiscard]] bool ensureLocalSyncFolder(); + [[nodiscard]] bool ensureStartFromScratch(); [[nodiscard]] bool createSyncFolder(AccountState *accountState); [[nodiscard]] QUrl localFolderServerUrl() const; void setCurrentStep(Step step); @@ -212,6 +236,10 @@ private slots: QString _syncEverythingDescription; QString _localSyncFolder; QString _localSyncFolderError; + QString _localSyncFolderFreeSpace; + bool _localSyncFolderWarning = false; + bool _localSyncFolderHasExistingData = false; + bool _syncFromScratch = false; bool _localSyncFolderValid = false; bool _localSyncFolderSelected = false; SyncMode _syncMode = SyncEverything; @@ -219,6 +247,8 @@ private slots: bool _askBeforeLargeFolders = true; int _largeFolderThresholdMb = 500; bool _askBeforeExternalStorage = true; + qint64 _syncEverythingSize = -1; + qint64 _selectiveSyncSize = -1; QString _remoteFolder; QStringList _selectiveSyncBlacklist; WizardProxySettingsDialog::WizardProxySettings _proxySettings; diff --git a/src/gui/wizard/qml/AccountWizardWindow.qml b/src/gui/wizard/qml/AccountWizardWindow.qml index 1a32aff73c9b2..58c02acefb79f 100644 --- a/src/gui/wizard/qml/AccountWizardWindow.qml +++ b/src/gui/wizard/qml/AccountWizardWindow.qml @@ -128,6 +128,17 @@ ApplicationWindow { onClicked: root.controller.skipFolderConfiguration() }, + WizardButton { + visible: root.controller + && root.controller.currentStep === AccountWizardController.SyncOptionsStep + && root.controller.hasAdvancedOptions + enabled: root.controller && !root.controller.busy + text: qsTr("Advanced") + Layout.fillWidth: true + Layout.preferredWidth: 1 + onClicked: root.controller.openAdvancedOptions() + }, + WizardButton { visible: root.controller && root.controller.currentStep === AccountWizardController.ServerStep enabled: root.controller && !root.controller.busy diff --git a/src/gui/wizard/qml/AdvancedOptionsDialog.qml b/src/gui/wizard/qml/AdvancedOptionsDialog.qml index c287eec6d9501..a59996e7b1204 100644 --- a/src/gui/wizard/qml/AdvancedOptionsDialog.qml +++ b/src/gui/wizard/qml/AdvancedOptionsDialog.qml @@ -38,6 +38,7 @@ Dialog { } CheckBox { + visible: root.controller.showLargeFolderConfirmation text: qsTr("Ask before syncing folders larger than") checked: root.controller.askBeforeLargeFolders font.pixelSize: Style.pixelSize + 2 @@ -46,6 +47,7 @@ Dialog { } SpinBox { + visible: root.controller.showLargeFolderConfirmation from: 0 to: 1048576 value: root.controller.largeFolderThresholdMb @@ -59,6 +61,7 @@ Dialog { } CheckBox { + visible: root.controller.showExternalStorageConfirmation text: qsTr("Ask before syncing external storage") checked: root.controller.askBeforeExternalStorage font.pixelSize: Style.pixelSize + 2 diff --git a/src/gui/wizard/qml/OptionRow.qml b/src/gui/wizard/qml/OptionRow.qml index f728f6b60faae..a70a8a342c14a 100644 --- a/src/gui/wizard/qml/OptionRow.qml +++ b/src/gui/wizard/qml/OptionRow.qml @@ -27,8 +27,8 @@ Control { background: Rectangle { radius: 6 border.width: 1 - border.color: root.selected ? "#d8e7f1" : "#e1e8ee" - color: root.selected + border.color: !root.enabled ? "#edf1f4" : root.selected ? "#d8e7f1" : "#e1e8ee" + color: !root.enabled ? "#fafafa" : root.selected ? "#eef5fb" : "#f5f8fa" } @@ -42,7 +42,7 @@ Control { Layout.alignment: Qt.AlignVCenter radius: width / 2 border.width: 2 - border.color: root.selected ? "#0076b5" : "#0076b5" + border.color: root.enabled ? "#0076b5" : "#b7c0c7" color: "transparent" Rectangle { @@ -51,7 +51,7 @@ Control { height: 8 radius: width / 2 color: "#0076b5" - visible: root.selected + visible: root.selected && root.enabled } } @@ -63,7 +63,7 @@ Control { EnforcedPlainTextLabel { id: titleLabel Layout.fillWidth: true - color: root.primaryTextColor + color: root.enabled ? root.primaryTextColor : root.hintTextColor font.bold: true font.pixelSize: Style.pixelSize + 1 elide: Text.ElideRight @@ -84,7 +84,8 @@ Control { MouseArea { anchors.fill: parent hoverEnabled: true - cursorShape: Qt.PointingHandCursor + enabled: root.enabled + cursorShape: root.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor onClicked: root.clicked() } } diff --git a/src/gui/wizard/qml/SyncOptionsPage.qml b/src/gui/wizard/qml/SyncOptionsPage.qml index 944238be5b269..6766bf8e91714 100644 --- a/src/gui/wizard/qml/SyncOptionsPage.qml +++ b/src/gui/wizard/qml/SyncOptionsPage.qml @@ -95,6 +95,7 @@ Item { OptionRow { Layout.fillWidth: true + enabled: root.controller.canUseClassicSync title: qsTr("Synchronize everything") description: root.controller.syncEverythingDescription selected: root.controller.syncMode === AccountWizardController.SyncEverything @@ -103,6 +104,7 @@ Item { OptionRow { Layout.fillWidth: true + enabled: root.controller.canUseClassicSync title: qsTr("Choose what to sync") description: "" selected: root.controller.syncMode === AccountWizardController.SelectiveSync @@ -133,7 +135,9 @@ Item { Layout.preferredHeight: 36 radius: 6 border.width: 1 - border.color: root.controller.localSyncFolderError === "" ? "#e1e8ee" : "#d84b4b" + border.color: root.controller.localSyncFolderError === "" + ? "#e1e8ee" + : root.controller.localSyncFolderWarning ? "#b36b00" : "#d84b4b" color: "#f5f8fa" EnforcedPlainTextLabel { @@ -152,19 +156,62 @@ Item { text: qsTr("Choose") Layout.preferredWidth: 96 Layout.preferredHeight: 36 + enabled: root.controller.canUseClassicSync onClicked: root.controller.chooseLocalSyncFolder() } } + EnforcedPlainTextLabel { + visible: root.controller.localSyncFolderFreeSpace !== "" + text: root.controller.localSyncFolderFreeSpace + color: root.hintTextColor + font.pixelSize: Style.pixelSize + Layout.fillWidth: true + elide: Text.ElideRight + } + EnforcedPlainTextLabel { visible: root.controller.localSyncFolderError !== "" text: root.controller.localSyncFolderError - color: "#b00020" + color: root.controller.localSyncFolderWarning ? "#8a5200" : "#b00020" font.pixelSize: Style.pixelSize Layout.fillWidth: true wrapMode: Text.WordWrap maximumLineCount: 2 } + + ColumnLayout { + visible: root.controller.localSyncFolderHasExistingData + && root.controller.canUseClassicSync + && root.controller.localSyncFolderRequired + Layout.fillWidth: true + spacing: 2 + + EnforcedPlainTextLabel { + text: qsTr("Warning: The local folder is not empty. Pick a resolution!") + color: root.primaryTextColor + font.pixelSize: Style.pixelSize + font.bold: true + Layout.fillWidth: true + wrapMode: Text.WordWrap + } + + RadioButton { + text: qsTr("Keep local data") + checked: !root.controller.syncFromScratch + font.pixelSize: Style.pixelSize + onClicked: root.controller.setSyncFromScratch(false) + Layout.fillWidth: true + } + + RadioButton { + text: qsTr("Erase local folder and start a clean sync") + checked: root.controller.syncFromScratch + font.pixelSize: Style.pixelSize + onClicked: root.controller.setSyncFromScratch(true) + Layout.fillWidth: true + } + } } Item { diff --git a/src/gui/wizard/qml/WizardButton.qml b/src/gui/wizard/qml/WizardButton.qml index 0e3a5085807fb..db0a8aeacc6f0 100644 --- a/src/gui/wizard/qml/WizardButton.qml +++ b/src/gui/wizard/qml/WizardButton.qml @@ -5,9 +5,10 @@ import QtQuick import QtQuick.Controls +import QtQuick.Controls.Basic as BasicControls import Style -Button { +BasicControls.Button { id: root property bool primary: false