initialstart: Add first login experience

Adds an experience for users on first login, allowing some basic configuration.

This is separate from a first start wizard, which would run as a separate user with elevated permissions, and include options that an installer would have.
This commit is contained in:
Devin Lin 2023-04-01 07:09:57 +00:00
parent 0d7fde0ea1
commit 5c0fd57600
63 changed files with 2845 additions and 5 deletions

View file

@ -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

View file

@ -5,6 +5,7 @@ add_library(ppc-mmqmlplugin)
target_sources(ppc-mmqmlplugin PRIVATE
mmqmlplugin.cpp
signalindicator.cpp
profilesettings.cpp
)
target_link_libraries(ppc-mmqmlplugin

View file

@ -0,0 +1,139 @@
// SPDX-FileCopyrightText: 2022 Devin Lin <espidev@gmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "profilesettings.h"
#include <KLocalizedString>
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<NetworkManager::GsmSetting>();
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;
}

View file

@ -0,0 +1,75 @@
// SPDX-FileCopyrightText: 2021-2023 Devin Lin <espidev@gmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QList>
#include <QString>
#include <NetworkManagerQt/CdmaSetting>
#include <NetworkManagerQt/ConnectionSettings>
#include <NetworkManagerQt/GsmSetting>
#include <NetworkManagerQt/Manager>
#include <NetworkManagerQt/ModemDevice>
#include <NetworkManagerQt/Settings>
#include <ModemManagerQt/GenericTypes>
#include <ModemManagerQt/Manager>
#include <ModemManagerQt/Modem3Gpp>
#include <ModemManagerQt/ModemDevice>
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;
};

View file

@ -11,6 +11,8 @@
#include <KUser>
#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<ProfileSettings *> &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<NetworkManager::GsmSetting>()) {
m_profileList.append(new ProfileSettings(this, setting.dynamicCast<NetworkManager::GsmSetting>(), 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<QDBusObjectPath> 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<NetworkManager::GsmSetting>();
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<QDBusObjectPath> 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<NetworkManager::GsmSetting>();
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();
}
}

View file

@ -12,6 +12,8 @@
#include <QObject>
#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<ProfileSettings *> 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<ProfileSettings *> &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<ProfileSettings *> m_profileList;
void updateModemManagerModem();
void updateNetworkManagerModem();
};

View file

@ -14,7 +14,6 @@
#include <QProcess>
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");

View file

@ -0,0 +1,30 @@
# SPDX-FileCopyrightText: 2022 Devin Lin <devin@kde.org>
# 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})

6
initialstart/Messages.sh Normal file
View file

@ -0,0 +1,6 @@
#! /usr/bin/env bash
# SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
# 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

View file

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: None
SPDX-License-Identifier: CC0-1.0

72
initialstart/main.cpp Normal file
View file

@ -0,0 +1,72 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include <QApplication>
#include <QCommandLineParser>
#include <QIcon>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QString>
#include <KAboutData>
#include <KLocalizedString>
#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<QCommandLineParser> 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<Wizard>("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();
}

View file

@ -0,0 +1,8 @@
# SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
# 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)

View file

@ -0,0 +1,33 @@
# SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
# 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)

View file

@ -0,0 +1,12 @@
// SPDX-FileCopyrightText: 2023 by Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "cellularplugin.h"
#include <QQmlContext>
#include <QQuickItem>
void CellularPlugin::registerTypes(const char *uri)
{
Q_ASSERT(QLatin1String(uri) == QLatin1String("org.kde.plasma.mobileinitialstart.cellular"));
}

View file

@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: 2023 by Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QUrl>
#include <QQmlEngine>
#include <QQmlExtensionPlugin>
class CellularPlugin : public QQmlExtensionPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface")
public:
void registerTypes(const char *uri) override;
};

View file

@ -0,0 +1,66 @@
// SPDX-FileCopyrightText: 2020-2023 Devin Lin <espidev@gmail.com>
// 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);
}
}
}
}
}

View file

@ -0,0 +1,152 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// 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();
}
}
}
}
}
}

View file

@ -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"
}
}

View file

@ -0,0 +1,7 @@
# SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
# SPDX-License-Identifier: GPL-2.0-or-later
module org.kde.plasma.mobileinitialstart.cellular
plugin cellularplugin
classname CellularPlugin

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View file

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: None
SPDX-License-Identifier: CC0-1.0

View file

@ -0,0 +1,36 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// 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. <br /><br />Enjoy <b>Plasma 6.0</b>!")
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"
}
}
}

View file

@ -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"
}
}

View file

@ -0,0 +1,31 @@
# SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
# 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)

View file

@ -0,0 +1,163 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// 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));
}
}
}
}
}
}

View file

