Feature : Avatar from default account.

- Update SDK for accountRemoved callback and fix on setting default account.
- Update AccountGui from list modification and default account selection.
- Add Avatar provider for Qml.
- Create avatar file and store it into avatars folder.
- Delete old avatar file if replaced.
This commit is contained in:
Julien Wadel 2023-11-29 14:47:04 +01:00
parent 9e1e797d8c
commit b316074feb
14 changed files with 120 additions and 29 deletions

View file

@ -46,9 +46,9 @@
#include "model/object/VariantObject.hpp" #include "model/object/VariantObject.hpp"
#include "tool/Constants.hpp" #include "tool/Constants.hpp"
#include "tool/Utils.hpp" #include "tool/Utils.hpp"
#include "tool/thread/Thread.hpp" #include "tool/providers/AvatarProvider.hpp"
#include "tool/providers/ImageProvider.hpp" #include "tool/providers/ImageProvider.hpp"
#include "tool/thread/Thread.hpp"
App::App(int &argc, char *argv[]) App::App(int &argc, char *argv[])
: SingleApplication(argc, argv, true, Mode::User | Mode::ExcludeAppPath | Mode::ExcludeAppVersion) { : SingleApplication(argc, argv, true, Mode::User | Mode::ExcludeAppPath | Mode::ExcludeAppVersion) {
@ -102,6 +102,7 @@ void App::init() {
mEngine->rootContext()->setContextProperty("applicationDirPath", QGuiApplication::applicationDirPath()); mEngine->rootContext()->setContextProperty("applicationDirPath", QGuiApplication::applicationDirPath());
initCppInterfaces(); initCppInterfaces();
mEngine->addImageProvider(ImageProvider::ProviderId, new ImageProvider()); mEngine->addImageProvider(ImageProvider::ProviderId, new ImageProvider());
mEngine->addImageProvider(AvatarProvider::ProviderId, new AvatarProvider());
// Enable notifications. // Enable notifications.
mNotifier = new Notifier(mEngine); mNotifier = new Notifier(mEngine);

View file

@ -71,7 +71,8 @@ void AccountList::setSelf(QSharedPointer<AccountList> me) {
}); });
}); });
}); });
mModelConnection->makeConnect(CoreModel::getInstance().get(), &CoreModel::defaultAccountChanged,
[this]() { mModelConnection->invokeToCore([this]() { defaultAccountChanged(); }); });
lUpdate(); lUpdate();
} }

View file

@ -49,6 +49,7 @@ public:
signals: signals:
void lUpdate(); void lUpdate();
void haveAccountChanged(); void haveAccountChanged();
void defaultAccountChanged();
private: private:
bool mHaveAccount = false; bool mHaveAccount = false;

View file

@ -25,7 +25,8 @@
AccountProxy::AccountProxy(QObject *parent) : SortFilterProxy(parent) { AccountProxy::AccountProxy(QObject *parent) : SortFilterProxy(parent) {
qDebug() << "[AccountProxy] new" << this; qDebug() << "[AccountProxy] new" << this;
mList = AccountList::create(); mList = AccountList::create();
connect(mList.get(), &AccountList::countChanged, this, &AccountProxy::defaultAccountChanged); connect(mList.get(), &AccountList::countChanged, this, &AccountProxy::resetDefaultAccount);
connect(mList.get(), &AccountList::defaultAccountChanged, this, &AccountProxy::resetDefaultAccount);
connect(mList.get(), &AccountList::haveAccountChanged, this, &AccountProxy::haveAccountChanged); connect(mList.get(), &AccountList::haveAccountChanged, this, &AccountProxy::haveAccountChanged);
setSourceModel(mList.get()); setSourceModel(mList.get());
sort(0); sort(0);
@ -47,13 +48,20 @@ void AccountProxy::setFilterText(const QString &filter) {
} }
} }
AccountGui *AccountProxy::getDefaultAccount() const { AccountGui *AccountProxy::getDefaultAccount() {
return dynamic_cast<AccountList *>(sourceModel())->getDefaultAccount(); if (!mDefaultAccount) mDefaultAccount = dynamic_cast<AccountList *>(sourceModel())->getDefaultAccount();
return mDefaultAccount;
} }
void AccountProxy::setDefaultAccount(AccountGui *account) { void AccountProxy::setDefaultAccount(AccountGui *account) {
} }
// Reset the default account to let UI build its new object if needed.
void AccountProxy::resetDefaultAccount() {
mDefaultAccount = nullptr;
this->defaultAccountChanged(); // Warn the UI
}
bool AccountProxy::getHaveAccount() const { bool AccountProxy::getHaveAccount() const {
return dynamic_cast<AccountList *>(sourceModel())->getHaveAccount(); return dynamic_cast<AccountList *>(sourceModel())->getHaveAccount();
} }

