diff --git a/CMakeLists.txt b/CMakeLists.txt index a0aaf129..50304e54 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,6 +67,7 @@ find_package(KF6 ${KF6_MIN_VERSION} REQUIRED COMPONENTS NetworkManagerQt KCMUtils Package + DBusAddons ) find_package(PkgConfig REQUIRED) @@ -104,6 +105,7 @@ add_subdirectory(kcms) add_subdirectory(kded) add_subdirectory(kwin) add_subdirectory(envmanager) +add_subdirectory(initialstart) find_program(PlasmaOpenSettings plasma-open-settings) set_package_properties(PlasmaOpenSettings PROPERTIES diff --git a/components/mmplugin/CMakeLists.txt b/components/mmplugin/CMakeLists.txt index 222b310d..b0d02d83 100644 --- a/components/mmplugin/CMakeLists.txt +++ b/components/mmplugin/CMakeLists.txt @@ -5,6 +5,7 @@ add_library(ppc-mmqmlplugin) target_sources(ppc-mmqmlplugin PRIVATE mmqmlplugin.cpp signalindicator.cpp + profilesettings.cpp ) target_link_libraries(ppc-mmqmlplugin diff --git a/components/mmplugin/profilesettings.cpp b/components/mmplugin/profilesettings.cpp new file mode 100644 index 00000000..09a48099 --- /dev/null +++ b/components/mmplugin/profilesettings.cpp @@ -0,0 +1,139 @@ +// SPDX-FileCopyrightText: 2022 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "profilesettings.h" + +#include + +ProfileSettings::ProfileSettings(QObject *parent, + const QString &name, + const QString &apn, + const QString &user, + const QString &password, + NetworkManager::GsmSetting::NetworkType networkType, + const QString &connectionUni) + : QObject{parent} + , m_name(name) + , m_apn(apn) + , m_user(user) + , m_password(password) + , m_networkType(networkTypeStr(networkType)) + , m_connectionUni(connectionUni) +{ + setParent(parent); +} + +ProfileSettings::ProfileSettings(QObject *parent, NetworkManager::Setting::Ptr setting, NetworkManager::Connection::Ptr connection) + : QObject{parent} + , m_connectionUni(connection->uuid()) +{ + NetworkManager::GsmSetting::Ptr gsmSetting = setting.staticCast(); + + m_name = connection->name(); + m_apn = gsmSetting->apn(); + m_user = gsmSetting->username(); + m_password = gsmSetting->password(); + m_networkType = networkTypeStr(gsmSetting->networkType()); +} + +QString ProfileSettings::name() const +{ + return m_name; +} + +QString ProfileSettings::apn() const +{ + return m_apn; +} + +void ProfileSettings::setApn(const QString &apn) +{ + if (apn != m_apn) { + m_apn = apn; + Q_EMIT apnChanged(); + } +} + +QString ProfileSettings::user() const +{ + return m_user; +} + +void ProfileSettings::setUser(const QString &user) +{ + if (user != m_user) { + m_user = user; + Q_EMIT userChanged(); + } +} + +QString ProfileSettings::password() const +{ + return m_password; +} + +void ProfileSettings::setPassword(const QString &password) +{ + if (password != m_password) { + m_password = password; + Q_EMIT passwordChanged(); + } +} + +QString ProfileSettings::networkType() const +{ + return m_networkType; +} + +void ProfileSettings::setNetworkType(const QString &networkType) +{ + if (networkType != m_networkType) { + m_networkType = networkType; + Q_EMIT networkTypeChanged(); + } +} + +QString ProfileSettings::connectionUni() const +{ + return m_connectionUni; +} + +QString ProfileSettings::networkTypeStr(NetworkManager::GsmSetting::NetworkType networkType) +{ + if (networkType == NetworkManager::GsmSetting::NetworkType::Any) { + return QStringLiteral("Any"); + } else if (networkType == NetworkManager::GsmSetting::NetworkType::GprsEdgeOnly) { + return QStringLiteral("Only 2G"); + } else if (networkType == NetworkManager::GsmSetting::NetworkType::Only3G) { + return QStringLiteral("Only 3G"); + } else if (networkType == NetworkManager::GsmSetting::NetworkType::Only4GLte) { + return QStringLiteral("Only 4G"); + } else if (networkType == NetworkManager::GsmSetting::NetworkType::Prefer2G) { + return QStringLiteral("2G"); + } else if (networkType == NetworkManager::GsmSetting::NetworkType::Prefer3G) { + return QStringLiteral("3G/2G"); + } else if (networkType == NetworkManager::GsmSetting::NetworkType::Prefer4GLte) { + return QStringLiteral("4G/3G/2G"); + } + return QStringLiteral("Any"); +} + +NetworkManager::GsmSetting::NetworkType ProfileSettings::networkTypeFlag(const QString &networkType) +{ + if (networkType == QStringLiteral("Any")) { + return NetworkManager::GsmSetting::NetworkType::Any; + } else if (networkType == QStringLiteral("Only 2G")) { + return NetworkManager::GsmSetting::NetworkType::GprsEdgeOnly; + } else if (networkType == QStringLiteral("Only 3G")) { + return NetworkManager::GsmSetting::NetworkType::Only3G; + } else if (networkType == QStringLiteral("Only 4G")) { + return NetworkManager::GsmSetting::NetworkType::Only4GLte; + } else if (networkType == QStringLiteral("2G")) { + return NetworkManager::GsmSetting::NetworkType::Prefer2G; + } else if (networkType == QStringLiteral("3G/2G")) { + return NetworkManager::GsmSetting::NetworkType::Prefer3G; + } else if (networkType == QStringLiteral("4G/3G/2G")) { + return NetworkManager::GsmSetting::NetworkType::Prefer4GLte; + } + return NetworkManager::GsmSetting::NetworkType::Any; +} diff --git a/components/mmplugin/profilesettings.h b/components/mmplugin/profilesettings.h new file mode 100644 index 00000000..e0f51418 --- /dev/null +++ b/components/mmplugin/profilesettings.h @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2021-2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +class ProfileSettings : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString name READ name NOTIFY nameChanged) + Q_PROPERTY(QString apn READ apn WRITE setApn NOTIFY apnChanged) + Q_PROPERTY(QString user READ user WRITE setUser NOTIFY userChanged) + Q_PROPERTY(QString password READ password WRITE setPassword NOTIFY passwordChanged) + Q_PROPERTY(QString networkType READ networkType WRITE setNetworkType NOTIFY networkTypeChanged) + Q_PROPERTY(QString connectionUni READ connectionUni NOTIFY connectionUniChanged) + +public: + ProfileSettings(QObject *parent = nullptr) + : QObject{parent} + { + } + ProfileSettings(QObject *parent, + const QString &name, + const QString &apn, + const QString &user, + const QString &password, + NetworkManager::GsmSetting::NetworkType networkType, + const QString &connectionUni); + ProfileSettings(QObject *parent, NetworkManager::Setting::Ptr settings, NetworkManager::Connection::Ptr connection); + + QString name() const; + QString apn() const; + void setApn(const QString &apn); + QString user() const; + void setUser(const QString &user); + QString password() const; + void setPassword(const QString &password); + QString networkType() const; + void setNetworkType(const QString &ipType); + QString connectionUni() const; + + // utilities + static QString networkTypeStr(NetworkManager::GsmSetting::NetworkType networkType); + static NetworkManager::GsmSetting::NetworkType networkTypeFlag(const QString &networkType); + +Q_SIGNALS: + void nameChanged(); + void apnChanged(); + void userChanged(); + void passwordChanged(); + void networkTypeChanged(); + void connectionUniChanged(); + +private: + QString m_name; + QString m_apn; + QString m_user; + QString m_password; + QString m_networkType; + QString m_connectionUni; +}; diff --git a/components/mmplugin/signalindicator.cpp b/components/mmplugin/signalindicator.cpp index 3ebfd5b6..cff4dba3 100644 --- a/components/mmplugin/signalindicator.cpp +++ b/components/mmplugin/signalindicator.cpp @@ -11,6 +11,8 @@ #include +#include "signalindicator.h" + SignalIndicator::SignalIndicator(QObject *parent) : QObject{parent} , m_nmModem{nullptr} @@ -139,6 +141,160 @@ void SignalIndicator::setMobileDataEnabled(bool enabled) } } +QString SignalIndicator::activeConnectionUni() const +{ + if (m_nmModem && m_nmModem->activeConnection() && m_nmModem->activeConnection()->connection()) { + return m_nmModem->activeConnection()->connection()->uuid(); + } + return QString(); +} + +QList &SignalIndicator::profileList() +{ + return m_profileList; +} + +void SignalIndicator::refreshProfiles() +{ + m_profileList.clear(); + + if (!m_nmModem) { + Q_EMIT profileListChanged(); + qWarning() << "No NetworkManager modem found, cannot refresh profiles."; + return; + } + + for (auto connection : m_nmModem->availableConnections()) { + for (auto setting : connection->settings()->settings()) { + if (setting.dynamicCast()) { + m_profileList.append(new ProfileSettings(this, setting.dynamicCast(), connection)); + } + } + } + Q_EMIT profileListChanged(); +} + +void SignalIndicator::activateProfile(const QString &connectionUni) +{ + if (!m_nmModem) { + qWarning() << "Cannot activate profile since there is no NetworkManager modem"; + return; + } + + qDebug() << QStringLiteral("Activating profile on modem") << m_nmModem->uni() << QStringLiteral("for connection") << connectionUni << "."; + + NetworkManager::Connection::Ptr con; + + // disable autoconnect for all other connections + for (auto connection : m_nmModem->availableConnections()) { + if (connection->uuid() == connectionUni) { + connection->settings()->setAutoconnect(true); + con = connection; + } else { + connection->settings()->setAutoconnect(false); + } + } + + if (!con) { + qDebug() << QStringLiteral("Connection") << connectionUni << QStringLiteral("not found."); + return; + } + + // activate connection manually + // despite the documentation saying otherwise, activateConnection seems to need the DBus path, not uuid of the connection + QDBusPendingReply reply = NetworkManager::activateConnection(con->path(), m_nmModem->uni(), {}); + reply.waitForFinished(); + if (reply.isError()) { + qWarning() << QStringLiteral("Error activating connection:") << reply.error().message(); + return; + } +} + +void SignalIndicator::addProfile(const QString &name, const QString &apn, const QString &username, const QString &password, const QString &networkType) +{ + if (!m_nmModem) { + qWarning() << "Cannot add profile since there is no NetworkManager modem"; + return; + } + + NetworkManager::ConnectionSettings::Ptr settings{new NetworkManager::ConnectionSettings(NetworkManager::ConnectionSettings::Gsm)}; + settings->setId(name); + settings->setUuid(NetworkManager::ConnectionSettings::createNewUuid()); + settings->setAutoconnect(true); + settings->addToPermissions(KUser().loginName(), QString()); + + NetworkManager::GsmSetting::Ptr gsmSetting = settings->setting(NetworkManager::Setting::Gsm).dynamicCast(); + gsmSetting->setApn(apn); + gsmSetting->setUsername(username); + gsmSetting->setPassword(password); + gsmSetting->setPasswordFlags(password.isEmpty() ? NetworkManager::Setting::NotRequired : NetworkManager::Setting::AgentOwned); + gsmSetting->setNetworkType(ProfileSettings::networkTypeFlag(networkType)); + + gsmSetting->setInitialized(true); + + QDBusPendingReply reply = NetworkManager::addAndActivateConnection(settings->toMap(), m_nmModem->uni(), {}); + reply.waitForFinished(); + if (reply.isError()) { + qWarning() << "Error adding connection:" << reply.error().message(); + } else { + qDebug() << "Successfully added a new connection" << name << "with APN" << apn << "."; + } +} + +void SignalIndicator::removeProfile(const QString &connectionUni) +{ + NetworkManager::Connection::Ptr con = NetworkManager::findConnectionByUuid(connectionUni); + if (!con) { + qWarning() << "Could not find connection" << connectionUni << "to update!"; + return; + } + + QDBusPendingReply reply = con->remove(); + reply.waitForFinished(); + if (reply.isError()) { + qWarning() << "Error removing connection" << reply.error().message(); + } +} + +void SignalIndicator::updateProfile(const QString &connectionUni, + const QString &name, + const QString &apn, + const QString &username, + const QString &password, + const QString &networkType) +{ + NetworkManager::Connection::Ptr con = NetworkManager::findConnectionByUuid(connectionUni); + if (!con) { + qWarning() << "Could not find connection" << connectionUni << "to update!"; + return; + } + + NetworkManager::ConnectionSettings::Ptr conSettings = con->settings(); + if (!conSettings) { + qWarning() << "Could not find connection settings for" << connectionUni << "to update!"; + return; + } + + conSettings->setId(name); + + NetworkManager::GsmSetting::Ptr gsmSetting = conSettings->setting(NetworkManager::Setting::Gsm).dynamicCast(); + gsmSetting->setApn(apn); + gsmSetting->setUsername(username); + gsmSetting->setPassword(password); + gsmSetting->setPasswordFlags(password.isEmpty() ? NetworkManager::Setting::NotRequired : NetworkManager::Setting::AgentOwned); + gsmSetting->setNetworkType(ProfileSettings::networkTypeFlag(networkType)); + + gsmSetting->setInitialized(true); + + QDBusPendingReply reply = con->update(conSettings->toMap()); + reply.waitForFinished(); + if (reply.isError()) { + qWarning() << "Error updating connection settings for" << connectionUni << ":" << reply.error().message() << "."; + } else { + qDebug() << "Successfully updated connection settings" << connectionUni << "."; + } +} + void SignalIndicator::updateModemManagerModem() { m_modemDevice = nullptr; @@ -188,6 +344,14 @@ void SignalIndicator::updateNetworkManagerModem() connect(m_nmModem.get(), &NetworkManager::Device::stateChanged, this, &SignalIndicator::mobileDataEnabledChanged); connect(m_nmModem.get(), &NetworkManager::Device::availableConnectionAppeared, this, &SignalIndicator::mobileDataEnabledChanged); connect(m_nmModem.get(), &NetworkManager::Device::availableConnectionDisappeared, this, &SignalIndicator::mobileDataEnabledChanged); + + connect(m_nmModem.data(), &NetworkManager::ModemDevice::availableConnectionChanged, this, &SignalIndicator::refreshProfiles); + connect(m_nmModem.data(), &NetworkManager::ModemDevice::activeConnectionChanged, this, [this]() -> void { + refreshProfiles(); + Q_EMIT activeConnectionUniChanged(); + }); + + refreshProfiles(); } } diff --git a/components/mmplugin/signalindicator.h b/components/mmplugin/signalindicator.h index e0f40005..69d57c37 100644 --- a/components/mmplugin/signalindicator.h +++ b/components/mmplugin/signalindicator.h @@ -12,6 +12,8 @@ #include +#include "profilesettings.h" + // We make the assumption that there is only one modem. class SignalIndicator : public QObject { @@ -27,6 +29,8 @@ class SignalIndicator : public QObject Q_PROPERTY(bool mobileDataEnabled READ mobileDataEnabled WRITE setMobileDataEnabled NOTIFY mobileDataEnabledChanged) Q_PROPERTY(bool needsAPNAdded READ needsAPNAdded NOTIFY mobileDataEnabledChanged) + Q_PROPERTY(QList profiles READ profileList NOTIFY profileListChanged) + Q_PROPERTY(QString activeConnectionUni READ activeConnectionUni NOTIFY activeConnectionUniChanged) public: SignalIndicator(QObject *parent = nullptr); @@ -39,9 +43,23 @@ public: bool mobileDataSupported() const; bool mobileDataEnabled() const; bool needsAPNAdded() const; + QString activeConnectionUni() const; void setMobileDataEnabled(bool enabled); + // connection profiles + QList &profileList(); + void refreshProfiles(); + Q_INVOKABLE void activateProfile(const QString &connectionUni); + Q_INVOKABLE void addProfile(const QString &name, const QString &apn, const QString &username, const QString &password, const QString &networkType); + Q_INVOKABLE void removeProfile(const QString &connectionUni); + Q_INVOKABLE void updateProfile(const QString &connectionUni, + const QString &name, + const QString &apn, + const QString &username, + const QString &password, + const QString &networkType); + Q_SIGNALS: void strengthChanged(); void nameChanged(); @@ -50,6 +68,8 @@ Q_SIGNALS: void simEmptyChanged(); void mobileDataSupportedChanged(); void mobileDataEnabledChanged(); + void profileListChanged(); + void activeConnectionUniChanged(); private: NetworkManager::ModemDevice::Ptr m_nmModem; @@ -57,6 +77,8 @@ private: ModemManager::Modem::Ptr m_modem; ModemManager::Modem3gpp::Ptr m_3gppModem; + QList m_profileList; + void updateModemManagerModem(); void updateNetworkManagerModem(); }; diff --git a/envmanager/settings.cpp b/envmanager/settings.cpp index 507c4788..f8ed3a2a 100644 --- a/envmanager/settings.cpp +++ b/envmanager/settings.cpp @@ -14,7 +14,6 @@ #include const QString CONFIG_FILE = QStringLiteral("plasmamobilerc"); -const QString INITIAL_START_CONFIG_GROUP = QStringLiteral("InitialStart"); const QString SAVED_CONFIG_GROUP = QStringLiteral("SavedConfig"); const QString MOBILE_LOOK_AND_FEEL = QStringLiteral("org.kde.breeze.mobile"); diff --git a/initialstart/CMakeLists.txt b/initialstart/CMakeLists.txt new file mode 100644 index 00000000..ee1389e2 --- /dev/null +++ b/initialstart/CMakeLists.txt @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: 2022 Devin Lin +# SPDX-License-Identifier: GPL-2.0-or-later + +set(plasma-mobile-initial-start_SRCS + main.cpp + wizard.cpp + settings.cpp + utils.h +) + +qt_add_resources(RESOURCES resources.qrc) + +add_subdirectory(modules) + +add_executable(plasma-mobile-initial-start ${plasma-mobile-initial-start_SRCS} ${RESOURCES}) +target_link_libraries(plasma-mobile-initial-start + Qt::Qml + Qt::Gui + Qt::Widgets + Qt::Quick + KF6::I18n + KF6::ConfigCore + KF6::ConfigGui + KF6::CoreAddons + KF6::DBusAddons + KF6::Package +) + +target_include_directories(plasma-mobile-initial-start PRIVATE ${CMAKE_BINARY_DIR}) +install(TARGETS plasma-mobile-initial-start ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/initialstart/Messages.sh b/initialstart/Messages.sh new file mode 100644 index 00000000..05685a83 --- /dev/null +++ b/initialstart/Messages.sh @@ -0,0 +1,6 @@ +#! /usr/bin/env bash + +# SPDX-FileCopyrightText: 2023 Devin Lin +# SPDX-License-Identifier: GPL-2.0-or-later + +$XGETTEXT `find . -name \*.js -o -name \*.qml -o -name \*.cpp` -o $podir/plasma_org.kde.plasma.mobileinitialstart.pot diff --git a/initialstart/assets/background.png b/initialstart/assets/background.png new file mode 100644 index 00000000..f70235a2 Binary files /dev/null and b/initialstart/assets/background.png differ diff --git a/initialstart/assets/background.png.license b/initialstart/assets/background.png.license new file mode 100644 index 00000000..1c196758 --- /dev/null +++ b/initialstart/assets/background.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: None +SPDX-License-Identifier: CC0-1.0 + diff --git a/initialstart/main.cpp b/initialstart/main.cpp new file mode 100644 index 00000000..eabdbbe2 --- /dev/null +++ b/initialstart/main.cpp @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: LGPL-2.0-or-later + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "settings.h" +#include "version.h" +#include "wizard.h" + +QCommandLineParser *createParser() +{ + QCommandLineParser *parser = new QCommandLineParser; + parser->addOption(QCommandLineOption(QStringLiteral("test-wizard"), i18n("Opens the initial start wizard without modifying configuration"))); + parser->addVersionOption(); + parser->addHelpOption(); + return parser; +} + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + // parse command + QScopedPointer parser{createParser()}; + parser->process(app); + + bool testWizard = parser->isSet(QStringLiteral("test-wizard")); + + if (!testWizard) { + // if the wizard has already been run, or we aren't in plasma mobile + if (!Settings::self()->shouldStartWizard()) { + qDebug() << "Wizard will not be started since either it has already been run, or the current session is not Plasma Mobile."; + return 0; + } + } + + // start wizard + KLocalizedString::setApplicationDomain("plasma-mobile-initial-start"); + KAboutData aboutData(QStringLiteral("plasma-mobile-initial-start"), + QStringLiteral("Initial Start"), + QStringLiteral(PLASMA_MOBILE_VERSION_STRING), + QStringLiteral(""), + KAboutLicense::GPL, + i18n("© 2023 KDE Community")); + aboutData.addAuthor(i18n("Devin Lin"), QString(), QStringLiteral("devin@kde.org")); + KAboutData::setApplicationData(aboutData); + + QQmlApplicationEngine *engine = new QQmlApplicationEngine; + engine->rootContext()->setContextObject(new KLocalizedContext{engine}); + + Wizard *wizard = new Wizard{nullptr, engine}; + wizard->setTestingMode(testWizard); + wizard->load(); + + qmlRegisterSingletonType("initialstart", 1, 0, "Wizard", [wizard](QQmlEngine *, QJSEngine *) -> QObject * { + return wizard; + }); + + engine->load(QUrl(QStringLiteral("qrc:/qml/main.qml"))); + + app.setWindowIcon(QIcon::fromTheme(QStringLiteral("start-here-symbolic"))); + + return app.exec(); +} diff --git a/initialstart/modules/CMakeLists.txt b/initialstart/modules/CMakeLists.txt new file mode 100644 index 00000000..fb826a0f --- /dev/null +++ b/initialstart/modules/CMakeLists.txt @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2023 Devin Lin +# SPDX-License-Identifier: GPL-2.0-or-later + +plasma_install_package(finished org.kde.plasma.mobileinitialstart.finished mobileinitialstart) +add_subdirectory(cellular) +add_subdirectory(prepare) +add_subdirectory(time) +add_subdirectory(wifi) diff --git a/initialstart/modules/cellular/CMakeLists.txt b/initialstart/modules/cellular/CMakeLists.txt new file mode 100644 index 00000000..a6430554 --- /dev/null +++ b/initialstart/modules/cellular/CMakeLists.txt @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: 2023 Devin Lin +# SPDX-License-Identifier: GPL-2.0-or-later + +set(cellularplugin_SRCS + cellularplugin.cpp +) + +add_library(cellularplugin ${cellularplugin_SRCS}) + +target_link_libraries(cellularplugin + Qt::Core + Qt::DBus + Qt::Quick + KF6::CoreAddons + KF6::ConfigCore + KF6::ConfigGui + KF6::I18n + KF6::Notifications + KF6::NetworkManagerQt + KF6::ModemManagerQt +) + +set_property(TARGET cellularplugin PROPERTY LIBRARY_OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/org/kde/plasma/mobileinitialstart/cellular) +file(COPY qmldir DESTINATION ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/org/kde/plasma/mobileinitialstart/cellular) + +install(TARGETS cellularplugin DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/mobileinitialstart/cellular) +install(FILES qmldir ${qml_SRC} DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/mobileinitialstart/cellular) + +plasma_install_package(package org.kde.plasma.mobileinitialstart.cellular mobileinitialstart) + + + + diff --git a/initialstart/modules/cellular/cellularplugin.cpp b/initialstart/modules/cellular/cellularplugin.cpp new file mode 100644 index 00000000..b2cef28c --- /dev/null +++ b/initialstart/modules/cellular/cellularplugin.cpp @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2023 by Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "cellularplugin.h" + +#include +#include + +void CellularPlugin::registerTypes(const char *uri) +{ + Q_ASSERT(QLatin1String(uri) == QLatin1String("org.kde.plasma.mobileinitialstart.cellular")); +} diff --git a/initialstart/modules/cellular/cellularplugin.h b/initialstart/modules/cellular/cellularplugin.h new file mode 100644 index 00000000..78e74ed0 --- /dev/null +++ b/initialstart/modules/cellular/cellularplugin.h @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2023 by Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include +#include + +class CellularPlugin : public QQmlExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") + +public: + void registerTypes(const char *uri) override; +}; diff --git a/initialstart/modules/cellular/package/contents/ui/EditProfileDialog.qml b/initialstart/modules/cellular/package/contents/ui/EditProfileDialog.qml new file mode 100644 index 00000000..4d920601 --- /dev/null +++ b/initialstart/modules/cellular/package/contents/ui/EditProfileDialog.qml @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: 2020-2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick 2.12 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.12 as Controls + +import org.kde.kirigami 2.19 as Kirigami + +import org.kde.plasma.mm 1.0 as PlasmaMM + +Kirigami.Dialog { + id: dialog + title: i18n("Edit APN") + clip: true + + property var profile + + standardButtons: Controls.Dialog.Ok | Controls.Dialog.Cancel + + onAccepted: { + if (profile === null) { // create new profile + PlasmaMM.SignalIndicator.addProfile(profileName.text, profileApn.text, profileUsername.text, profilePassword.text, profileNetworkType.value); + } else { // edit existing profile + PlasmaMM.SignalIndicator.updateProfile(profile.connectionUni, profileName.text, profileApn.text, profileUsername.text, profilePassword.text, profileNetworkType.value); + } + } + preferredWidth: Kirigami.Units.gridUnit * 20 + padding: Kirigami.Units.gridUnit + + ColumnLayout { + Kirigami.FormLayout { + Layout.fillWidth: true + wideMode: false + + Controls.TextField { + id: profileName + Kirigami.FormData.label: i18n("Name") + text: profile !== null ? profile.name : "" + } + Controls.TextField { + id: profileApn + Kirigami.FormData.label: i18n("APN") + text: profile != null ? profile.apn : "" + } + Controls.TextField { + id: profileUsername + Kirigami.FormData.label: i18n("Username") + text: profile != null ? profile.user : "" + } + Controls.TextField { + id: profilePassword + Kirigami.FormData.label: i18n("Password") + text: profile != null ? profile.password : "" + } + Controls.ComboBox { + id: profileNetworkType + Kirigami.FormData.label: i18n("Network type") + model: [i18n("4G/3G/2G"), i18n("3G/2G"), i18n("2G"), i18n("Only 4G"), i18n("Only 3G"), i18n("Only 2G"), i18n("Any")] + Component.onCompleted: if (profile !== null) { + currentIndex = indexOfValue(profile.networkType); + } + } + } + } +} diff --git a/initialstart/modules/cellular/package/contents/ui/main.qml b/initialstart/modules/cellular/package/contents/ui/main.qml new file mode 100644 index 00000000..5855613f --- /dev/null +++ b/initialstart/modules/cellular/package/contents/ui/main.qml @@ -0,0 +1,152 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import org.kde.kirigami 2.20 as Kirigami +import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm +import org.kde.plasma.mm 1.0 as PlasmaMM + +Item { + id: root + property string name: i18n("Cellular") + + readonly property real cardWidth: Math.min(Kirigami.Units.gridUnit * 30, root.width - Kirigami.Units.gridUnit * 2) + + function toggleMobileData() { + if (PlasmaMM.SignalIndicator.needsAPNAdded || !PlasmaMM.SignalIndicator.mobileDataSupported) { + // open settings if unable to toggle mobile data + MobileShell.ShellUtil.executeCommand("plasma-open-settings kcm_cellular_network"); + } else { + PlasmaMM.SignalIndicator.mobileDataEnabled = !PlasmaMM.SignalIndicator.mobileDataEnabled; + } + } + + EditProfileDialog { + id: profileDialog + profile: null + } + + ColumnLayout { + anchors { + fill: parent + topMargin: Kirigami.Units.gridUnit + bottomMargin: Kirigami.Units.largeSpacing + } + width: root.width + spacing: Kirigami.Units.gridUnit + + Label { + Layout.leftMargin: Kirigami.Units.gridUnit + Layout.rightMargin: Kirigami.Units.gridUnit + Layout.alignment: Qt.AlignTop + Layout.fillWidth: true + + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + text: { + if (!PlasmaMM.SignalIndicator.modemAvailable) { + return i18n("Your device does not have a modem available."); + } else if (PlasmaMM.SignalIndicator.needsAPNAdded) { + return i18n("Please configure your APN below for mobile data, further information will be available with your carrier."); + } else if (PlasmaMM.SignalIndicator.mobileDataSupported) { + return i18n("You are connected to the mobile network."); + } else if (PlasmaMM.SignalIndicator.simEmpty) { + return i18n("Please insert a SIM card into your device."); + } else { + return i18n("Your device does not have a modem available."); + } + } + } + + MobileForm.FormCard { + visible: PlasmaMM.SignalIndicator.modemAvailable && PlasmaMM.SignalIndicator.mobileDataSupported + maximumWidth: root.cardWidth + + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + Layout.fillWidth: true + + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormSwitchDelegate { + Layout.fillWidth: true + text: i18n("Mobile Data") + checked: PlasmaMM.SignalIndicator.mobileDataEnabled + onCheckedChanged: { + if (checked !== PlasmaMM.SignalIndicator.mobileDataEnabled) { + root.toggleMobileData(); + } + } + } + } + } + + MobileForm.FormCard { + visible: PlasmaMM.SignalIndicator.modemAvailable && !PlasmaMM.SignalIndicator.simEmpty + maximumWidth: root.cardWidth + + Layout.fillHeight: true + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + Layout.fillWidth: true + + contentItem: ColumnLayout { + spacing: 0 + + ListView { + id: listView + currentIndex: -1 + clip: true + + Layout.fillWidth: true + Layout.fillHeight: true + + model: PlasmaMM.SignalIndicator.profiles + + delegate: MobileForm.FormRadioDelegate { + width: listView.width + text: modelData.name + description: modelData.apn + checked: modem.activeConnectionUni == modelData.connectionUni + + onCheckedChanged: { + if (checked) { + PlasmaMM.SignalIndicator.activateProfile(modelData.connectionUni); + checked = Qt.binding(() => { return modem.activeConnectionUni == modelData.connectionUni }); + } + } + + trailing: RowLayout { + ToolButton { + icon.name: "entry-edit" + text: i18n("Edit") + onClicked: { + profileDialog.profile = modelData; + profileDialog.open(); + } + } + ToolButton { + icon.name: "delete" + text: i18n("Delete") + onClicked: PlasmaMM.SignalIndicator.removeProfile(modelData.connectionUni) + } + } + } + } + + MobileForm.FormButtonDelegate { + icon.name: "list-add" + text: i18n("Add APN") + onClicked: { + profileDialog.profile = null; + profileDialog.open(); + } + } + } + } + } +} + + diff --git a/initialstart/modules/cellular/package/metadata.json b/initialstart/modules/cellular/package/metadata.json new file mode 100644 index 00000000..7cbb7135 --- /dev/null +++ b/initialstart/modules/cellular/package/metadata.json @@ -0,0 +1,20 @@ +{ + "KPackageStructure": "KPackage/GenericQML", + "KPlugin": { + "Authors": [ + { + "Email": "devin@kde.org", + "Name": "Devin Lin" + } + ], + "Description": "Cellular initialstart module for Plasma Mobile", + "Id": "org.kde.plasma.mobileinitialstart.cellular", + "License": "GPL-2.0+", + "Name": "Cellular", + "ServiceTypes": [ + "KPackage/GenericQML" + ], + "Version": "0.1", + "Website": "https://kde.org" + } +} diff --git a/initialstart/modules/cellular/qmldir b/initialstart/modules/cellular/qmldir new file mode 100644 index 00000000..175a6c89 --- /dev/null +++ b/initialstart/modules/cellular/qmldir @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2023 Devin Lin +# SPDX-License-Identifier: GPL-2.0-or-later + +module org.kde.plasma.mobileinitialstart.cellular +plugin cellularplugin +classname CellularPlugin + diff --git a/initialstart/modules/finished/contents/ui/konqi-calling.png b/initialstart/modules/finished/contents/ui/konqi-calling.png new file mode 100644 index 00000000..f00af963 Binary files /dev/null and b/initialstart/modules/finished/contents/ui/konqi-calling.png differ diff --git a/initialstart/modules/finished/contents/ui/konqi-calling.png.license b/initialstart/modules/finished/contents/ui/konqi-calling.png.license new file mode 100644 index 00000000..1c196758 --- /dev/null +++ b/initialstart/modules/finished/contents/ui/konqi-calling.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: None +SPDX-License-Identifier: CC0-1.0 + diff --git a/initialstart/modules/finished/contents/ui/main.qml b/initialstart/modules/finished/contents/ui/main.qml new file mode 100644 index 00000000..cc6e157d --- /dev/null +++ b/initialstart/modules/finished/contents/ui/main.qml @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import org.kde.kirigami 2.20 as Kirigami + +Item { + id: root + + property string name: i18n("Complete!") + + ColumnLayout { + anchors.fill: parent + anchors.margins: Kirigami.Units.gridUnit + + Label { + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter | Qt.AlignTop + text: i18n("Your device is now ready.

Enjoy Plasma 6.0!") + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + } + + Item { Layout.fillHeight: true } + + Image { + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + fillMode: Image.PreserveAspectFit + source: "konqi-calling.png" + } + } +} + diff --git a/initialstart/modules/finished/metadata.json b/initialstart/modules/finished/metadata.json new file mode 100644 index 00000000..c0439f66 --- /dev/null +++ b/initialstart/modules/finished/metadata.json @@ -0,0 +1,20 @@ +{ + "KPackageStructure": "KPackage/GenericQML", + "KPlugin": { + "Authors": [ + { + "Email": "devin@kde.org", + "Name": "Devin Lin" + } + ], + "Description": "Finished initialstart module for Plasma Mobile", + "Id": "org.kde.plasma.mobileinitialstart.finished", + "License": "GPL-2.0+", + "Name": "Finished", + "ServiceTypes": [ + "KPackage/GenericQML" + ], + "Version": "0.1", + "Website": "https://kde.org" + } +} diff --git a/initialstart/modules/prepare/CMakeLists.txt b/initialstart/modules/prepare/CMakeLists.txt new file mode 100644 index 00000000..0cb8ba8c --- /dev/null +++ b/initialstart/modules/prepare/CMakeLists.txt @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: 2023 Devin Lin +# SPDX-License-Identifier: GPL-2.0-or-later + +set(prepareplugin_SRCS + prepareplugin.cpp + prepareutil.cpp +) + +add_library(prepareplugin ${prepareplugin_SRCS}) + +target_link_libraries(prepareplugin + Qt::Core + Qt::DBus + Qt::Quick + KF6::CoreAddons + KF6::ConfigCore + KF6::ConfigGui + KF6::I18n + KF6::Notifications +) + +set_property(TARGET prepareplugin PROPERTY LIBRARY_OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/org/kde/plasma/mobileinitialstart/prepare) +file(COPY qmldir DESTINATION ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/org/kde/plasma/mobileinitialstart/prepare) + +install(TARGETS prepareplugin DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/mobileinitialstart/prepare) +install(FILES qmldir ${qml_SRC} DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/mobileinitialstart/prepare) + +plasma_install_package(package org.kde.plasma.mobileinitialstart.prepare mobileinitialstart) + + + diff --git a/initialstart/modules/prepare/package/contents/ui/main.qml b/initialstart/modules/prepare/package/contents/ui/main.qml new file mode 100644 index 00000000..7518eb80 --- /dev/null +++ b/initialstart/modules/prepare/package/contents/ui/main.qml @@ -0,0 +1,163 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import org.kde.kirigami 2.20 as Kirigami +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm +import org.kde.plasma.mobileinitialstart.prepare 1.0 as Prepare +import org.kde.plasma.plasma5support 2.0 as P5Support + +Item { + id: root + property string name: i18n("Before we get started…") + + readonly property real cardWidth: Math.min(Kirigami.Units.gridUnit * 30, root.width - Kirigami.Units.gridUnit * 2) + + // brightness controls + property int screenBrightness: 0 + property bool disableBrightnessUpdate: true + readonly property int maximumScreenBrightness: pmSource.data["PowerDevil"] ? pmSource.data["PowerDevil"]["Maximum Screen Brightness"] || 0 : 0 + property QtObject updateScreenBrightnessJob + + function updateBrightnessUI() { + if (updateScreenBrightnessJob) + return; + + root.disableBrightnessUpdate = true; + root.screenBrightness = pmSource.data["PowerDevil"]["Screen Brightness"]; + root.disableBrightnessUpdate = false; + } + + onScreenBrightnessChanged: { + brightnessSlider.value = root.screenBrightness + + if (!disableBrightnessUpdate) { + const service = pmSource.serviceForSource("PowerDevil"); + const operation = service.operationDescription("setBrightness"); + operation.brightness = screenBrightness; + operation.silent = true; // don't show OSD + + updateScreenBrightnessJob = service.startOperationCall(operation); + updateScreenBrightnessJob.finished.connect(function (job) { + root.updateBrightnessUI(); + }); + } + } + + P5Support.DataSource { + id: pmSource + engine: "powermanagement" + connectedSources: ["PowerDevil"] + onSourceAdded: if (source === "PowerDevil") { + disconnectSource(source); + connectSource(source); + } + onDataChanged: root.updateBrightnessUI() + } + + ScrollView { + anchors { + fill: parent + topMargin: Kirigami.Units.gridUnit + } + + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + contentWidth: -1 + + ColumnLayout { + width: root.width + spacing: Kirigami.Units.gridUnit + + Label { + Layout.leftMargin: Kirigami.Units.gridUnit + Layout.rightMargin: Kirigami.Units.gridUnit + Layout.alignment: Qt.AlignTop + Layout.fillWidth: true + + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + text: i18n("Adjust the screen brightness to be comfortable for the installation process.") + } + + MobileForm.FormCard { + maximumWidth: root.cardWidth + + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + Layout.fillWidth: true + + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.AbstractFormDelegate { + Layout.fillWidth: true + + background: Item {} + + contentItem: RowLayout { + spacing: Kirigami.Units.gridUnit + + Kirigami.Icon { + implicitWidth: Kirigami.Units.iconSizes.smallMedium + implicitHeight: Kirigami.Units.iconSizes.smallMedium + source: "brightness-low" + } + + Slider { + id: brightnessSlider + Layout.fillWidth: true + from: 1 + to: root.maximumScreenBrightness + value: root.screenBrightness + onMoved: root.screenBrightness = value; + } + + Kirigami.Icon { + implicitWidth: Kirigami.Units.iconSizes.smallMedium + implicitHeight: Kirigami.Units.iconSizes.smallMedium + source: "brightness-high" + } + } + } + } + } + + Label { + Layout.leftMargin: Kirigami.Units.gridUnit + Layout.rightMargin: Kirigami.Units.gridUnit + Layout.alignment: Qt.AlignTop + Layout.fillWidth: true + + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + text: i18n("Adjust the size of elements on the screen.") + } + + MobileForm.FormCard { + maximumWidth: root.cardWidth + + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + Layout.fillWidth: true + + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormComboBoxDelegate { + id: displayScaling + Layout.fillWidth: true + text: i18n("Display Scaling") + displayMode: MobileForm.FormComboBoxDelegate.Dialog + currentIndex: Prepare.PrepareUtil.scalingOptions.indexOf(Prepare.PrepareUtil.scaling.toString() + "%"); + model: Prepare.PrepareUtil.scalingOptions + + // remove % suffix + onCurrentValueChanged: Prepare.PrepareUtil.scaling = parseInt(currentValue.substring(0, currentValue.length - 1)); + } + } + } + } + } +} diff --git a/initialstart/modules/prepare/package/metadata.json b/initialstart/modules/prepare/package/metadata.json new file mode 100644 index 00000000..7e1e855e --- /dev/null +++ b/initialstart/modules/prepare/package/metadata.json @@ -0,0 +1,20 @@ +{ + "KPackageStructure": "KPackage/GenericQML", + "KPlugin": { + "Authors": [ + { + "Email": "devin@kde.org", + "Name": "Devin Lin" + } + ], + "Description": "Prepare initialstart module for Plasma Mobile", + "Id": "org.kde.plasma.mobileinitialstart.prepare", + "License": "GPL-2.0+", + "Name": "Prepare", + "ServiceTypes": [ + "KPackage/GenericQML" + ], + "Version": "0.1", + "Website": "https://kde.org" + } +} diff --git a/initialstart/modules/prepare/prepareplugin.cpp b/initialstart/modules/prepare/prepareplugin.cpp new file mode 100644 index 00000000..c6c049a1 --- /dev/null +++ b/initialstart/modules/prepare/prepareplugin.cpp @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2023 by Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "prepareplugin.h" + +#include +#include + +#include "prepareutil.h" + +void PreparePlugin::registerTypes(const char *uri) +{ + Q_ASSERT(QLatin1String(uri) == QLatin1String("org.kde.plasma.mobileinitialstart.prepare")); + + qmlRegisterSingletonType(uri, 1, 0, "PrepareUtil", [](QQmlEngine *, QJSEngine *) { + return new PrepareUtil; + }); +} diff --git a/initialstart/modules/prepare/prepareplugin.h b/initialstart/modules/prepare/prepareplugin.h new file mode 100644 index 00000000..454bc578 --- /dev/null +++ b/initialstart/modules/prepare/prepareplugin.h @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2023 by Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include +#include + +class PreparePlugin : public QQmlExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") + +public: + void registerTypes(const char *uri) override; +}; diff --git a/initialstart/modules/prepare/prepareutil.cpp b/initialstart/modules/prepare/prepareutil.cpp new file mode 100644 index 00000000..eaa4d7ce --- /dev/null +++ b/initialstart/modules/prepare/prepareutil.cpp @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: 2023 by Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "prepareutil.h" + +#include +#include + +PrepareUtil::PrepareUtil(QObject *parent) + : QObject{parent} + , m_process{new QProcess{this}} +{ + connect(m_process, QOverload::of(&QProcess::finished), this, &PrepareUtil::receiveScalingFactor); + + // HACK: we are using kscreen-doctor to determine scaling, should switch to API + m_process->start("kscreen-doctor", {"-o"}); +} + +int PrepareUtil::scaling() const +{ + return m_scaling; +} + +void PrepareUtil::setScaling(int scaling) +{ + if (scaling != m_scaling) { + const QString scalingNum = QString::number(((double)scaling) / 100); + qDebug() << "scaling" << scalingNum; + + m_process->start("kscreen-doctor", {"output." + m_display + ".scale." + scalingNum}); + + m_scaling = scaling; + Q_EMIT scalingChanged(); + } +} + +QStringList PrepareUtil::scalingOptions() +{ + return {"50%", "100%", "150%", "200%", "250%", "300%"}; +} + +void PrepareUtil::receiveScalingFactor(int exitCode, QProcess::ExitStatus exitStatus) +{ + Q_UNUSED(exitCode) + Q_UNUSED(exitStatus) + + // only trigger this slot once, on first time + disconnect(m_process, QOverload::of(&QProcess::finished), this, &PrepareUtil::receiveScalingFactor); + + // remove ansi color codes + const auto ansiEscape = QRegularExpression{"\\\u001B\\[.*?m"}; + const auto output = QString::fromUtf8(m_process->readAllStandardOutput()).replace(ansiEscape, "").replace("\\n", " "); + auto split = output.split(" "); + + // HACK: hardcode how we get the output from kscreen-doctor + // we assume the first display is the phone screen + for (int i = 0; i < split.size(); ++i) { + if (i == 2) { + m_display = split[i]; + } else if (split[i] == "Scale:") { + if (i + 1 < split.size()) { + m_scaling = split[i + 1].toDouble() * 100; + Q_EMIT scalingChanged(); + } + + break; + } + } +} diff --git a/initialstart/modules/prepare/prepareutil.h b/initialstart/modules/prepare/prepareutil.h new file mode 100644 index 00000000..49f536a0 --- /dev/null +++ b/initialstart/modules/prepare/prepareutil.h @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2023 by Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +class PrepareUtil : public QObject +{ + Q_OBJECT + Q_PROPERTY(int scaling READ scaling WRITE setScaling NOTIFY scalingChanged); + Q_PROPERTY(QStringList scalingOptions READ scalingOptions CONSTANT); + +public: + PrepareUtil(QObject *parent = nullptr); + + int scaling() const; + void setScaling(int scaling); + + QStringList scalingOptions(); + +Q_SIGNALS: + void scalingChanged(); + +public Q_SLOTS: + void receiveScalingFactor(int exitCode, QProcess::ExitStatus exitStatus); + +private: + int m_scaling; + QString m_display; + + QProcess *m_process; +}; diff --git a/initialstart/modules/prepare/qmldir b/initialstart/modules/prepare/qmldir new file mode 100644 index 00000000..9e423d4e --- /dev/null +++ b/initialstart/modules/prepare/qmldir @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: 2023 Devin Lin +# SPDX-License-Identifier: GPL-2.0-or-later + +module org.kde.plasma.mobileinitialstart.prepare +plugin prepareplugin +classname PreparePlugin diff --git a/initialstart/modules/time/CMakeLists.txt b/initialstart/modules/time/CMakeLists.txt new file mode 100644 index 00000000..27a890a5 --- /dev/null +++ b/initialstart/modules/time/CMakeLists.txt @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: 2023 Devin Lin +# SPDX-License-Identifier: GPL-2.0-or-later + +set(timeplugin_SRCS + timeplugin.cpp + timeutil.cpp + timezonemodel.cpp +) + +add_library(timeplugin ${timeplugin_SRCS}) + +target_link_libraries(timeplugin + Qt::Core + Qt::DBus + Qt::Quick + KF6::CoreAddons + KF6::ConfigCore + KF6::ConfigGui + KF6::I18n + KF6::Notifications +) + +set_property(TARGET timeplugin PROPERTY LIBRARY_OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/org/kde/plasma/mobileinitialstart/time) +file(COPY qmldir DESTINATION ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/org/kde/plasma/mobileinitialstart/time) + +install(TARGETS timeplugin DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/mobileinitialstart/time) +install(FILES qmldir ${qml_SRC} DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/mobileinitialstart/time) + +plasma_install_package(package org.kde.plasma.mobileinitialstart.time mobileinitialstart) + + + + diff --git a/initialstart/modules/time/package/contents/ui/main.qml b/initialstart/modules/time/package/contents/ui/main.qml new file mode 100644 index 00000000..1f151bd1 --- /dev/null +++ b/initialstart/modules/time/package/contents/ui/main.qml @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import org.kde.kirigami 2.20 as Kirigami +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm +import org.kde.plasma.mobileinitialstart.time 1.0 as Time + +Item { + id: root + property string name: i18n("Time and Date") + + readonly property real cardWidth: Math.min(Kirigami.Units.gridUnit * 30, root.width - Kirigami.Units.gridUnit * 2) + + ColumnLayout { + anchors { + fill: parent + topMargin: Kirigami.Units.gridUnit + bottomMargin: Kirigami.Units.largeSpacing + } + + width: root.width + spacing: Kirigami.Units.gridUnit + + Label { + Layout.leftMargin: Kirigami.Units.gridUnit + Layout.rightMargin: Kirigami.Units.gridUnit + Layout.alignment: Qt.AlignTop + Layout.fillWidth: true + + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + text: i18n("Select your time zone and preferred time format.") + } + + MobileForm.FormCard { + maximumWidth: root.cardWidth + + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + Layout.fillWidth: true + + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormSwitchDelegate { + Layout.fillWidth: true + text: i18n("24-Hour Format") + checked: Time.TimeUtil.is24HourTime + onCheckedChanged: { + if (checked !== Time.TimeUtil.is24HourTime) { + Time.TimeUtil.is24HourTime = checked; + } + } + } + } + } + + MobileForm.FormCard { + maximumWidth: root.cardWidth + + Layout.fillHeight: true + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + Layout.fillWidth: true + + contentItem: ColumnLayout { + spacing: 0 + + ListView { + clip: true + id: listView + Layout.fillWidth: true + Layout.fillHeight: true + model: Time.TimeUtil.timeZones + + header: Control { + width: listView.width + leftPadding: Kirigami.Units.largeSpacing + rightPadding: Kirigami.Units.largeSpacing + topPadding: Kirigami.Units.largeSpacing + bottomPadding: Kirigami.Units.largeSpacing + + contentItem: Kirigami.SearchField { + id: searchField + + onTextChanged: { + Time.TimeUtil.timeZones.filterString = text; + // HACK: search field seems to lose focus every time the text changes + focusTimer.restart(); + } + + Timer { + id: focusTimer + interval: 1 + onTriggered: searchField.forceActiveFocus() + } + } + } + + delegate: MobileForm.FormRadioDelegate { + required property string timeZoneId + + width: ListView.view.width + text: timeZoneId + checked: Time.TimeUtil.currentTimeZone === timeZoneId + onCheckedChanged: { + if (checked && timeZoneId !== Time.TimeUtil.currentTimeZone) { + Time.TimeUtil.currentTimeZone = model.timeZoneId; + checked = Qt.binding(() => Time.TimeUtil.currentTimeZone === timeZoneId); + } + } + } + } + } + } + } +} + diff --git a/initialstart/modules/time/package/metadata.json b/initialstart/modules/time/package/metadata.json new file mode 100644 index 00000000..6a62aa47 --- /dev/null +++ b/initialstart/modules/time/package/metadata.json @@ -0,0 +1,21 @@ +{ + "KPackageStructure": "KPackage/GenericQML", + "KPlugin": { + "Authors": [ + { + "Email": "devin@kde.org", + "Name": "Devin Lin" + } + ], + "Description": "Time initialstart module for Plasma Mobile", + "Id": "org.kde.plasma.mobileinitialstart.time", + "License": "GPL-2.0+", + "Name": "Time", + "ServiceTypes": [ + "KPackage/GenericQML" + ], + "Version": "0.1", + "Website": "https://kde.org" + } +} + diff --git a/initialstart/modules/time/qmldir b/initialstart/modules/time/qmldir new file mode 100644 index 00000000..080c4e48 --- /dev/null +++ b/initialstart/modules/time/qmldir @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2023 Devin Lin +# SPDX-License-Identifier: GPL-2.0-or-later + +module org.kde.plasma.mobileinitialstart.time +plugin timeplugin +classname TimePlugin + diff --git a/initialstart/modules/time/timeplugin.cpp b/initialstart/modules/time/timeplugin.cpp new file mode 100644 index 00000000..44be0872 --- /dev/null +++ b/initialstart/modules/time/timeplugin.cpp @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2023 by Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "timeplugin.h" + +#include +#include + +#include "timeutil.h" + +void TimePlugin::registerTypes(const char *uri) +{ + Q_ASSERT(QLatin1String(uri) == QLatin1String("org.kde.plasma.mobileinitialstart.time")); + + qmlRegisterSingletonType(uri, 1, 0, "TimeUtil", [](QQmlEngine *, QJSEngine *) { + return new TimeUtil; + }); +} + +// #include "moc_flashlightplugin.cpp" diff --git a/initialstart/modules/time/timeplugin.h b/initialstart/modules/time/timeplugin.h new file mode 100644 index 00000000..e1da02d4 --- /dev/null +++ b/initialstart/modules/time/timeplugin.h @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2023 by Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include +#include + +class TimePlugin : public QQmlExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") + +public: + void registerTypes(const char *uri) override; +}; diff --git a/initialstart/modules/time/timeutil.cpp b/initialstart/modules/time/timeutil.cpp new file mode 100644 index 00000000..c369e8fd --- /dev/null +++ b/initialstart/modules/time/timeutil.cpp @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: 2023 by Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "timeutil.h" + +#include +#include +#include + +#include +#include + +#define FORMAT24H "HH:mm:ss" +#define FORMAT12H "h:mm:ss ap" + +TimeUtil::TimeUtil(QObject *parent) + : QObject{parent} + , m_timeZoneModel{new TimeZoneModel{this}} + , m_filterModel{new TimeZoneFilterProxy{this}} +{ + m_filterModel->setSourceModel(m_timeZoneModel); +} + +bool TimeUtil::is24HourTime() const +{ + return m_is24HourTime; +} + +void TimeUtil::setIs24HourTime(bool is24HourTime) +{ + if (is24HourTime != m_is24HourTime) { + auto config = KSharedConfig::openConfig(QStringLiteral("kdeglobals"), KConfig::SimpleConfig); + auto group = KConfigGroup(config, "Locale"); + group.writeEntry(QStringLiteral("TimeFormat"), is24HourTime ? FORMAT24H : FORMAT12H, KConfig::Notify); + config->sync(); + + m_is24HourTime = is24HourTime; + Q_EMIT is24HourTimeChanged(); + } +} + +QString TimeUtil::currentTimeZone() const +{ + return QString{QTimeZone::systemTimeZoneId()}; +} + +void TimeUtil::setCurrentTimeZone(QString timeZone) +{ + QProcess::execute("timedatectl", {"set-timezone", timeZone}); + Q_EMIT currentTimeZoneChanged(); +} + +TimeZoneFilterProxy *TimeUtil::timeZones() const +{ + return m_filterModel; +} diff --git a/initialstart/modules/time/timeutil.h b/initialstart/modules/time/timeutil.h new file mode 100644 index 00000000..dbd06a31 --- /dev/null +++ b/initialstart/modules/time/timeutil.h @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2023 by Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "timezonemodel.h" + +class TimeUtil : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool is24HourTime READ is24HourTime WRITE setIs24HourTime NOTIFY is24HourTimeChanged); + Q_PROPERTY(QString currentTimeZone READ currentTimeZone WRITE setCurrentTimeZone NOTIFY currentTimeZoneChanged); + Q_PROPERTY(TimeZoneFilterProxy *timeZones READ timeZones CONSTANT); + +public: + TimeUtil(QObject *parent = nullptr); + + bool is24HourTime() const; + void setIs24HourTime(bool is24HourTime); + + QString currentTimeZone() const; + void setCurrentTimeZone(QString timeZone); + + TimeZoneFilterProxy *timeZones() const; + +Q_SIGNALS: + void is24HourTimeChanged(); + void currentTimeZoneChanged(); + +private: + bool m_is24HourTime; + + TimeZoneModel *m_timeZoneModel; + TimeZoneFilterProxy *m_filterModel; +}; diff --git a/initialstart/modules/time/timezonemodel.cpp b/initialstart/modules/time/timezonemodel.cpp new file mode 100644 index 00000000..045f75d2 --- /dev/null +++ b/initialstart/modules/time/timezonemodel.cpp @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: 2014 Kai Uwe Broulik +// SPDX-FileCopyrightText: 2014 Martin Klapetek +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "timezonemodel.h" + +#include + +#include +#include +#include + +TimeZoneFilterProxy::TimeZoneFilterProxy(QObject *parent) + : QSortFilterProxyModel(parent) +{ + m_stringMatcher.setCaseSensitivity(Qt::CaseInsensitive); +} + +bool TimeZoneFilterProxy::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const +{ + if (!sourceModel() || m_filterString.isEmpty()) { + return true; + } + const QString id = sourceModel()->index(source_row, 0, source_parent).data(TimeZoneModel::TimeZoneIdRole).toString(); + return m_stringMatcher.indexIn(id) != -1; +} + +void TimeZoneFilterProxy::setFilterString(const QString &filterString) +{ + m_filterString = filterString; + m_stringMatcher.setPattern(filterString); + Q_EMIT filterStringChanged(); + invalidateFilter(); +} + +TimeZoneModel::TimeZoneModel(QObject *parent) + : QAbstractListModel(parent) +{ + update(); +} + +TimeZoneModel::~TimeZoneModel() +{ +} + +int TimeZoneModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return m_data.count(); +} + +QVariant TimeZoneModel::data(const QModelIndex &index, int role) const +{ + if (index.isValid()) { + switch (role) { + case TimeZoneIdRole: + return m_data[index.row()]; + } + } + + return QVariant(); +} + +void TimeZoneModel::update() +{ + beginResetModel(); + m_data.clear(); + + QTimeZone localZone = QTimeZone(QTimeZone::systemTimeZoneId()); + m_data.append(localZone.id()); + + QStringList cities; + QHash zonesByCity; + + const QList systemTimeZones = QTimeZone::availableTimeZoneIds(); + + for (QByteArray arr : systemTimeZones) { + m_data.append(arr); + } + + endResetModel(); +} + +QHash TimeZoneModel::roleNames() const +{ + return {{TimeZoneIdRole, "timeZoneId"}}; +} diff --git a/initialstart/modules/time/timezonemodel.h b/initialstart/modules/time/timezonemodel.h new file mode 100644 index 00000000..1f20f411 --- /dev/null +++ b/initialstart/modules/time/timezonemodel.h @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: 2014 Kai Uwe Broulik +// SPDX-FileCopyrightText: 2014 Martin Klapetek +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +class TimeZoneFilterProxy : public QSortFilterProxyModel +{ + Q_OBJECT + Q_PROPERTY(QString filterString WRITE setFilterString MEMBER m_filterString NOTIFY filterStringChanged) + +public: + explicit TimeZoneFilterProxy(QObject *parent = nullptr); + bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; + + void setFilterString(const QString &filterString); + +Q_SIGNALS: + void filterStringChanged(); + +private: + QString m_filterString; + QStringMatcher m_stringMatcher; +}; + +class TimeZoneModel : public QAbstractListModel +{ + Q_OBJECT + +public: + explicit TimeZoneModel(QObject *parent = nullptr); + ~TimeZoneModel() override; + + enum Roles { TimeZoneIdRole = Qt::UserRole + 1 }; + + int rowCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + + void update(); + +protected: + QHash roleNames() const override; + +private: + QList m_data; +}; diff --git a/initialstart/modules/wifi/CMakeLists.txt b/initialstart/modules/wifi/CMakeLists.txt new file mode 100644 index 00000000..b0ce63a5 --- /dev/null +++ b/initialstart/modules/wifi/CMakeLists.txt @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: 2023 Devin Lin +# SPDX-License-Identifier: GPL-2.0-or-later + +set(wifiplugin_SRCS + wifiplugin.cpp + wifiutil.cpp +) + +add_library(wifiplugin ${wifiplugin_SRCS}) + +target_link_libraries(wifiplugin + Qt::Core + Qt::DBus + Qt::Quick + KF6::CoreAddons + KF6::ConfigCore + KF6::ConfigGui + KF6::NetworkManagerQt + KF6::I18n +) + +set_property(TARGET wifiplugin PROPERTY LIBRARY_OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/org/kde/plasma/mobileinitialstart/wifi) +file(COPY qmldir DESTINATION ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/org/kde/plasma/mobileinitialstart/wifi) + +install(TARGETS wifiplugin DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/mobileinitialstart/wifi) +install(FILES qmldir ${qml_SRC} DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/mobileinitialstart/wifi) + +plasma_install_package(package org.kde.plasma.mobileinitialstart.wifi mobileinitialstart) + + + + + diff --git a/initialstart/modules/wifi/package/contents/ui/ConnectDialog.qml b/initialstart/modules/wifi/package/contents/ui/ConnectDialog.qml new file mode 100644 index 00000000..5947abff --- /dev/null +++ b/initialstart/modules/wifi/package/contents/ui/ConnectDialog.qml @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Controls 2.15 as Controls + +import org.kde.kirigami 2.20 as Kirigami + +Kirigami.PromptDialog { + id: dialogRoot + property int securityType + property string headingText + property string devicePath + property string specificPath + + signal donePressed(string password) + + function openAndClear() { + warning.visible = false; + this.open(); + passwordField.text = ""; + passwordField.focus = true; + } + + title: headingText + standardButtons: Controls.Dialog.Ok | Controls.Dialog.Cancel + + onOpened: passwordField.forceActiveFocus() + onRejected: { + dialogRoot.close(); + passwordField.focus = false; + } + onAccepted: { + if (passwordField.acceptableInput) { + dialogRoot.close(); + handler.addAndActivateConnection(devicePath, specificPath, passwordField.text); + } else { + warning.visible = true; + } + passwordField.focus = false; + } + + ColumnLayout { + id: column + spacing: Kirigami.Units.largeSpacing + + PasswordField { + id: passwordField + Layout.fillWidth: true + securityType: dialogRoot.securityType + onAccepted: dialogRoot.accept() + } + + Controls.Label { + id: warning + text: i18n("Invalid input.") + visible: false + } + } +} diff --git a/initialstart/modules/wifi/package/contents/ui/ConnectionItemDelegate.qml b/initialstart/modules/wifi/package/contents/ui/ConnectionItemDelegate.qml new file mode 100644 index 00000000..6b17d242 --- /dev/null +++ b/initialstart/modules/wifi/package/contents/ui/ConnectionItemDelegate.qml @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: 2017 Martin Kacej +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Controls 2.15 as Controls + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.networkmanagement 0.2 as PlasmaNM +import org.kde.kirigami 2.2 as Kirigami +import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm + +MobileForm.AbstractFormDelegate { + topPadding: Kirigami.Units.smallSpacing + bottomPadding: Kirigami.Units.smallSpacing + + property var map : [] + property bool predictableWirelessPassword: !Uuid && Type == PlasmaNM.Enums.Wireless && + (SecurityType == PlasmaNM.Enums.StaticWep || + SecurityType == PlasmaNM.Enums.WpaPsk || + SecurityType == PlasmaNM.Enums.Wpa2Psk || + SecurityType == PlasmaNM.Enums.SAE) + + onClicked: { + changeState() + } + + function changeState() { + if (Uuid || !predictableWirelessPassword) { + if (ConnectionState == PlasmaNM.Enums.Deactivated) { + if (!predictableWirelessPassword && !Uuid) { + handler.addAndActivateConnection(DevicePath, SpecificPath); + } else { + handler.activateConnection(ConnectionPath, DevicePath, SpecificPath); + } + } else{ + //show popup + } + } else if (predictableWirelessPassword) { + connectionDialog.headingText = i18n("Connect to") + " " + ItemUniqueName; + connectionDialog.devicePath = DevicePath; + connectionDialog.specificPath = SpecificPath; + connectionDialog.securityType = SecurityType; + connectionDialog.openAndClear(); + } + } + + contentItem: RowLayout { + spacing: Kirigami.Units.largeSpacing + + Item { + Layout.preferredWidth: Kirigami.Units.gridUnit + Layout.preferredHeight: Kirigami.Units.gridUnit + + PlasmaCore.SvgItem { + id: connectionSvgIcon + elementId: mobileProxyModel.showSavedMode ? "network-wireless-connected-100" : ConnectionIcon + + svg: PlasmaCore.Svg { + multipleImages: true + imagePath: "icons/network" + colorGroup: PlasmaCore.ColorScope.colorGroup + } + } + + Controls.BusyIndicator { + id: connectingIndicator + + anchors { + horizontalCenter: connectionSvgIcon.horizontalCenter + verticalCenter: connectionSvgIcon.verticalCenter + } + running: ConnectionState == PlasmaNM.Enums.Activating + visible: running + } + } + + Controls.Label { + id: connectionNameLabel + + Layout.fillWidth: true + elide: Text.ElideRight + text: ItemUniqueName + textFormat: Text.PlainText + } + + Controls.ToolButton { + icon.name: "network-connect" + visible: ConnectionState != PlasmaNM.Enums.Activated + onClicked: changeState() + } + + Controls.ToolButton { + icon.name: "network-disconnect" + visible: ConnectionState == PlasmaNM.Enums.Activated + onClicked: handler.deactivateConnection(ConnectionPath, DevicePath) + } + } +} diff --git a/initialstart/modules/wifi/package/contents/ui/PasswordField.qml b/initialstart/modules/wifi/package/contents/ui/PasswordField.qml new file mode 100644 index 00000000..8c6c839d --- /dev/null +++ b/initialstart/modules/wifi/package/contents/ui/PasswordField.qml @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2013-2017 Jan Grulich +// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL + +import QtQuick 2.15 +import org.kde.kirigami 2.10 as Kirigami +import org.kde.plasma.networkmanagement 0.2 as PlasmaNM + +Kirigami.PasswordField { + property int securityType + placeholderText: i18n("Password…") + validator: RegularExpressionValidator { + regularExpression: if (securityType == PlasmaNM.Enums.StaticWep) { + /^(?:.{5}|[0-9a-fA-F]{10}|.{13}|[0-9a-fA-F]{26}){1}$/ + } else { + /^(?:.{8,64}){1}$/ + } + } +} + diff --git a/initialstart/modules/wifi/package/contents/ui/main.qml b/initialstart/modules/wifi/package/contents/ui/main.qml new file mode 100644 index 00000000..7cb25a6b --- /dev/null +++ b/initialstart/modules/wifi/package/contents/ui/main.qml @@ -0,0 +1,118 @@ +// SPDX-FileCopyrightText: 2017 Martin Kacej +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import org.kde.kirigami 2.20 as Kirigami +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm +import org.kde.plasma.networkmanagement 0.2 as PlasmaNM +import org.kde.plasma.mobileinitialstart.wifi 1.0 as WiFi + +Item { + id: root + property string name: i18n("Network") + + readonly property real cardWidth: Math.min(Kirigami.Units.gridUnit * 30, root.width - Kirigami.Units.gridUnit * 2) + + PlasmaNM.Handler { + id: handler + } + + PlasmaNM.EnabledConnections { + id: enabledConnections + } + + PlasmaNM.NetworkModel { + id: connectionModel + } + + PlasmaNM.MobileProxyModel { + id: mobileProxyModel + sourceModel: connectionModel + showSavedMode: false + } + + ConnectDialog { + id: connectionDialog + } + + Component.onCompleted: handler.requestScan() + + Timer { + id: scanTimer + interval: 10200 + repeat: true + running: parent.visible + + onTriggered: handler.requestScan() + } + + ColumnLayout { + anchors.fill: parent + anchors.topMargin: Kirigami.Units.gridUnit + anchors.bottomMargin: Kirigami.Units.largeSpacing + width: root.width + spacing: Kirigami.Units.gridUnit + + Label { + Layout.leftMargin: Kirigami.Units.gridUnit + Layout.rightMargin: Kirigami.Units.gridUnit + Layout.alignment: Qt.AlignTop + Layout.fillWidth: true + + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + text: i18n("Connect to a WiFi network for network access.") + } + + MobileForm.FormCard { + maximumWidth: root.cardWidth + + Layout.fillHeight: true + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + Layout.fillWidth: true + + contentItem: ColumnLayout { + spacing: 0 + + ListView { + id: listView + currentIndex: -1 + clip: true + Layout.fillWidth: true + Layout.fillHeight: true + + section.property: "Section" + section.delegate: Kirigami.ListSectionHeader { + text: section + } + + model: mobileProxyModel + + Kirigami.PlaceholderMessage { + anchors.centerIn: parent + width: parent.width - (Kirigami.Units.largeSpacing * 4) + visible: !enabledConnections.wirelessEnabled + text: i18n("Wi-Fi is disabled") + icon.name: "network-wireless-disconnected" + helpfulAction: Kirigami.Action { + iconName: "network-wireless-connected" + text: i18n("Enable") + onTriggered: handler.enableWireless(true) + } + } + + delegate: ConnectionItemDelegate { + width: listView.width + } + } + } + } + } +} + + diff --git a/initialstart/modules/wifi/package/metadata.json b/initialstart/modules/wifi/package/metadata.json new file mode 100644 index 00000000..d31ca5c0 --- /dev/null +++ b/initialstart/modules/wifi/package/metadata.json @@ -0,0 +1,22 @@ +{ + "KPackageStructure": "KPackage/GenericQML", + "KPlugin": { + "Authors": [ + { + "Email": "devin@kde.org", + "Name": "Devin Lin" + } + ], + "Description": "WiFi initialstart module for Plasma Mobile", + "Id": "org.kde.plasma.mobileinitialstart.wifi", + "License": "GPL-2.0+", + "Name": "WiFi", + "ServiceTypes": [ + "KPackage/GenericQML" + ], + "Version": "0.1", + "Website": "https://kde.org" + } +} + + diff --git a/initialstart/modules/wifi/qmldir b/initialstart/modules/wifi/qmldir new file mode 100644 index 00000000..7f602aa6 --- /dev/null +++ b/initialstart/modules/wifi/qmldir @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2023 Devin Lin +# SPDX-License-Identifier: GPL-2.0-or-later + +module org.kde.plasma.mobileinitialstart.wifi +plugin wifiplugin +classname WiFiPlugin + + diff --git a/initialstart/modules/wifi/wifiplugin.cpp b/initialstart/modules/wifi/wifiplugin.cpp new file mode 100644 index 00000000..ba4bc265 --- /dev/null +++ b/initialstart/modules/wifi/wifiplugin.cpp @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2023 by Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "wifiplugin.h" + +#include +#include + +#include "wifiutil.h" + +void WiFiPlugin::registerTypes(const char *uri) +{ + Q_ASSERT(QLatin1String(uri) == QLatin1String("org.kde.plasma.mobileinitialstart.wifi")); + + qmlRegisterSingletonType(uri, 1, 0, "WiFiUtil", [](QQmlEngine *, QJSEngine *) { + return new WiFiUtil; + }); +} diff --git a/initialstart/modules/wifi/wifiplugin.h b/initialstart/modules/wifi/wifiplugin.h new file mode 100644 index 00000000..891ce14b --- /dev/null +++ b/initialstart/modules/wifi/wifiplugin.h @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2023 by Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include +#include + +class WiFiPlugin : public QQmlExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") + +public: + void registerTypes(const char *uri) override; +}; diff --git a/initialstart/modules/wifi/wifiutil.cpp b/initialstart/modules/wifi/wifiutil.cpp new file mode 100644 index 00000000..a0775607 --- /dev/null +++ b/initialstart/modules/wifi/wifiutil.cpp @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2023 by Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "wifiutil.h" + +#include +#include +#include + +#include +#include + +WiFiUtil::WiFiUtil(QObject *parent) + : QObject{parent} +{ +} diff --git a/initialstart/modules/wifi/wifiutil.h b/initialstart/modules/wifi/wifiutil.h new file mode 100644 index 00000000..97e39b6d --- /dev/null +++ b/initialstart/modules/wifi/wifiutil.h @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2023 by Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +class WiFiUtil : public QObject +{ + Q_OBJECT + +public: + WiFiUtil(QObject *parent = nullptr); +}; diff --git a/initialstart/qml/LandingComponent.qml b/initialstart/qml/LandingComponent.qml new file mode 100644 index 00000000..39955e68 --- /dev/null +++ b/initialstart/qml/LandingComponent.qml @@ -0,0 +1,113 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import org.kde.kirigami 2.20 as Kirigami + +Item { + id: root + + readonly property real scaleStart: 1.4 + readonly property real scaleLanding: 1.2 + readonly property real scaleSteps: 1 + + signal requestNextPage() + + function returnToLanding() { + backgroundImage.scale = scaleLanding; + contentOpacityAnim.to = 1; + contentOpacityAnim.restart(); + } + + property real contentOpacity: 0 + NumberAnimation on contentOpacity { + id: contentOpacityAnim + running: true + duration: 1000 + to: 1 + + // shorten animation after initial run + onFinished: duration = 200 + } + + Image { + id: backgroundImage + anchors.fill: parent + source: "qrc:/assets/background.png" + fillMode: Image.PreserveAspectCrop + + opacity: 0 + + NumberAnimation on opacity { + running: true + duration: 400 + to: 1 + easing.type: Easing.InOutQuad + } + + // zoom animation + scale: scaleStart + Component.onCompleted: scale = scaleLanding + + Behavior on scale { + NumberAnimation { + duration: 2000 + easing.type: Easing.OutExpo + } + } + + // darken image slightly + Rectangle { + anchors.fill: parent + color: Qt.rgba(0, 0, 0, 0.3) + } + } + + ColumnLayout { + opacity: root.contentOpacity + spacing: Kirigami.Units.largeSpacing + + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: Kirigami.Units.gridUnit * 4 + anchors.rightMargin: Kirigami.Units.gridUnit * 4 + + Label { + Layout.fillWidth: true + + text: i18n("Welcome to Plasma") + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.Wrap + + font.pointSize: 18 + color: "white" + } + } + + Button { + id: button + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.margins: Kirigami.Units.gridUnit + + topPadding: Kirigami.Units.largeSpacing + bottomPadding: Kirigami.Units.largeSpacing + leftPadding: Kirigami.Units.gridUnit + rightPadding: Kirigami.Units.gridUnit + + opacity: root.contentOpacity + text: i18n("Next") + icon.name: "go-next-symbolic" + + onClicked: { + backgroundImage.scale = scaleSteps; + contentOpacityAnim.to = 0; + contentOpacityAnim.restart(); + root.requestNextPage() + } + } +} + diff --git a/initialstart/qml/Wizard.qml b/initialstart/qml/Wizard.qml new file mode 100644 index 00000000..84a6329c --- /dev/null +++ b/initialstart/qml/Wizard.qml @@ -0,0 +1,340 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.kirigami 2.20 as Kirigami +import org.kde.plasma.private.mobileshell 1.0 as MobileShell + +import initialstart 1.0 as InitialStart + +Kirigami.Page { + id: root + + leftPadding: 0 + rightPadding: 0 + topPadding: 0 + bottomPadding: 0 + + property int currentIndex: 0 + property int stepCount: 0 + property bool showingLanding: true + + // filled by items + property var currentStepItem + property var nextStepItem + property var previousStepItem + + readonly property bool onFinalPage: currentIndex === (stepCount - 1) + + // step animation + // manually doing the animation is more performant and less glitchy with window resize than a SwipeView + property real previousStepItemX: 0 + property real currentStepItemX: 0 + property real nextStepItemX: 0 + + NumberAnimation on previousStepItemX { + id: previousStepAnim + duration: 400 + easing.type: Easing.OutExpo + onFinished: { + if (root.previousStepItemX != 0) { + root.previousStepItem.visible = false; + } + } + } + + NumberAnimation on currentStepItemX { + id: currentStepAnim + duration: 400 + easing.type: Easing.OutExpo + } + + NumberAnimation on nextStepItemX { + id: nextStepAnim + duration: 400 + easing.type: Easing.OutExpo + onFinished: { + if (root.nextStepItemX != 0) { + root.nextStepItem.visible = false; + } + } + } + + function finishFinalPage() { + InitialStart.Wizard.wizardFinished(); + applicationWindow().close(); + } + + function requestNextPage() { + if (previousStepAnim.running || currentStepAnim.running || nextStepAnim.running) { + return; + } + + previousStepItemX = 0; + + currentIndex++; + stepHeading.changeText(currentStepItem.name); + + currentStepItemX = root.width; + currentStepItem.visible = true; + + previousStepAnim.to = -root.width; + previousStepAnim.restart(); + currentStepAnim.to = 0; + currentStepAnim.restart(); + } + + function requestPreviousPage() { + if (previousStepAnim.running || currentStepAnim.running || nextStepAnim.running) { + return; + } + + if (currentIndex === 0) { + root.showingLanding = true; + landingComponent.returnToLanding(); + } else { + nextStepItemX = 0; + + currentIndex--; + stepHeading.changeText(currentStepItem.name); + + currentStepItemX = -root.width; + currentStepItem.visible = true; + + nextStepAnim.to = root.width; + nextStepAnim.restart(); + currentStepAnim.to = 0; + currentStepAnim.restart(); + } + } + + // top status bar + MobileShell.StatusBar { + id: statusBar + z: 1 + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + height: Kirigami.Units.gridUnit * 1.25 + + colorGroup: PlasmaCore.Theme.ComplementaryColorGroup + backgroundColor: "transparent" + + showSecondRow: false + showDropShadow: true + showTime: true + disableSystemTray: true // prevent SIGABRT, since loading the system tray leads to bad... things + } + + LandingComponent { + id: landingComponent + anchors.fill: parent + + onRequestNextPage: { + root.showingLanding = false; + stepHeading.changeText(root.currentStepItem.name); + } + } + + Item { + id: stepsComponent + anchors.fill: parent + + // animation when we switch to step stage + opacity: root.showingLanding ? 0 : 1 + property real translateY: root.showingLanding ? overlaySteps.height : 0 + + Behavior on opacity { + NumberAnimation { + duration: 1000 + easing.type: Easing.OutExpo + } + } + + Behavior on translateY { + NumberAnimation { + duration: 1000 + easing.type: Easing.OutExpo + } + } + + transform: Translate { y: stepsComponent.translateY } + + // heading for all the wizard steps + Label { + id: stepHeading + opacity: 0 + color: "white" + horizontalAlignment: Text.AlignHCenter + font.pointSize: 18 + + anchors.left: parent.left + anchors.leftMargin: Kirigami.Units.gridUnit + anchors.right: parent.right + anchors.rightMargin: Kirigami.Units.gridUnit + anchors.bottom: parent.bottom + anchors.bottomMargin: root.height * 0.7 + Kirigami.Units.gridUnit + + property string toText + + function changeText(text) { + toText = text; + toHidden.restart(); + } + + NumberAnimation on opacity { + id: toHidden + duration: 200 + to: 0 + onFinished: { + stepHeading.text = stepHeading.toText; + toShown.restart(); + } + } + + NumberAnimation on opacity { + id: toShown + duration: 200 + to: 1 + } + } + + Rectangle { + id: overlaySteps + + Kirigami.Theme.inherit: false + Kirigami.Theme.colorSet: Kirigami.Theme.Window + + color: Kirigami.Theme.backgroundColor + + anchors.fill: parent + anchors.topMargin: root.height * 0.3 + + // all steps are in this container + Item { + anchors.fill: parent + anchors.bottomMargin: stepFooter.implicitHeight + + // setup steps + Repeater { + model: InitialStart.Wizard.steps + + delegate: MobileShell.BaseItem { + id: item + visible: model.index === 0 // the binding is broken later + contentItem: modelData + transform: Translate { + x: { + if (item.currentIndex === root.currentIndex - 1) { + return previousStepItemX; + } else if (item.currentIndex === root.currentIndex + 1) { + return nextStepItemX; + } else if (item.currentIndex === root.currentIndex) { + return currentStepItemX; + } + return 0; + } + } + + anchors.fill: parent + + // pass up the property + property string name: contentItem.name + property int currentIndex: model.index + + function updateRootItems() { + if (model.index === root.currentIndex) { + root.currentStepItem = item; + } else if (model.index === root.currentIndex - 1) { + root.previousStepItem = item; + } else if (model.index === root.currentIndex + 1) { + root.nextStepItem = item; + } + } + + Component.onCompleted: { + root.stepCount++ + updateRootItems(); + } + + // keep root properties updated + Connections { + target: root + + function onCurrentIndexChanged() { + item.updateRootItems(); + } + } + } + } + } + + // bottom footer + RowLayout { + id: stepFooter + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + + Button { + Layout.alignment: Qt.AlignLeft + Layout.leftMargin: Kirigami.Units.gridUnit + Layout.bottomMargin: Kirigami.Units.gridUnit + + topPadding: Kirigami.Units.largeSpacing + bottomPadding: Kirigami.Units.largeSpacing + leftPadding: Kirigami.Units.gridUnit + rightPadding: Kirigami.Units.gridUnit + + text: i18n("Back") + icon.name: "arrow-left" + + onClicked: root.requestPreviousPage() + } + + Item {} + + Button { + Layout.alignment: Qt.AlignRight + Layout.rightMargin: Kirigami.Units.gridUnit + Layout.bottomMargin: Kirigami.Units.gridUnit + + topPadding: Kirigami.Units.largeSpacing + bottomPadding: Kirigami.Units.largeSpacing + leftPadding: Kirigami.Units.gridUnit + rightPadding: Kirigami.Units.gridUnit + + visible: !root.onFinalPage + text: i18n("Next") + icon.name: "arrow-right" + + onClicked: root.requestNextPage(); + } + + Button { + Layout.alignment: Qt.AlignRight + Layout.rightMargin: Kirigami.Units.gridUnit + Layout.bottomMargin: Kirigami.Units.gridUnit + + topPadding: Kirigami.Units.largeSpacing + bottomPadding: Kirigami.Units.largeSpacing + leftPadding: Kirigami.Units.gridUnit + rightPadding: Kirigami.Units.gridUnit + + visible: root.onFinalPage + text: i18n("Finish") + icon.name: "dialog-ok" + + onClicked: root.finishFinalPage(); + } + } + } + } +} + diff --git a/initialstart/qml/main.qml b/initialstart/qml/main.qml new file mode 100644 index 00000000..216b6776 --- /dev/null +++ b/initialstart/qml/main.qml @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import org.kde.kirigami 2.20 as Kirigami + +import initialstart 1.0 as InitialStart + +Kirigami.ApplicationWindow { + id: root + + width: 360 + height: 720 + visibility: InitialStart.Wizard.testingMode ? "Windowed" : "FullScreen" + + title: i18n("Initial Start") + + pageStack.globalToolBar.style: Kirigami.ApplicationHeaderStyle.None + pageStack.columnView.columnResizeMode: Kirigami.ColumnView.SingleColumn + + pageStack.initialPage: Wizard {} +} diff --git a/initialstart/resources.qrc b/initialstart/resources.qrc new file mode 100644 index 00000000..6791f239 --- /dev/null +++ b/initialstart/resources.qrc @@ -0,0 +1,15 @@ + + + + + qml/main.qml + qml/Wizard.qml + qml/LandingComponent.qml + + assets/background.png + + + diff --git a/initialstart/settings.cpp b/initialstart/settings.cpp new file mode 100644 index 00000000..138f840a --- /dev/null +++ b/initialstart/settings.cpp @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "settings.h" + +#include +#include + +const QString CONFIG_FILE = QStringLiteral("plasmamobilerc"); +const QString INITIAL_START_CONFIG_GROUP = QStringLiteral("InitialStart"); + +Settings::Settings(QObject *parent) + : QObject{parent} + , m_mobileConfig{KSharedConfig::openConfig(CONFIG_FILE, KConfig::SimpleConfig)} + , m_isMobilePlatform{KRuntimePlatform::runtimePlatform().contains(QStringLiteral("phone"))} +{ +} + +bool Settings::shouldStartWizard() +{ + if (!m_isMobilePlatform) { + return false; + } + + auto group = KConfigGroup{m_mobileConfig, INITIAL_START_CONFIG_GROUP}; + return !group.readEntry("wizardRun", false); +} + +void Settings::setWizardFinished() +{ + auto group = KConfigGroup{m_mobileConfig, INITIAL_START_CONFIG_GROUP}; + group.writeEntry("wizardRun", true, KConfigGroup::Notify); + m_mobileConfig->sync(); +} + +Settings *Settings::self() +{ + static auto instance = new Settings; + return instance; +} diff --git a/initialstart/settings.h b/initialstart/settings.h new file mode 100644 index 00000000..f415f124 --- /dev/null +++ b/initialstart/settings.h @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include + +class Settings : public QObject +{ + Q_OBJECT + +public: + Settings(QObject *parent = nullptr); + static Settings *self(); + + bool shouldStartWizard(); + void setWizardFinished(); + +private: + KSharedConfig::Ptr m_mobileConfig; + bool m_isMobilePlatform; +}; diff --git a/initialstart/utils.h b/initialstart/utils.h new file mode 100644 index 00000000..78050620 --- /dev/null +++ b/initialstart/utils.h @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +static const QLoggingCategory &LOGGING_CATEGORY() +{ + static const QLoggingCategory category("plasma-mobile-initial-start"); + return category; +} diff --git a/initialstart/wizard.cpp b/initialstart/wizard.cpp new file mode 100644 index 00000000..75a81d31 --- /dev/null +++ b/initialstart/wizard.cpp @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "wizard.h" +#include "settings.h" +#include "utils.h" + +#include + +#include +#include + +// TODO read distro provided config file +const QList WIZARD_MODULE_ORDER = {QStringLiteral("org.kde.plasma.mobileinitialstart.prepare"), + QStringLiteral("org.kde.plasma.mobileinitialstart.time"), + QStringLiteral("org.kde.plasma.mobileinitialstart.wifi"), + QStringLiteral("org.kde.plasma.mobileinitialstart.cellular"), + QStringLiteral("org.kde.plasma.mobileinitialstart.finished")}; + +Wizard::Wizard(QObject *parent, QQmlEngine *engine) + : QObject{parent} + , m_engine{engine} +{ +} + +void Wizard::load() +{ + if (!m_engine) { + return; + } + + qCDebug(LOGGING_CATEGORY) << "Loading initialstart packages..."; + + // load initialstart packages + const auto packages = KPackage::PackageLoader::self()->listPackages(QStringLiteral("KPackage/GenericQML"), QStringLiteral("plasma/mobileinitialstart")); + for (auto &metaData : packages) { + KPackage::Package package = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("KPackage/GenericQML"), QFileInfo(metaData.fileName()).path()); + if (!package.isValid()) { + qCWarning(LOGGING_CATEGORY) << "initialstart package invalid:" << metaData.fileName(); + continue; + } + m_modulePackages.push_back({new KPluginMetaData{metaData}, package}); + } + + // sort modules by order + std::sort(m_modulePackages.begin(), m_modulePackages.end(), [](const auto &lhs, const auto &rhs) { + return WIZARD_MODULE_ORDER.indexOf(lhs.first->pluginId()) < WIZARD_MODULE_ORDER.indexOf(rhs.first->pluginId()); + }); + + QQmlComponent *c = new QQmlComponent(m_engine, this); + + // load initialstart QML items + for (auto &pair : m_modulePackages) { + // load QML from kpackage + c->loadUrl(pair.second.fileUrl("mainscript"), QQmlComponent::PreferSynchronous); + + auto created = c->create(m_engine->rootContext()); + auto createdItem = qobject_cast(created); + + // print errors if there were issues loading + if (!createdItem) { + qCWarning(LOGGING_CATEGORY) << "Unable to load initialstart module:" << created; + for (auto error : c->errors()) { + qCWarning(LOGGING_CATEGORY) << error; + } + delete created; + continue; + } + + m_moduleItems.push_back(createdItem); + + qCDebug(LOGGING_CATEGORY) << "Loaded initialstart module" << pair.first->pluginId(); + } + + delete c; +} + +void Wizard::setTestingMode(bool testingMode) +{ + if (testingMode != m_testingMode) { + m_testingMode = testingMode; + Q_EMIT testingModeChanged(); + } +} + +bool Wizard::testingMode() +{ + return m_testingMode; +} + +QList Wizard::steps() +{ + return m_moduleItems; +} + +void Wizard::wizardFinished() +{ + Settings::self()->setWizardFinished(); +} diff --git a/initialstart/wizard.h b/initialstart/wizard.h new file mode 100644 index 00000000..ef940015 --- /dev/null +++ b/initialstart/wizard.h @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +#include +#include + +class Wizard : public QObject +{ + Q_OBJECT + Q_PROPERTY(QList steps READ steps CONSTANT) + Q_PROPERTY(bool testingMode READ testingMode NOTIFY testingModeChanged) + +public: + Wizard(QObject *parent = nullptr, QQmlEngine *engine = nullptr); + + void load(); + + void setTestingMode(bool testingMode); + bool testingMode(); + + QList steps(); + +public Q_SLOTS: + void wizardFinished(); + +Q_SIGNALS: + void testingModeChanged(); + +private: + QList> m_modulePackages; + QList m_moduleItems; + + bool m_testingMode; + QQmlEngine *m_engine; +}; diff --git a/kded/start.cpp b/kded/start.cpp index 25484d16..1b8a4580 100644 --- a/kded/start.cpp +++ b/kded/start.cpp @@ -12,10 +12,15 @@ Start::Start(QObject *parent, const QList &) : KDEDModule{parent} { - auto *job = new KIO::CommandLauncherJob(QStringLiteral("plasma-mobile-envmanager --apply-settings"), {}); - job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoErrorHandlingEnabled)); - job->setDesktopName(QStringLiteral("org.kde.plasma-mobile-envmanager")); - job->start(); + auto *envmanagerJob = new KIO::CommandLauncherJob(QStringLiteral("plasma-mobile-envmanager --apply-settings"), {}); + envmanagerJob->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoErrorHandlingEnabled)); + envmanagerJob->setDesktopName(QStringLiteral("org.kde.plasma-mobile-envmanager")); + envmanagerJob->start(); + + auto *initialstartJob = new KIO::CommandLauncherJob(QStringLiteral("plasma-mobile-initial-start"), {}); + initialstartJob->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoErrorHandlingEnabled)); + initialstartJob->setDesktopName(QStringLiteral("org.kde.plasma-mobile-initial-start")); + initialstartJob->start(); } #include "start.moc"