@ -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"
}
}

View file

@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: 2023 by Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "prepareplugin.h"
#include <QQmlContext>
#include <QQuickItem>
#include "prepareutil.h"
void PreparePlugin::registerTypes(const char *uri)
{
Q_ASSERT(QLatin1String(uri) == QLatin1String("org.kde.plasma.mobileinitialstart.prepare"));
qmlRegisterSingletonType<PrepareUtil>(uri, 1, 0, "PrepareUtil", [](QQmlEngine *, QJSEngine *) {
return new PrepareUtil;
});
}

View file

@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: 2023 by Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QUrl>
#include <QQmlEngine>
#include <QQmlExtensionPlugin>
class PreparePlugin : public QQmlExtensionPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface")
public:
void registerTypes(const char *uri) override;
};

View file

@ -0,0 +1,69 @@
// SPDX-FileCopyrightText: 2023 by Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "prepareutil.h"
#include <QDebug>
#include <QRegularExpression>
PrepareUtil::PrepareUtil(QObject *parent)
: QObject{parent}
, m_process{new QProcess{this}}
{
connect(m_process, QOverload<int, QProcess::ExitStatus>::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<int, QProcess::ExitStatus>::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;
}
}
}

View file

@ -0,0 +1,34 @@
// SPDX-FileCopyrightText: 2023 by Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QObject>
#include <QProcess>
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;
};

View file

@ -0,0 +1,6 @@
# SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
# SPDX-License-Identifier: GPL-2.0-or-later
module org.kde.plasma.mobileinitialstart.prepare
plugin prepareplugin
classname PreparePlugin

View file

@ -0,0 +1,33 @@
# SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
# 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)

View file

@ -0,0 +1,121 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// 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);
}
}
}
}
}
}
}
}

View file

@ -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"
}
}

View file

@ -0,0 +1,7 @@
# SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
# SPDX-License-Identifier: GPL-2.0-or-later
module org.kde.plasma.mobileinitialstart.time
plugin timeplugin
classname TimePlugin

View file

@ -0,0 +1,20 @@
// SPDX-FileCopyrightText: 2023 by Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "timeplugin.h"
#include <QQmlContext>
#include <QQuickItem>
#include "timeutil.h"
void TimePlugin::registerTypes(const char *uri)
{
Q_ASSERT(QLatin1String(uri) == QLatin1String("org.kde.plasma.mobileinitialstart.time"));
qmlRegisterSingletonType<TimeUtil>(uri, 1, 0, "TimeUtil", [](QQmlEngine *, QJSEngine *) {
return new TimeUtil;
});
}
// #include "moc_flashlightplugin.cpp"

View file

@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: 2023 by Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QUrl>
#include <QQmlEngine>
#include <QQmlExtensionPlugin>
class TimePlugin : public QQmlExtensionPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface")
public:
void registerTypes(const char *uri) override;
};

View file

@ -0,0 +1,56 @@
// SPDX-FileCopyrightText: 2023 by Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "timeutil.h"
#include <QDebug>
#include <QRegularExpression>
#include <QTimeZone>
#include <KConfigGroup>
#include <KSharedConfig>
#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;
}

View file

@ -0,0 +1,38 @@
// SPDX-FileCopyrightText: 2023 by Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QObject>
#include <QProcess>
#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;
};

View file

@ -0,0 +1,88 @@
// SPDX-FileCopyrightText: 2014 Kai Uwe Broulik <kde@privat.broulik.de>
// SPDX-FileCopyrightText: 2014 Martin Klapetek <mklapetek@kde.org>
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "timezonemodel.h"
#include <KLocalizedString>
#include <QDebug>
#include <QStringMatcher>
#include <QTimeZone>
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<QString, QTimeZone> zonesByCity;
const QList<QByteArray> systemTimeZones = QTimeZone::availableTimeZoneIds();
for (QByteArray arr : systemTimeZones) {
m_data.append(arr);
}
endResetModel();
}
QHash<int, QByteArray> TimeZoneModel::roleNames() const
{
return {{TimeZoneIdRole, "timeZoneId"}};
}

View file

@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: 2014 Kai Uwe Broulik <kde@privat.broulik.de>
// SPDX-FileCopyrightText: 2014 Martin Klapetek <mklapetek@kde.org>
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QAbstractListModel>
#include <QSortFilterProxyModel>
#include <QTimeZone>
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<int, QByteArray> roleNames() const override;
private:
QList<QString> m_data;
};

View file

@ -0,0 +1,33 @@
# SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
# 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)

View file

@ -0,0 +1,61 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// 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
}
}
}

View file

@ -0,0 +1,100 @@
// SPDX-FileCopyrightText: 2017 Martin Kacej <m.kacej@atlas.sk>
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// 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)
}
}
}

