1
0
Fork 0
mirror of https://git.jami.net/savoirfairelinux/jami-client-qt.git synced 2025-07-01 22:25:26 +02:00

account: implement export-from-device using new API

- Implements new APIs
- Implements export-from-device mechanism
(link device in account settings)

Gitlab: #1695
Change-Id: I3d3486380e695ea44c199dbe0a0448f724b4d2db
This commit is contained in:
Kateryna Kostiuk 2025-03-07 10:26:10 -05:00
parent 33da15daba
commit d3c76eac8d
15 changed files with 857 additions and 314 deletions

4
.gitmodules vendored
View file

@ -27,3 +27,7 @@
path = 3rdparty/tidy-html5 path = 3rdparty/tidy-html5
url = https://github.com/htacg/tidy-html5.git url = https://github.com/htacg/tidy-html5.git
ignore = dirty ignore = dirty
[submodule "3rdparty/zxing-cpp"]
path = 3rdparty/zxing-cpp
url = https://github.com/nu-book/zxing-cpp.git
ignore = dirty

1
3rdparty/zxing-cpp vendored Submodule

@ -0,0 +1 @@
Subproject commit a920817b6fe0508cc4aca9003003c2812a78e935

View file

@ -364,6 +364,8 @@ set(COMMON_SOURCES
${APP_SRC_DIR}/pluginversionmanager.cpp ${APP_SRC_DIR}/pluginversionmanager.cpp
${APP_SRC_DIR}/connectioninfolistmodel.cpp ${APP_SRC_DIR}/connectioninfolistmodel.cpp
${APP_SRC_DIR}/pluginversionmanager.cpp ${APP_SRC_DIR}/pluginversionmanager.cpp
${APP_SRC_DIR}/linkdevicemodel.cpp
${APP_SRC_DIR}/qrcodescannermodel.cpp
) )
set(COMMON_HEADERS set(COMMON_HEADERS
@ -436,6 +438,8 @@ set(COMMON_HEADERS
${APP_SRC_DIR}/pttlistener.h ${APP_SRC_DIR}/pttlistener.h
${APP_SRC_DIR}/crashreportclient.h ${APP_SRC_DIR}/crashreportclient.h
${APP_SRC_DIR}/crashreporter.h ${APP_SRC_DIR}/crashreporter.h
${APP_SRC_DIR}/linkdevicemodel.h
${APP_SRC_DIR}/qrcodescannermodel.h
) )
# For libavutil/avframe. # For libavutil/avframe.
@ -678,6 +682,15 @@ list(APPEND CLIENT_LINK_DIRS ${tidy_BINARY_DIR}/Release)
list(APPEND CLIENT_INCLUDE_DIRS ${tidy_SOURCE_DIR}/include) list(APPEND CLIENT_INCLUDE_DIRS ${tidy_SOURCE_DIR}/include)
list(APPEND CLIENT_LIBS tidy-static) list(APPEND CLIENT_LIBS tidy-static)
# ZXing-cpp configuration
set(BUILD_EXAMPLES OFF CACHE BOOL "")
set(BUILD_BLACKBOX_TESTS OFF CACHE BOOL "")
add_subdirectory(3rdparty/zxing-cpp)
# Add ZXing-cpp to includes and libraries
list(APPEND CLIENT_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/zxing-cpp/core/src)
list(APPEND CLIENT_LIBS ZXing)
# common executable sources # common executable sources
qt_add_executable( qt_add_executable(
${PROJECT_NAME} ${PROJECT_NAME}

140
src/app/linkdevicemodel.cpp Normal file
View file

@ -0,0 +1,140 @@
/*
* Copyright (C) 2025-2025 Savoir-faire Linux Inc.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "linkdevicemodel.h"
#include "lrcinstance.h"
#include "api/accountmodel.h"
#include "api/account.h"
using namespace lrc::api::account;
LinkDeviceModel::LinkDeviceModel(LRCInstance* lrcInstance, QObject* parent)
: QObject(parent)
, lrcInstance_(lrcInstance)
{
set_deviceAuthState(static_cast<int>(DeviceAuthState::INIT));
connect(&lrcInstance_->accountModel(),
&lrc::api::AccountModel::addDeviceStateChanged,
this,
[this](const QString& accountId,
uint32_t operationId,
int state,
const MapStringString& details) {
if (operationId != operationId_)
return;
auto deviceState = static_cast<DeviceAuthState>(state);
switch (deviceState) {
case DeviceAuthState::CONNECTING:
handleConnectingSignal();
break;
case DeviceAuthState::AUTHENTICATING:
handleAuthenticatingSignal(Utils::mapStringStringToVariantMap(details));
break;
case DeviceAuthState::IN_PROGRESS:
handleInProgressSignal();
break;
case DeviceAuthState::DONE:
handleDoneSignal(Utils::mapStringStringToVariantMap(details));
break;
default:
break;
}
});
}
void
LinkDeviceModel::addDevice(const QString& token)
{
set_tokenErrorMessage("");
auto errorMessage = QObject::tr(
"New device identifier is not recognized.\nPlease follow above instruction.");
if (!token.startsWith("jami-auth://") || (token.length() != 59)) {
set_tokenErrorMessage(errorMessage);
return;
}
int32_t result = lrcInstance_->accountModel().addDevice(lrcInstance_->getCurrentAccountInfo().id,
token);
if (result > 0) {
operationId_ = result;
} else {
set_tokenErrorMessage(errorMessage);
}
}
void
LinkDeviceModel::handleConnectingSignal()
{
set_deviceAuthState(static_cast<int>(DeviceAuthState::CONNECTING));
}
void
LinkDeviceModel::handleAuthenticatingSignal(const QVariantMap& details)
{
QString peerAddress = details.value("peer_address").toString();
set_ipAddress(peerAddress);
set_deviceAuthState(static_cast<int>(DeviceAuthState::AUTHENTICATING));
}
void
LinkDeviceModel::handleInProgressSignal()
{
set_deviceAuthState(static_cast<int>(DeviceAuthState::IN_PROGRESS));
}
void
LinkDeviceModel::handleDoneSignal(const QVariantMap& details)
{
QString errorString = details.value("error").toString();
if (!errorString.isEmpty() && errorString != "none") {
auto error = mapLinkDeviceError(errorString.toStdString());
set_linkDeviceError(getLinkDeviceString(error));
set_deviceAuthState(static_cast<int>(DeviceAuthState::DONE));
} else {
set_deviceAuthState(static_cast<int>(DeviceAuthState::DONE));
}
}
void
LinkDeviceModel::confirmAddDevice()
{
handleInProgressSignal();
lrcInstance_->accountModel().confirmAddDevice(lrcInstance_->getCurrentAccountInfo().id,
operationId_);
}
void
LinkDeviceModel::cancelAddDevice()
{
handleInProgressSignal();
lrcInstance_->accountModel().cancelAddDevice(lrcInstance_->getCurrentAccountInfo().id,
operationId_);
}
void
LinkDeviceModel::reset()
{
set_deviceAuthState(static_cast<int>(DeviceAuthState::INIT));
set_linkDeviceError("");
set_ipAddress("");
set_tokenErrorMessage("");
}

57
src/app/linkdevicemodel.h Normal file
View file

@ -0,0 +1,57 @@
/*
* Copyright (C) 2025-2025 Savoir-faire Linux Inc.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "api/account.h"
#include "qmladapterbase.h"
#include "qtutils.h"
#include <QObject>
#include <QVariant>
#include <QMap>
class LRCInstance;
class LinkDeviceModel : public QObject
{
Q_OBJECT
QML_PROPERTY(QString, tokenErrorMessage);
QML_PROPERTY(QString, linkDeviceError);
QML_PROPERTY(int, deviceAuthState);
QML_PROPERTY(QString, ipAddress);
public:
explicit LinkDeviceModel(LRCInstance* lrcInstance, QObject* parent = nullptr);
Q_INVOKABLE void addDevice(const QString& token);
Q_INVOKABLE void confirmAddDevice();
Q_INVOKABLE void cancelAddDevice();
Q_INVOKABLE void reset();
private:
bool checkNewStateValidity(lrc::api::account::DeviceAuthState newState) const;
void handleConnectingSignal();
void handleAuthenticatingSignal(const QVariantMap& details);
void handleInProgressSignal();
void handleDoneSignal(const QVariantMap& details);
LRCInstance* lrcInstance_ = nullptr;
uint32_t operationId_;
};

View file

@ -74,7 +74,7 @@ Item {
property string scanToImportAccount: qsTr("Scan this QR code on your other device to proceed with importing your account.") property string scanToImportAccount: qsTr("Scan this QR code on your other device to proceed with importing your account.")
property string waitingForToken: qsTr("Please wait…") property string waitingForToken: qsTr("Please wait…")
property string scanQRCode: qsTr("Scan QR code") property string scanQRCode: qsTr("Scan QR code")
property string connectingToDevice: qsTr("Action required.\nPlease confirm account on your old device.") property string connectingToDevice: qsTr("Action required.\nPlease confirm account on the source device.")
property string confirmAccountImport: qsTr("Authenticating device") property string confirmAccountImport: qsTr("Authenticating device")
property string transferringAccount: qsTr("Transferring account…") property string transferringAccount: qsTr("Transferring account…")
property string cantScanQRCode: qsTr("If you are unable to scan the QR code, enter this token on your other device to proceed.") property string cantScanQRCode: qsTr("If you are unable to scan the QR code, enter this token on your other device to proceed.")
@ -600,12 +600,17 @@ Item {
property string enterAccountPassword: qsTr("Enter account password") property string enterAccountPassword: qsTr("Enter account password")
property string enterPasswordPinCode: qsTr("This account is password encrypted, enter the password to generate a PIN code.") property string enterPasswordPinCode: qsTr("This account is password encrypted, enter the password to generate a PIN code.")
property string addDevice: qsTr("Add Device") property string addDevice: qsTr("Add Device")
property string pinExpired: qsTr("PIN code has expired.")
property string onAnotherDevice: qsTr("On another device")
property string onAnotherDeviceInstruction: qsTr("Install and launch Jami, select “Import from another device” and scan the QR code.")
property string linkNewDevice: qsTr("Link new device") property string linkNewDevice: qsTr("Link new device")
property string linkingInstructions: qsTr("In Jami, scan the QR code or manually enter the PIN code.") property string linkDeviceConnecting: qsTr("Connecting to your new device…")
property string pinValidity: qsTr("The PIN code will expire in: ") property string linkDeviceInProgress: qsTr("The export account operation to the new device is in progress.\nPlease confirm the import on the new device.")
property string linkDeviceScanQR: qsTr("On the new device, initiate a new account.\nSelect Add account -> Connect from another device.\nWhen ready, scan the QR code.")
property string linkDeviceEnterManually: qsTr("Alternatively you could enter the authentication code manually.")
property string linkDeviceEnterCodePlaceholder: qsTr("Enter authentication code")
property string linkDeviceAllSet: qsTr("You are all set!\nYour account is successfully imported on the new device!")
property string linkDeviceFoundAddress: qsTr("New device found at address below. Is that you?\nClicking on confirm will continue transfering account.")
property string linkDeviceNewDeviceIP: qsTr("New device IP address: %1")
property string linkDeviceCloseWarningTitle: qsTr("Do you want to exit?")
property string linkDeviceCloseWarningMessage: qsTr("Exiting will cancel the import account operation.")
// PasswordDialog // PasswordDialog
property string enterPassword: qsTr("Enter password") property string enterPassword: qsTr("Enter password")

View file

@ -62,6 +62,8 @@
#include "pluginlistpreferencemodel.h" #include "pluginlistpreferencemodel.h"
#include "preferenceitemlistmodel.h" #include "preferenceitemlistmodel.h"
#include "wizardviewstepmodel.h" #include "wizardviewstepmodel.h"
#include "linkdevicemodel.h"
#include "qrcodescannermodel.h"
#include "api/peerdiscoverymodel.h" #include "api/peerdiscoverymodel.h"
#include "api/codecmodel.h" #include "api/codecmodel.h"
@ -185,6 +187,12 @@ registerTypes(QQmlEngine* engine,
QQmlEngine::setObjectOwnership(wizardViewStepModel, QQmlEngine::CppOwnership); QQmlEngine::setObjectOwnership(wizardViewStepModel, QQmlEngine::CppOwnership);
REG_QML_SINGLETON<WizardViewStepModel>(REG_MODEL, "WizardViewStepModel", CREATE(wizardViewStepModel)); REG_QML_SINGLETON<WizardViewStepModel>(REG_MODEL, "WizardViewStepModel", CREATE(wizardViewStepModel));
// LinkDeviceModel
auto linkdevicemodel = new LinkDeviceModel(lrcInstance);
qApp->setProperty("LinkDeviceModel", QVariant::fromValue(linkdevicemodel));
QQmlEngine::setObjectOwnership(linkdevicemodel, QQmlEngine::CppOwnership);
REG_QML_SINGLETON<LinkDeviceModel>(REG_MODEL, "LinkDeviceModel", CREATE(linkdevicemodel));
// Register app-level objects that are used by QML created objects. // Register app-level objects that are used by QML created objects.
// These MUST be set prior to loading the initial QML file, in order to // These MUST be set prior to loading the initial QML file, in order to
// be available to the QML adapter class factory creation methods. // be available to the QML adapter class factory creation methods.
@ -195,6 +203,7 @@ registerTypes(QQmlEngine* engine,
qApp->setProperty("PreviewEngine", QVariant::fromValue(previewEngine)); qApp->setProperty("PreviewEngine", QVariant::fromValue(previewEngine));
// qml adapter registration // qml adapter registration
QML_REGISTERSINGLETON_TYPE(NS_HELPERS, QRCodeScannerModel);
QML_REGISTERSINGLETON_TYPE(NS_HELPERS, AvatarRegistry); QML_REGISTERSINGLETON_TYPE(NS_HELPERS, AvatarRegistry);
QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, AccountAdapter); QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, AccountAdapter);
QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, CallAdapter); QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, CallAdapter);

View file

@ -0,0 +1,63 @@
/*
* Copyright (C) 2025-2025 Savoir-faire Linux Inc.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "qrcodescannermodel.h"
#include <Barcode.h>
#include <MultiFormatReader.h>
#include <ReadBarcode.h>
#include <QDebug>
QRCodeScannerModel::QRCodeScannerModel(QObject* parent)
: QObject(parent)
{}
QString
QRCodeScannerModel::scanImage(const QImage& image)
{
if (image.isNull())
return QString();
// Convert QImage to grayscale and get raw data
QImage grayImage = image.convertToFormat(QImage::Format_Grayscale8);
int width = grayImage.width();
int height = grayImage.height();
try {
// Create ZXing image
ZXing::ImageView imageView(grayImage.bits(), width, height, ZXing::ImageFormat::Lum);
// Configure reader
ZXing::ReaderOptions options;
options.setTryHarder(true);
options.setTryRotate(true);
options.setFormats(ZXing::BarcodeFormat::QRCode);
// Try to detect QR code
auto result = ZXing::ReadBarcode(imageView, options);
if (result.isValid()) {
QString text = QString::fromStdString(result.text());
return text;
}
} catch (const std::exception& e) {
qWarning() << "QR code scanning error:" << e.what();
}
return QString();
}

View file

@ -0,0 +1,39 @@
/*
* Copyright (C) 2025-2025 Savoir-faire Linux Inc.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <QObject>
#include <QString>
#include <QImage>
#include <QQmlEngine> // QML registration
class QRCodeScannerModel : public QObject
{
Q_OBJECT
public:
static QRCodeScannerModel* create(QQmlEngine*, QJSEngine*)
{
return new QRCodeScannerModel();
}
explicit QRCodeScannerModel(QObject* parent = nullptr);
Q_INVOKABLE QString scanImage(const QImage& image);
};

View file

@ -20,6 +20,8 @@ import QtQuick.Layouts
import net.jami.Models 1.1 import net.jami.Models 1.1
import net.jami.Adapters 1.1 import net.jami.Adapters 1.1
import net.jami.Constants 1.1 import net.jami.Constants 1.1
import net.jami.Enums 1.1
import Qt.labs.platform
import "../../commoncomponents" import "../../commoncomponents"
import "../../mainview/components" import "../../mainview/components"
@ -32,368 +34,381 @@ BaseModalDialog {
property bool darkTheme: UtilsAdapter.useApplicationTheme() property bool darkTheme: UtilsAdapter.useApplicationTheme()
popupContent: StackLayout { autoClose: false
id: stackedWidget closeButtonVisible: false
function setGeneratingPage() { // Function to check if dialog can be closed directly
if (passwordEdit.length === 0 && CurrentAccount.hasArchivePassword) { function canCloseDirectly() {
setExportPage(NameDirectory.ExportOnRingStatus.WRONG_PASSWORD, ""); return LinkDeviceModel.deviceAuthState === DeviceAuthStateEnum.INIT ||
return; LinkDeviceModel.deviceAuthState === DeviceAuthStateEnum.DONE
} }
stackedWidget.currentIndex = exportingSpinnerPage.pageIndex;
spinnerMovie.playing = true; // Close button. Use custom close button to show a confirmation dialog.
JamiPushButton {
anchors {
top: parent.top
right: parent.right
topMargin: 5
rightMargin: 5
} }
function setExportPage(status, pin) { Layout.preferredHeight: 20
if (status === NameDirectory.ExportOnRingStatus.SUCCESS) { Layout.preferredWidth: 20
infoLabel.success = true;
pinRectangle.visible = true imageColor: hovered ? JamiTheme.textColor : JamiTheme.buttonTintedGreyHovered
exportedPIN.text = pin; normalColor: "transparent"
source: JamiResources.round_close_24dp_svg
onClicked: {
if (canCloseDirectly()) {
root.close();
} else { } else {
infoLabel.success = false; confirmCloseDialog.open();
infoLabel.visible = true;
switch (status) {
case NameDirectory.ExportOnRingStatus.WRONG_PASSWORD:
infoLabel.text = JamiStrings.incorrectPassword;
break;
case NameDirectory.ExportOnRingStatus.NETWORK_ERROR:
infoLabel.text = JamiStrings.linkDeviceNetWorkError;
break;
case NameDirectory.ExportOnRingStatus.INVALID:
infoLabel.text = JamiStrings.somethingWentWrong;
break;
}
}
stackedWidget.currentIndex = exportingInfoPage.pageIndex;
stackedWidget.height = exportingLayout.implicitHeight;
}
onVisibleChanged: {
if (visible) {
if (CurrentAccount.hasArchivePassword) {
stackedWidget.currentIndex = enterPasswordPage.pageIndex;
} else {
setGeneratingPage();
}
} }
} }
}
// Index = 0 MessageDialog {
Item { id: confirmCloseDialog
id: enterPasswordPage
readonly property int pageIndex: 0 text: JamiStrings.linkDeviceCloseWarningTitle
informativeText: JamiStrings.linkDeviceCloseWarningMessage
buttons: MessageDialog.Ok | MessageDialog.Cancel
Component.onCompleted: passwordEdit.forceActiveFocus() onOkClicked: function(button) {
root.close();
}
}
onHeightChanged: { popupContent: Item {
stackedWidget.height = passwordLayout.implicitHeight id: content
} width: 400
height: 450
ColumnLayout { // Scrollable container for StackLayout
id: passwordLayout ScrollView {
spacing: JamiTheme.preferredMarginSize id: scrollView
anchors.centerIn: parent
Label { anchors.fill: parent
Layout.alignment: Qt.AlignCenter
Layout.maximumWidth: root.width - 4 * JamiTheme.preferredMarginSize
wrapMode: Text.Wrap
text: JamiStrings.enterPasswordPinCode anchors.leftMargin: 20
color: JamiTheme.textColor anchors.rightMargin: 20
font.pointSize: JamiTheme.textFontSize anchors.bottomMargin: 20
font.kerning: true clip: true
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
RowLayout { ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
Layout.topMargin: 10 ScrollBar.vertical.policy: ScrollBar.AsNeeded
Layout.leftMargin: JamiTheme.cornerIconSize contentHeight: stackLayout.implicitHeight
Layout.rightMargin: JamiTheme.cornerIconSize
spacing: JamiTheme.preferredMarginSize
Layout.bottomMargin: JamiTheme.preferredMarginSize
PasswordTextEdit { StackLayout {
id: passwordEdit id: stackLayout
width: Math.min(scrollView.width, scrollView.availableWidth)
firstEntry: true currentIndex: scanAndEnterCodeView.index
placeholderText: JamiStrings.password
Layout.alignment: Qt.AlignLeft Connections {
Layout.fillWidth: true target: LinkDeviceModel
KeyNavigation.up: btnConfirm function onDeviceAuthStateChanged() {
KeyNavigation.down: KeyNavigation.up switch (LinkDeviceModel.deviceAuthState) {
case DeviceAuthStateEnum.INIT:
onDynamicTextChanged: { stackLayout.currentIndex = scanAndEnterCodeView.index
btnConfirm.enabled = dynamicText.length > 0; break
btnConfirm.hoverEnabled = dynamicText.length > 0; case DeviceAuthStateEnum.CONNECTING:
stackLayout.currentIndex = deviceLinkLoadingView.index
deviceLinkLoadingView.loadingText = JamiStrings.linkDeviceConnecting
break
case DeviceAuthStateEnum.AUTHENTICATING:
stackLayout.currentIndex = deviceConfirmationView.index
break
case DeviceAuthStateEnum.IN_PROGRESS:
stackLayout.currentIndex = deviceLinkLoadingView.index
deviceLinkLoadingView.loadingText = JamiStrings.linkDeviceInProgress
break
case DeviceAuthStateEnum.DONE:
if (LinkDeviceModel.linkDeviceError.length > 0) {
stackLayout.currentIndex = deviceLinkErrorView.index
} else {
stackLayout.currentIndex = deviceLinkSuccessView.index
}
break
default:
break
} }
onAccepted: btnConfirm.clicked()
}
JamiPushButton {
id: btnConfirm
Layout.alignment: Qt.AlignCenter
height: 36
width: 36
hoverEnabled: false
enabled: false
imageColor: JamiTheme.secondaryBackgroundColor
hoveredColor: JamiTheme.buttonTintedBlueHovered
source: JamiResources.check_black_24dp_svg
normalColor: JamiTheme.tintedBlue
onClicked: stackedWidget.setGeneratingPage()
} }
} }
}
}
// Index = 1 // Common base component for stack layout items
Item { component StackViewBase: Item {
id: exportingSpinnerPage id: baseItem
readonly property int pageIndex: 1 required property string title
default property alias content: contentLayout.data
onHeightChanged: { Layout.fillWidth: true
stackedWidget.height = spinnerLayout.implicitHeight Layout.alignment: Qt.AlignHCenter
} implicitHeight: contentLayout.implicitHeight
onWidthChanged: stackedWidget.width = exportingLayout.implicitWidth
ColumnLayout { ColumnLayout {
id: spinnerLayout id: contentLayout
anchors {
spacing: JamiTheme.preferredMarginSize left: parent.left
anchors.centerIn: parent right: parent.right
verticalCenter: parent.verticalCenter
Label { }
Layout.alignment: Qt.AlignCenter Layout.preferredWidth: scrollView.width
spacing: 20
text: JamiStrings.linkDevice }
color: JamiTheme.textColor
font.pointSize: JamiTheme.headerFontSize
font.kerning: true
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
} }
AnimatedImage { StackViewBase {
id: spinnerMovie id: deviceLinkErrorView
property int index: 0
title: "Error"
Layout.alignment: Qt.AlignCenter Text {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: 30 text: LinkDeviceModel.linkDeviceError
Layout.preferredHeight: 30 Layout.preferredWidth: scrollView.width
color: JamiTheme.textColor
source: JamiResources.jami_rolling_spinner_gif font.pixelSize: JamiTheme.wizardViewDescriptionFontPixelSize
playing: visible lineHeight: JamiTheme.wizardViewTextLineHeight
fillMode: Image.PreserveAspectFit horizontalAlignment: Text.AlignHCenter
mipmap: true wrapMode: Text.WrapAtWordBoundaryOrAnywhere
} }
}
}
// Index = 2
Item {
id: exportingInfoPage
readonly property int pageIndex: 2
width: childrenRect.width
height: childrenRect.height
onHeightChanged: {
stackedWidget.height = exportingLayout.implicitHeight
}
onWidthChanged: stackedWidget.width = exportingLayout.implicitWidth
ColumnLayout {
id: exportingLayout
spacing: JamiTheme.preferredMarginSize
Label {
id: instructionLabel
Layout.maximumWidth: Math.min(root.maximumPopupWidth, root.width) - 2 * root.popupMargins
Layout.alignment: Qt.AlignLeft
color: JamiTheme.textColor
wrapMode: Text.Wrap
text: JamiStrings.linkingInstructions
font.pointSize: JamiTheme.textFontSize
font.kerning: true
verticalAlignment: Text.AlignVCenter
MaterialButton {
Layout.alignment: Qt.AlignHCenter
text: JamiStrings.close
toolTipText: JamiStrings.optionTryAgain
primary: true
onClicked: {
root.close();
}
}
} }
RowLayout { StackViewBase {
spacing: 10 id: deviceLinkSuccessView
Layout.maximumWidth: Math.min(root.maximumPopupWidth, root.width) - 2 * root.popupMargins property int index: 1
title: "Success"
Rectangle { Text {
Layout.alignment: Qt.AlignCenter Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: scrollView.width
text: JamiStrings.linkDeviceAllSet
color: JamiTheme.textColor
font.pixelSize: JamiTheme.wizardViewDescriptionFontPixelSize
lineHeight: JamiTheme.wizardViewTextLineHeight
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
}
radius: 5 MaterialButton {
color: JamiTheme.backgroundRectangleColor Layout.alignment: Qt.AlignHCenter
width: 100 text: JamiStrings.close
height: 100 toolTipText: JamiStrings.optionTryAgain
primary: true
onClicked: {
root.close();
}
}
}
Rectangle { StackViewBase {
width: qrImage.width + 4 id: deviceLinkLoadingView
height: qrImage.height + 4 property int index: 2
anchors.centerIn: parent title: "Loading"
radius: 5 property string loadingText: ""
color: JamiTheme.whiteColor
Image { BusyIndicator {
id: qrImage Layout.alignment: Qt.AlignHCenter
anchors.centerIn: parent Layout.preferredWidth: 50
mipmap: false Layout.preferredHeight: 50
smooth: false running: true
source: "image://qrImage/raw_" + exportedPIN.text }
sourceSize.width: 80
sourceSize.height: 80 Text {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: scrollView.width
text: deviceLinkLoadingView.loadingText
color: JamiTheme.textColor
font.pixelSize: JamiTheme.wizardViewDescriptionFontPixelSize
lineHeight: JamiTheme.wizardViewTextLineHeight
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
opacity: 1
SequentialAnimation on opacity {
running: true
loops: Animation.Infinite
NumberAnimation {
from: 1
to: 0.3
duration: 1000
easing.type: Easing.InOutQuad
}
NumberAnimation {
from: 0.3
to: 1
duration: 1000
easing.type: Easing.InOutQuad
} }
} }
}
Rectangle {
id: pinRectangle
radius: 5
color: JamiTheme.backgroundRectangleColor
Layout.fillWidth: true
height: 100
Layout.minimumWidth: exportedPIN.width + 20
Layout.alignment: Qt.AlignCenter
MaterialLineEdit {
id: exportedPIN
padding: 10
anchors.centerIn: parent
text: JamiStrings.pin
wrapMode: Text.NoWrap
backgroundColor: JamiTheme.backgroundRectangleColor
color: darkTheme ? JamiTheme.editLineColor : JamiTheme.darkTintedBlue
selectByMouse: true
readOnly: true
font.pointSize: JamiTheme.tinyCreditsTextSize
font.kerning: true
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
} }
} }
Rectangle { StackViewBase {
radius: 5 id: deviceConfirmationView
color: JamiTheme.infoRectangleColor property int index: 3
Layout.fillWidth: true title: "Confirmation"
Layout.preferredHeight: infoLabels.height + 38
Text {
id: explanationConnect
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: scrollView.width
text: JamiStrings.linkDeviceFoundAddress
font.pixelSize: JamiTheme.wizardViewDescriptionFontPixelSize
lineHeight: JamiTheme.wizardViewTextLineHeight
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
color: JamiTheme.textColor
}
Text {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: scrollView.width
text: JamiStrings.linkDeviceNewDeviceIP.arg(LinkDeviceModel.ipAddress)
font.pixelSize: JamiTheme.wizardViewDescriptionFontPixelSize
lineHeight: JamiTheme.wizardViewTextLineHeight
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
color: JamiTheme.textColor
font.weight: Font.Bold
}
RowLayout { RowLayout {
id: infoLayout Layout.alignment: Qt.AlignHCenter
spacing: 16
anchors.centerIn: parent MaterialButton {
anchors.fill: parent id: confirm
anchors.margins: 14 primary: true
spacing: 10 Layout.alignment: Qt.AlignCenter
text: JamiStrings.optionConfirm
ResponsiveImage{ toolTipText: JamiStrings.optionConfirm
Layout.fillWidth: true onClicked: {
LinkDeviceModel.confirmAddDevice()
source: JamiResources.outline_info_24dp_svg }
fillMode: Image.PreserveAspectFit
color: darkTheme ? JamiTheme.editLineColor : JamiTheme.darkTintedBlue
Layout.fillHeight: true
} }
ColumnLayout{ MaterialButton {
id: infoLabels id: cancel
Layout.alignment: Qt.AlignCenter
Layout.fillHeight: true secondary: true
Layout.fillWidth: true toolTipText: JamiStrings.cancel
textLeftPadding: JamiTheme.buttontextWizzardPadding / 2
Label { textRightPadding: JamiTheme.buttontextWizzardPadding / 2
id: otherDeviceLabel text: JamiStrings.cancel
onClicked: {
Layout.alignment: Qt.AlignLeft LinkDeviceModel.cancelAddDevice()
color: JamiTheme.textColor
text: JamiStrings.onAnotherDevice
font.pointSize: JamiTheme.smallFontSize
font.kerning: true
font.bold: true
}
Label {
id: otherInstructionLabel
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft
wrapMode: Text.Wrap
color: JamiTheme.textColor
text: JamiStrings.onAnotherDeviceInstruction
font.pointSize: JamiTheme.smallFontSize
font.kerning: true
} }
} }
} }
} }
// Displays error messages StackViewBase {
Label { id: scanAndEnterCodeView
id: infoLabel property int index: 4
title: "Scan"
visible: false Component.onDestruction: {
if (qrScanner) {
qrScanner.stopScanner()
}
}
property bool success: false Text {
property int borderWidth: success ? 1 : 0 id: explanationScan
property int borderRadius: success ? 15 : 0 Layout.alignment: Qt.AlignHCenter
property string backgroundColor: success ? "whitesmoke" : "transparent" Layout.preferredWidth: scrollView.width
property string borderColor: success ? "lightgray" : "transparent" text: JamiStrings.linkDeviceScanQR
font.pixelSize: JamiTheme.wizardViewDescriptionFontPixelSize
lineHeight: JamiTheme.wizardViewTextLineHeight
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
color: JamiTheme.textColor
}
Layout.maximumWidth: JamiTheme.preferredDialogWidth QRCodeScanner {
Layout.margins: JamiTheme.preferredMarginSize id: qrScanner
Layout.alignment: Qt.AlignHCenter
width: 250
height: width * aspectRatio
visible: VideoDevices.listSize !== 0
Layout.alignment: Qt.AlignCenter onQrCodeDetected: function(code) {
console.log("QR code detected:", code)
LinkDeviceModel.addDevice(code)
}
}
color: success ? JamiTheme.successLabelColor : JamiTheme.redColor ColumnLayout {
padding: success ? 8 : 0 id: manualEntry
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: scrollView.width
spacing: 10
wrapMode: Text.Wrap Text {
font.pointSize: success ? JamiTheme.textFontSize : JamiTheme.textFontSize + 3 id: explanation
font.kerning: true Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter Layout.preferredWidth: scrollView.width
verticalAlignment: Text.AlignVCenter text: JamiStrings.linkDeviceEnterManually
font.pixelSize: JamiTheme.wizardViewDescriptionFontPixelSize
lineHeight: JamiTheme.wizardViewTextLineHeight
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
color: JamiTheme.textColor
}
background: Rectangle { ModalTextEdit {
id: infoLabelBackground id: codeInput
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: scrollView.width
Layout.preferredHeight: JamiTheme.preferredFieldHeight
placeholderText: JamiStrings.linkDeviceEnterCodePlaceholder
}
border.width: infoLabel.borderWidth Text {
border.color: infoLabel.borderColor Layout.alignment: Qt.AlignHCenter
radius: infoLabel.borderRadius Layout.maximumWidth: parent.width - 40
color: JamiTheme.secondaryBackgroundColor visible: LinkDeviceModel.tokenErrorMessage.length > 0
text: LinkDeviceModel.tokenErrorMessage
font.pointSize: JamiTheme.tinyFontSize
color: JamiTheme.redColor
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
}
}
MaterialButton {
id: connect
Layout.alignment: Qt.AlignHCenter
primary: true
text: JamiStrings.connect
toolTipText: JamiStrings.connect
enabled: codeInput.dynamicText.length > 0
onClicked: {
LinkDeviceModel.addDevice(codeInput.text)
}
} }
} }
} }
} }
} }
//Reset everything when dialog is closed
onClosed: {
LinkDeviceModel.reset()
}
} }

View file

@ -0,0 +1,134 @@
/*
* Copyright (C) 2025-2025 Savoir-faire Linux Inc.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import QtQuick
import net.jami.Constants 1.1
import net.jami.Adapters 1.1
import net.jami.Helpers 1.1
import "../../commoncomponents"
Item {
id: root
property bool isScanning: false
property real aspectRatio: 0.5625
onVisibleChanged: {
if (visible) {
startScanner()
} else {
stopScanner()
}
}
Component.onDestruction: {
stopScanner()
}
Rectangle {
id: cameraContainer
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width
height: parent.height
color: JamiTheme.primaryForegroundColor
clip: true
LocalVideo {
id: previewWidget
anchors.fill: parent
flip: true
// Camera not available
underlayItems: Text {
id: noCameraText
anchors.centerIn: parent
font.pointSize: 18
font.capitalization: Font.AllUppercase
color: "white"
text: JamiStrings.noCamera
visible: false // Start hidden
// Delay "No Camera" message to avoid flashing it when camera is starting up.
// If camera starts successfully within 5 seconds, user won't see this message.
// If there's a camera issue, message will be shown after the delay.
Timer {
id: visibilityTimer
interval: 5000
running: true
repeat: false
onTriggered: {
noCameraText.visible = true
destroy() // Remove the timer after it's done
}
}
}
}
// Scanning line animation
Rectangle {
id: scanLine
width: parent.width
height: 2
color: JamiTheme.whiteColor
opacity: 0.8
visible: root.isScanning && previewWidget.isRendering
SequentialAnimation on y {
running: root.isScanning
loops: Animation.Infinite
NumberAnimation {
from: 0
to: cameraContainer.height
duration: 2500
easing.type: Easing.InOutQuad
}
NumberAnimation {
from: cameraContainer.height
to: 0
duration: 2500
easing.type: Easing.InOutQuad
}
}
}
}
Timer {
id: scanTimer
interval: 500
repeat: true
running: root.isScanning && previewWidget.isRendering
onTriggered: {
var result = QRCodeScannerModel.scanImage(videoProvider.captureRawVideoFrame(VideoDevices.getDefaultDevice()));
if (result !== "") {
root.isScanning = false
root.qrCodeDetected(result)
}
}
}
signal qrCodeDetected(string code)
function startScanner() {
previewWidget.startWithId(VideoDevices.getDefaultDevice())
root.isScanning = true
}
function stopScanner() {
previewWidget.startWithId("")
root.isScanning = true
}
}

View file

@ -17,11 +17,11 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Dialogs
import net.jami.Adapters 1.1 import net.jami.Adapters 1.1
import net.jami.Models 1.1 import net.jami.Models 1.1
import net.jami.Constants 1.1 import net.jami.Constants 1.1
import net.jami.Enums 1.1 import net.jami.Enums 1.1
import Qt.labs.platform
import "../../commoncomponents" import "../../commoncomponents"
import "../../mainview/components" import "../../mainview/components"
@ -77,11 +77,9 @@ Rectangle {
informativeText: JamiStrings.linkDeviceCloseWarningMessage informativeText: JamiStrings.linkDeviceCloseWarningMessage
buttons: MessageDialog.Ok | MessageDialog.Cancel buttons: MessageDialog.Ok | MessageDialog.Cancel
onButtonClicked: function(button) { onOkClicked: function(button) {
if (button === MessageDialog.Ok) { AccountAdapter.cancelImportAccount();
AccountAdapter.cancelImportAccount(); WizardViewStepModel.previousStep();
WizardViewStepModel.previousStep();
}
} }
} }

View file

@ -126,6 +126,18 @@ public Q_SLOTS:
int state, int state,
const MapStringString& details); const MapStringString& details);
/**
* Emit addDeviceStateChanged.
* @param accountId
* @param operationId
* @param state
* @param details
*/
void slotAddDeviceStateChanged(const QString& accountID,
uint32_t operationId,
int state,
const MapStringString& details);
/** /**
* @param accountId * @param accountId
* @param details * @param details
@ -430,6 +442,10 @@ AccountModelPimpl::AccountModelPimpl(AccountModel& linked,
&CallbacksHandler::deviceAuthStateChanged, &CallbacksHandler::deviceAuthStateChanged,
&linked, &linked,
&AccountModel::deviceAuthStateChanged); &AccountModel::deviceAuthStateChanged);
connect(&callbacksHandler,
&CallbacksHandler::addDeviceStateChanged,
&linked,
&AccountModel::addDeviceStateChanged);
connect(&callbacksHandler, connect(&callbacksHandler,
&CallbacksHandler::nameRegistrationEnded, &CallbacksHandler::nameRegistrationEnded,
this, this,
@ -627,6 +643,15 @@ AccountModelPimpl::slotDeviceAuthStateChanged(const QString& accountId,
Q_EMIT linked.deviceAuthStateChanged(accountId, state, details); Q_EMIT linked.deviceAuthStateChanged(accountId, state, details);
} }
void
AccountModelPimpl::slotAddDeviceStateChanged(const QString& accountId,
uint32_t operationId,
int state,
const MapStringString& details)
{
Q_EMIT linked.addDeviceStateChanged(accountId, operationId, state, details);
}
void void
AccountModelPimpl::slotNameRegistrationEnded(const QString& accountId, AccountModelPimpl::slotNameRegistrationEnded(const QString& accountId,
int status, int status,

View file

@ -247,6 +247,12 @@ CallbacksHandler::CallbacksHandler(const Lrc& parent)
&CallbacksHandler::slotDeviceAuthStateChanged, &CallbacksHandler::slotDeviceAuthStateChanged,
Qt::QueuedConnection); Qt::QueuedConnection);
connect(&ConfigurationManager::instance(),
&ConfigurationManagerInterface::addDeviceStateChanged,
this,
&CallbacksHandler::slotAddDeviceStateChanged,
Qt::QueuedConnection);
connect(&ConfigurationManager::instance(), connect(&ConfigurationManager::instance(),
&ConfigurationManagerInterface::nameRegistrationEnded, &ConfigurationManagerInterface::nameRegistrationEnded,
this, this,
@ -687,6 +693,15 @@ CallbacksHandler::slotDeviceAuthStateChanged(const QString& accountId,
Q_EMIT deviceAuthStateChanged(accountId, state, details); Q_EMIT deviceAuthStateChanged(accountId, state, details);
} }
void
CallbacksHandler::slotAddDeviceStateChanged(const QString& accountId,
uint32_t operationId,
int state,
const MapStringString& details)
{
Q_EMIT addDeviceStateChanged(accountId, operationId, state, details);
}
void void
CallbacksHandler::slotNameRegistrationEnded(const QString& accountId, CallbacksHandler::slotNameRegistrationEnded(const QString& accountId,
int status, int status,

View file

@ -244,6 +244,19 @@ Q_SIGNALS:
*/ */
void deviceAuthStateChanged(const QString& accountId, int state, const MapStringString& details); void deviceAuthStateChanged(const QString& accountId, int state, const MapStringString& details);
/**
* Add device state has changed
* @param accountId
* @param operationId
* @param state
* @param details map
*/
void addDeviceStateChanged(const QString& accountId,
uint32_t operationId,
int state,
const MapStringString& details);
/** /**
* Name registration has ended * Name registration has ended
* @param accountId * @param accountId
@ -587,6 +600,18 @@ private Q_SLOTS:
int state, int state,
const MapStringString& details); const MapStringString& details);
/**
* Add device state has changed
* @param accountId
* @param operationId
* @param state
* @param details map
*/
void slotAddDeviceStateChanged(const QString& accountId,
uint32_t operationId,
int state,
const MapStringString& details);
/** /**
* Emit nameRegistrationEnded * Emit nameRegistrationEnded
* @param accountId * @param accountId