View file

@ -41,8 +41,9 @@ public:
QString getFilterText() const; QString getFilterText() const;
void setFilterText(const QString &filter); void setFilterText(const QString &filter);
AccountGui *getDefaultAccount() const; AccountGui *getDefaultAccount(); // Get a new object from List or give the stored one.
void setDefaultAccount(AccountGui *account); void setDefaultAccount(AccountGui *account); // TODO
void resetDefaultAccount(); // Reset the default account to let UI build its new object if needed.
bool getHaveAccount() const; bool getHaveAccount() const;
@ -56,6 +57,7 @@ protected:
virtual bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; virtual bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
QString mFilterText; QString mFilterText;
AccountGui *mDefaultAccount = nullptr; // When null, a new UI object is build from List
QSharedPointer<AccountList> mList; QSharedPointer<AccountList> mList;
}; };

View file

@ -20,10 +20,12 @@
#include "AccountModel.hpp" #include "AccountModel.hpp"
#include <QDebug> #include "core/path/Paths.hpp"
#include "model/core/CoreModel.hpp" #include "model/core/CoreModel.hpp"
#include "tool/Utils.hpp" #include "tool/Utils.hpp"
#include "tool/providers/AvatarProvider.hpp"
#include <QDebug>
#include <QUrl>
DEFINE_ABSTRACT_OBJECT(AccountModel) DEFINE_ABSTRACT_OBJECT(AccountModel)
@ -48,6 +50,15 @@ void AccountModel::setPictureUri(QString uri) {
mustBeInLinphoneThread(log().arg(Q_FUNC_INFO)); mustBeInLinphoneThread(log().arg(Q_FUNC_INFO));
auto account = std::dynamic_pointer_cast<linphone::Account>(mMonitor); auto account = std::dynamic_pointer_cast<linphone::Account>(mMonitor);
auto params = account->getParams()->clone(); auto params = account->getParams()->clone();
auto oldPictureUri = Utils::coreStringToAppString(params->getPictureUri());
if (!oldPictureUri.isEmpty()) {
QString appPrefix = QStringLiteral("image://%1/").arg(AvatarProvider::ProviderId);
if (oldPictureUri.startsWith(appPrefix)) {
oldPictureUri = Paths::getAvatarsDirPath() + oldPictureUri.mid(appPrefix.length());
}
QFile oldPicture(oldPictureUri);
if (!oldPicture.remove()) qWarning() << log().arg("Cannot delete old avatar file at " + oldPictureUri);
}
params->setPictureUri(Utils::appStringToCoreString(uri)); params->setPictureUri(Utils::appStringToCoreString(uri));
account->setParams(params); account->setParams(params);
emit pictureUriChanged(uri); emit pictureUriChanged(uri);
@ -61,5 +72,4 @@ void AccountModel::setDefault() {
mustBeInLinphoneThread(log().arg(Q_FUNC_INFO)); mustBeInLinphoneThread(log().arg(Q_FUNC_INFO));
auto core = CoreModel::getInstance()->getCore(); auto core = CoreModel::getInstance()->getCore();
core->setDefaultAccount(mMonitor); core->setDefaultAccount(mMonitor);
emit CoreModel::getInstance()->defaultAccountChanged();
} }

View file