View file

@ -0,0 +1,19 @@
// SPDX-FileCopyrightText: 2013-2017 Jan Grulich <jgrulich@redhat.com>
// 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}$/
}
}
}

View file

@ -0,0 +1,118 @@
// SPDX-FileCopyrightText: 2017 Martin Kacej <m.kacej@atlas.sk>
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// 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
}
}
}
}
}
}

View file

@ -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"
}
}

View file

@ -0,0 +1,8 @@
# SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
# SPDX-License-Identifier: GPL-2.0-or-later
module org.kde.plasma.mobileinitialstart.wifi
plugin wifiplugin
classname WiFiPlugin

View file

@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: 2023 by Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "wifiplugin.h"
#include <QQmlContext>
#include <QQuickItem>
#include "wifiutil.h"
void WiFiPlugin::registerTypes(const char *uri)
{
Q_ASSERT(QLatin1String(uri) == QLatin1String("org.kde.plasma.mobileinitialstart.wifi"));
qmlRegisterSingletonType<WiFiUtil>(uri, 1, 0, "WiFiUtil", [](QQmlEngine *, QJSEngine *) {
return new WiFiUtil;
});
}

View file

@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: 2023 by Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QUrl>
#include <QQmlEngine>
#include <QQmlExtensionPlugin>
class WiFiPlugin : public QQmlExtensionPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface")
public:
void registerTypes(const char *uri) override;
};

View file

@ -0,0 +1,16 @@
// SPDX-FileCopyrightText: 2023 by Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "wifiutil.h"
#include <QDebug>
#include <QRegularExpression>
#include <QTimeZone>
#include <KConfigGroup>
#include <KSharedConfig>
WiFiUtil::WiFiUtil(QObject *parent)
: QObject{parent}
{
}

View file

@ -0,0 +1,15 @@
// SPDX-FileCopyrightText: 2023 by Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QObject>
#include <QProcess>
class WiFiUtil : public QObject
{
Q_OBJECT
public:
WiFiUtil(QObject *parent = nullptr);
};

View file

@ -0,0 +1,113 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// 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 <b>Plasma</b>")
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()
}
}
}

340
initialstart/qml/Wizard.qml Normal file
View file

@ -0,0 +1,340 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// 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();
}
}
}
}
}

25
initialstart/qml/main.qml Normal file
View file

@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// 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 {}
}

View file

@ -0,0 +1,15 @@
<!--
- Copyright 2023 Devin Lin <devin@kde.org>
- SPDX-License-Identifier: GPL-2.0-or-later
-->
<RCC>
<qresource prefix="/">
<file>qml/main.qml</file>
<file>qml/Wizard.qml</file>
<file>qml/LandingComponent.qml</file>
<file>assets/background.png</file>
</qresource>
</RCC>

40
initialstart/settings.cpp Normal file
View file

@ -0,0 +1,40 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "settings.h"
#include <KConfigGroup>
#include <KRuntimePlatform>
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;
}

22
initialstart/settings.h Normal file
View file

@ -0,0 +1,22 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include <KConfig>
#include <KSharedConfig>
#include <QObject>
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;
};

12
initialstart/utils.h Normal file
View file

@ -0,0 +1,12 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QLoggingCategory>
static const QLoggingCategory &LOGGING_CATEGORY()
{
static const QLoggingCategory category("plasma-mobile-initial-start");
return category;
}

99
initialstart/wizard.cpp Normal file
View file

@ -0,0 +1,99 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "wizard.h"
#include "settings.h"
#include "utils.h"
#include <KPackage/PackageLoader>
#include <QFileInfo>
#include <QQmlComponent>
// TODO read distro provided config file
const QList<QString> 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<QQuickItem *>(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<QQuickItem *> Wizard::steps()
{
return m_moduleItems;
}
void Wizard::wizardFinished()
{
Settings::self()->setWizardFinished();
}

41
initialstart/wizard.h Normal file
View file

@ -0,0 +1,41 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QObject>
#include <QQmlEngine>
#include <QQuickItem>
#include <KPackage/PackageLoader>
#include <KPluginMetaData>
class Wizard : public QObject
{
Q_OBJECT
Q_PROPERTY(QList<QQuickItem *> 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<QQuickItem *> steps();
public Q_SLOTS:
void wizardFinished();
Q_SIGNALS:
void testingModeChanged();
private:
QList<std::pair<KPluginMetaData *, KPackage::Package>> m_modulePackages;
QList<QQuickItem *> m_moduleItems;
bool m_testingMode;
QQmlEngine *m_engine;
};

View file

@ -12,10 +12,15 @@
Start::Start(QObject *parent, const QList<QVariant> &)
: 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"