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:
parent
33da15daba
commit
d3c76eac8d
15 changed files with 857 additions and 314 deletions
4
.gitmodules
vendored
4
.gitmodules
vendored
|
@ -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
1
3rdparty/zxing-cpp
vendored
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit a920817b6fe0508cc4aca9003003c2812a78e935
|
|
@ -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
140
src/app/linkdevicemodel.cpp
Normal 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
57
src/app/linkdevicemodel.h
Normal 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_;
|
||||||
|
};
|
|
@ -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")
|
||||||
|
|
|
@ -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);
|
||||||
|
|
63
src/app/qrcodescannermodel.cpp
Normal file
63
src/app/qrcodescannermodel.cpp
Normal 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();
|
||||||
|
}
|
39
src/app/qrcodescannermodel.h
Normal file
39
src/app/qrcodescannermodel.h
Normal 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);
|
||||||
|
};
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
134
src/app/settingsview/components/QRCodeScanner.qml
Normal file
134
src/app/settingsview/components/QRCodeScanner.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Reference in a new issue