@ -139,6 +139,14 @@ void CoreModel::setPathAfterStart() {
//--------------------------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------------------------
void CoreModel::onAccountAdded(const std::shared_ptr<linphone::Core> &core,
const std::shared_ptr<linphone::Account> &account) {
emit accountAdded(core, account);
}
void CoreModel::onAccountRemoved(const std::shared_ptr<linphone::Core> &core,
const std::shared_ptr<linphone::Account> &account) {
emit accountRemoved(core, account);
}
void CoreModel::onAccountRegistrationStateChanged(const std::shared_ptr<linphone::Core> &core, void CoreModel::onAccountRegistrationStateChanged(const std::shared_ptr<linphone::Core> &core,
const std::shared_ptr<linphone::Account> &account, const std::shared_ptr<linphone::Account> &account,
linphone::RegistrationState state, linphone::RegistrationState state,
@ -195,6 +203,10 @@ void CoreModel::onConfiguringStatus(const std::shared_ptr<linphone::Core> &core,
const std::string &message) { const std::string &message) {
emit configuringStatus(core, status, message); emit configuringStatus(core, status, message);
} }
void CoreModel::onDefaultAccountChanged(const std::shared_ptr<linphone::Core> &core,
const std::shared_ptr<linphone::Account> &account) {
emit defaultAccountChanged(core, account);
}
void CoreModel::onDtmfReceived(const std::shared_ptr<linphone::Core> &lc, void CoreModel::onDtmfReceived(const std::shared_ptr<linphone::Core> &lc,
const std::shared_ptr<linphone::Call> &call, const std::shared_ptr<linphone::Call> &call,
int dtmf) { int dtmf) {
@ -234,10 +246,7 @@ void CoreModel::onMessagesReceived(const std::shared_ptr<linphone::Core> &core,
const std::list<std::shared_ptr<linphone::ChatMessage>> &messages) { const std::list<std::shared_ptr<linphone::ChatMessage>> &messages) {
emit messagesReceived(core, room, messages); emit messagesReceived(core, room, messages);
} }
void CoreModel::onNewAccountAdded(const std::shared_ptr<linphone::Core> &core,
const std::shared_ptr<linphone::Account> &account) {
emit accountAdded(core, account);
}
void CoreModel::onNewMessageReaction(const std::shared_ptr<linphone::Core> &core, void CoreModel::onNewMessageReaction(const std::shared_ptr<linphone::Core> &core,
const std::shared_ptr<linphone::ChatRoom> &chatRoom, const std::shared_ptr<linphone::ChatRoom> &chatRoom,
const std::shared_ptr<linphone::ChatMessage> &message, const std::shared_ptr<linphone::ChatMessage> &message,

View file

@ -56,7 +56,6 @@ public:
signals: signals:
void loggerInitialized(); void loggerInitialized();
void defaultAccountChanged();
void friendAdded(); void friendAdded();
void friendRemoved(); void friendRemoved();
@ -74,6 +73,10 @@ private:
//-------------------------------------------------------------------------------- //--------------------------------------------------------------------------------
// LINPHONE // LINPHONE
//-------------------------------------------------------------------------------- //--------------------------------------------------------------------------------
virtual void onAccountAdded(const std::shared_ptr<linphone::Core> &core,
const std::shared_ptr<linphone::Account> &account) override;
virtual void onAccountRemoved(const std::shared_ptr<linphone::Core> &core,
const std::shared_ptr<linphone::Account> &account) override;
virtual void onAccountRegistrationStateChanged(const std::shared_ptr<linphone::Core> &core, virtual void onAccountRegistrationStateChanged(const std::shared_ptr<linphone::Core> &core,
const std::shared_ptr<linphone::Account> &account, const std::shared_ptr<linphone::Account> &account,
linphone::RegistrationState state, linphone::RegistrationState state,
@ -107,6 +110,8 @@ private:
virtual void onConfiguringStatus(const std::shared_ptr<linphone::Core> &core, virtual void onConfiguringStatus(const std::shared_ptr<linphone::Core> &core,
linphone::Config::ConfiguringState status, linphone::Config::ConfiguringState status,
const std::string &message) override; const std::string &message) override;
virtual void onDefaultAccountChanged(const std::shared_ptr<linphone::Core> &core,
const std::shared_ptr<linphone::Account> &account) override;
virtual void onDtmfReceived(const std::shared_ptr<linphone::Core> &lc, virtual void onDtmfReceived(const std::shared_ptr<linphone::Core> &lc,
const std::shared_ptr<linphone::Call> &call, const std::shared_ptr<linphone::Call> &call,
int dtmf) override; int dtmf) override;
@ -130,8 +135,6 @@ private:
virtual void onMessagesReceived(const std::shared_ptr<linphone::Core> &core, virtual void onMessagesReceived(const std::shared_ptr<linphone::Core> &core,
const std::shared_ptr<linphone::ChatRoom> &room, const std::shared_ptr<linphone::ChatRoom> &room,
const std::list<std::shared_ptr<linphone::ChatMessage>> &messages) override; const std::list<std::shared_ptr<linphone::ChatMessage>> &messages) override;
virtual void onNewAccountAdded(const std::shared_ptr<linphone::Core> &core,
const std::shared_ptr<linphone::Account> &account) override;
virtual void onNewMessageReaction(const std::shared_ptr<linphone::Core> &core, virtual void onNewMessageReaction(const std::shared_ptr<linphone::Core> &core,
const std::shared_ptr<linphone::ChatRoom> &chatRoom, const std::shared_ptr<linphone::ChatRoom> &chatRoom,
@ -159,7 +162,7 @@ private:
signals: signals:
void accountAdded(const std::shared_ptr<linphone::Core> &core, const std::shared_ptr<linphone::Account> &account); void accountAdded(const std::shared_ptr<linphone::Core> &core, const std::shared_ptr<linphone::Account> &account);
void accountRemoved(); void accountRemoved(const std::shared_ptr<linphone::Core> &core, const std::shared_ptr<linphone::Account> &account);
void accountRegistrationStateChanged(const std::shared_ptr<linphone::Core> &core, void accountRegistrationStateChanged(const std::shared_ptr<linphone::Core> &core,
const std::shared_ptr<linphone::Account> &account, const std::shared_ptr<linphone::Account> &account,
linphone::RegistrationState state, linphone::RegistrationState state,
@ -189,6 +192,8 @@ signals:
void configuringStatus(const std::shared_ptr<linphone::Core> &core, void configuringStatus(const std::shared_ptr<linphone::Core> &core,
linphone::Config::ConfiguringState status, linphone::Config::ConfiguringState status,
const std::string &message); const std::string &message);
void defaultAccountChanged(const std::shared_ptr<linphone::Core> &core,
const std::shared_ptr<linphone::Account> &account);
void dtmfReceived(const std::shared_ptr<linphone::Core> &lc, const std::shared_ptr<linphone::Call> &call, int dtmf); void dtmfReceived(const std::shared_ptr<linphone::Core> &lc, const std::shared_ptr<linphone::Call> &call, int dtmf);
void void
ecCalibrationResult(const std::shared_ptr<linphone::Core> &core, linphone::EcCalibratorStatus status, int delayMs); ecCalibrationResult(const std::shared_ptr<linphone::Core> &core, linphone::EcCalibratorStatus status, int delayMs);

View file

@ -6,6 +6,7 @@ list(APPEND _LINPHONEAPP_SOURCES
tool/thread/SafeSharedPointer.hpp tool/thread/SafeSharedPointer.hpp
tool/thread/SafeConnection.cpp tool/thread/SafeConnection.cpp
tool/thread/Thread.cpp tool/thread/Thread.cpp
tool/providers/AvatarProvider.cpp
tool/providers/ImageProvider.cpp tool/providers/ImageProvider.cpp
) )

View file

@ -22,8 +22,11 @@
#include "core/App.hpp" #include "core/App.hpp"
#include "core/call/CallGui.hpp" #include "core/call/CallGui.hpp"
#include "core/path/Paths.hpp"
#include "model/object/VariantObject.hpp" #include "model/object/VariantObject.hpp"
#include "model/tool/ToolModel.hpp" #include "model/tool/ToolModel.hpp"
#include "tool/providers/AvatarProvider.hpp"
#include <QImageReader>
// ============================================================================= // =============================================================================
@ -78,3 +81,36 @@ VariantObject *Utils::haveAccount() {
result->requestValue(); result->requestValue();
return result; return result;
} }
QString Utils::createAvatar(const QUrl &fileUrl) {
QString filePath = fileUrl.toLocalFile();
QString fileId; // uuid.ext
QString fileUri; // image://avatar/filename.ext
QFile file;
if (!filePath.isEmpty()) {
if (filePath.startsWith("image:")) { // No need to copy
fileUri = filePath;
} else {
file.setFileName(filePath);
if (!file.exists()) {
qWarning() << "[Utils] Avatar not found at " << filePath;
return "";
}
if (QImageReader::imageFormat(filePath).size() == 0) {
qWarning() << "[Utils] Avatar extension not supported by QImageReader for " << filePath;
return "";
}
QFileInfo info(file);
QString uuid = QUuid::createUuid().toString();
fileId = QStringLiteral("%1.%2")
.arg(uuid.mid(1, uuid.length() - 2)) // Remove `{}`.
.arg(info.suffix());
fileUri = QStringLiteral("image://%1/%2").arg(AvatarProvider::ProviderId).arg(fileId);
QString dest = Paths::getAvatarsDirPath() + fileId;
if (!file.copy(dest)) {
qWarning() << "[Utils] Avatar couldn't be created to " << dest;
return "";
}
}
}
return fileUri;
}

View file

@ -53,6 +53,7 @@ public:
const QString &prepareTransfertAddress = "", const QString &prepareTransfertAddress = "",
const QHash<QString, QString> &headers = {}); const QHash<QString, QString> &headers = {});
Q_INVOKABLE static VariantObject *haveAccount(); Q_INVOKABLE static VariantObject *haveAccount();
Q_INVOKABLE static QString createAvatar(const QUrl &fileUrl); // Return the avatar path
static inline QString coreStringToAppString(const std::string &str) { static inline QString coreStringToAppString(const std::string &str) {
if (Constants::LinphoneLocaleEncoding == QString("UTF-8")) return QString::fromStdString(str); if (Constants::LinphoneLocaleEncoding == QString("UTF-8")) return QString::fromStdString(str);

View file

@ -19,8 +19,6 @@
*/ */
#include "core/path/Paths.hpp" #include "core/path/Paths.hpp"
#include "core/utils/Constants.hpp"
// #include "tool/Utils.hpp"
#include "AvatarProvider.hpp" #include "AvatarProvider.hpp"
@ -30,9 +28,7 @@ const QString AvatarProvider::ProviderId = "avatar";
AvatarProvider::AvatarProvider() AvatarProvider::AvatarProvider()
: QQuickImageProvider(QQmlImageProviderBase::Image, QQmlImageProviderBase::ForceAsynchronousImageLoading) { : QQuickImageProvider(QQmlImageProviderBase::Image, QQmlImageProviderBase::ForceAsynchronousImageLoading) {
const auto &str = Paths::getAvatarsDirPath().toStdString(); mAvatarsPath = Paths::getAvatarsDirPath();
if (Constants::LinphoneLocaleEncoding == QString("UTF-8")) mAvatarsPath = QString::fromStdString(str);
else mAvatarsPath = QString::fromLocal8Bit(str.c_str(), int(str.size()));
} }
QImage AvatarProvider::requestImage(const QString &id, QSize *size, const QSize &) { QImage AvatarProvider::requestImage(const QString &id, QSize *size, const QSize &) {

View file

@ -2,11 +2,14 @@
* Qml template used for welcome and login/register pages * Qml template used for welcome and login/register pages
**/ **/
import QtCore
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import QtQuick.Controls 2.2 as Control import QtQuick.Controls 2.2 as Control
import QtQuick.Dialogs
import Linphone import Linphone
import UtilsCpp
Item { Item {
id: mainItem id: mainItem
@ -36,18 +39,35 @@ Item {
placeholderText: qsTr("Rechercher un contact, appeler ou envoyer un message...") placeholderText: qsTr("Rechercher un contact, appeler ou envoyer un message...")
} }
Control.Button { Control.Button {
id: avatarButton
AccountProxy{
id: accountProxy
property bool haveAvatar: defaultAccount && defaultAccount.core.pictureUri || false
}
Layout.preferredWidth: 30 Layout.preferredWidth: 30
Layout.preferredHeight: 30 Layout.preferredHeight: 30
background: Item { background: Item {
visible: false visible: false
} }
contentItem: Image { contentItem: Image {
//avatar id: avatar
source: AppIcons.welcomeLinphoneLogo source: accountProxy.haveAvatar ? accountProxy.defaultAccount.core.pictureUri : AppIcons.welcomeLinphoneLogo
// width: 30
// height: 30
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
} }
onClicked: {
fileDialog.open()
}
FileDialog {
id: fileDialog
currentFolder: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0]
onAccepted: {
var avatarPath = UtilsCpp.createAvatar( selectedFile )
if(avatarPath){
accountProxy.defaultAccount.core.pictureUri = avatarPath
}
}
}
} }
Control.Button { Control.Button {
enabled: false enabled: false

@ -1 +1 @@
Subproject commit 477d4b7d56e256ce6581e6c31d12d3c7f49f9f9a Subproject commit bf9106ee57b8a32aea2693f0fc69c2397a66d570