diff --git a/CHANGELOG.md b/CHANGELOG.md index c4b1c9757..f2166cc0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Added - Features: - * messages features : Reply, forward (to contact, to a SIP address or to a timeline) + * messages features : Reply, forward (to contact, to a SIP address or to a timeline), Vocal record (on send) ## 4.3.2 diff --git a/linphone-app/CMakeLists.txt b/linphone-app/CMakeLists.txt index de7f917f6..34e94ddb3 100644 --- a/linphone-app/CMakeLists.txt +++ b/linphone-app/CMakeLists.txt @@ -181,6 +181,8 @@ set(SOURCES src/components/participant-imdn/ParticipantImdnStateProxyModel.cpp src/components/presence/OwnPresenceModel.cpp src/components/presence/Presence.cpp + src/components/recorder/RecorderManager.cpp + src/components/recorder/RecorderModel.cpp src/components/search/SearchHandler.cpp src/components/search/SearchResultModel.cpp src/components/search/SearchSipAddressesModel.cpp @@ -288,6 +290,8 @@ set(HEADERS src/components/participant-imdn/ParticipantImdnStateProxyModel.cpp src/components/presence/OwnPresenceModel.hpp src/components/presence/Presence.hpp + src/components/recorder/RecorderManager.hpp + src/components/recorder/RecorderModel.hpp src/components/search/SearchHandler.hpp src/components/search/SearchResultModel.hpp src/components/search/SearchSipAddressesModel.hpp diff --git a/linphone-app/assets/images/chat_audio_pause_custom.svg b/linphone-app/assets/images/chat_audio_pause_custom.svg new file mode 100644 index 000000000..e6b1d6996 --- /dev/null +++ b/linphone-app/assets/images/chat_audio_pause_custom.svg @@ -0,0 +1,47 @@ + + + + + + + + + + diff --git a/linphone-app/assets/images/chat_audio_play_custom.svg b/linphone-app/assets/images/chat_audio_play_custom.svg new file mode 100644 index 000000000..34dfe5c4c --- /dev/null +++ b/linphone-app/assets/images/chat_audio_play_custom.svg @@ -0,0 +1,47 @@ + + + + + + + + + + diff --git a/linphone-app/assets/images/chat_audio_soundwave_custom.svg b/linphone-app/assets/images/chat_audio_soundwave_custom.svg new file mode 100644 index 000000000..773208494 --- /dev/null +++ b/linphone-app/assets/images/chat_audio_soundwave_custom.svg @@ -0,0 +1,48 @@ + + + + + + + + + + diff --git a/linphone-app/assets/images/chat_audio_stop_custom.svg b/linphone-app/assets/images/chat_audio_stop_custom.svg new file mode 100644 index 000000000..b6a7a56ee --- /dev/null +++ b/linphone-app/assets/images/chat_audio_stop_custom.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + diff --git a/linphone-app/resources.qrc b/linphone-app/resources.qrc index 3b9736223..3fa40297e 100644 --- a/linphone-app/resources.qrc +++ b/linphone-app/resources.qrc @@ -30,6 +30,10 @@ assets/images/cancel_custom.svg assets/images/chat_custom.svg assets/images/chat_amount.svg + assets/images/chat_audio_pause_custom.svg + assets/images/chat_audio_play_custom.svg + assets/images/chat_audio_soundwave_custom.svg + assets/images/chat_audio_stop_custom.svg assets/images/chat_count.svg assets/images/chat_delivered.svg assets/images/chat_error.svg @@ -193,6 +197,7 @@ ui/modules/Common/Helpers/InvertedMouseArea.qml ui/modules/Common/Image/Icon.qml ui/modules/Common/Image/RoundedImage.qml + ui/modules/Common/Indicators/MediaProgressBar.qml ui/modules/Common/Indicators/VuMeter.qml ui/modules/Common/Menus/ApplicationMenuEntry.qml ui/modules/Common/Menus/ApplicationMenu.qml @@ -238,6 +243,7 @@ ui/modules/Common/Styles/Form/Tab/TabButtonStyle.qml ui/modules/Common/Styles/Form/Tab/TabContainerStyle.qml ui/modules/Common/Styles/Form/TransparentTextInputStyle.qml + ui/modules/Common/Styles/Indicators/MediaProgressBarStyle.qml ui/modules/Common/Styles/Indicators/VuMeterStyle.qml ui/modules/Common/Styles/Menus/ApplicationMenuStyle.qml ui/modules/Common/Styles/Menus/DropDownStaticMenuStyle.qml @@ -270,6 +276,7 @@ ui/modules/Linphone/Chat/Chat.qml ui/modules/Linphone/Chat/ChatDeliveries.qml ui/modules/Linphone/Chat/ChatMenu.qml + ui/modules/Linphone/Chat/ChatAudioPreview.qml ui/modules/Linphone/Chat/ChatMessagePreview.qml ui/modules/Linphone/Chat/ChatForwardMessage.qml ui/modules/Linphone/Chat/ChatReplyMessage.qml @@ -314,6 +321,7 @@ ui/modules/Linphone/Styles/Calls/CallStatisticsStyle.qml ui/modules/Linphone/Styles/Calls/ConferenceControlsStyle.qml ui/modules/Linphone/Styles/Chat/ChatStyle.qml + ui/modules/Linphone/Styles/Chat/ChatAudioPreviewStyle.qml ui/modules/Linphone/Styles/Chat/ChatForwardMessageStyle.qml ui/modules/Linphone/Styles/Chat/ChatReplyMessageStyle.qml ui/modules/Linphone/Styles/Codecs/CodecsViewerStyle.qml diff --git a/linphone-app/src/app/App.cpp b/linphone-app/src/app/App.cpp index 76fd16ac8..eea329ac8 100644 --- a/linphone-app/src/app/App.cpp +++ b/linphone-app/src/app/App.cpp @@ -533,6 +533,14 @@ static inline void registerSharedSingletonType (const char *name) { qmlRegisterSingletonType(Constants::MainQmlUri, 1, 0, name, makeSharedSingleton); } +template +static QObject *makeSharedSingleton (QQmlEngine *, QJSEngine *) { + QObject *object = (CoreManager::getInstance()->*function)(); + QQmlEngine::setObjectOwnership(object, QQmlEngine::CppOwnership); + return object; +} + + template static QObject *makeSharedSingleton (QQmlEngine *, QJSEngine *) { QObject *object = (CoreManager::getInstance()->*function)(); @@ -545,6 +553,11 @@ static inline void registerSharedSingletonType (const char *name) { qmlRegisterSingletonType(Constants::MainQmlUri, 1, 0, name, makeSharedSingleton); } +template +static inline void registerSharedSingletonType (const char *name) { + qmlRegisterSingletonType(Constants::MainQmlUri, 1, 0, name, makeSharedSingleton); +} + template static inline void registerUncreatableType (const char *name) { qmlRegisterUncreatableType(Constants::MainQmlUri, 1, 0, name, QLatin1String("Uncreatable")); @@ -647,6 +660,7 @@ void App::registerTypes () { registerUncreatableType("ContentModel"); registerUncreatableType("HistoryModel"); registerUncreatableType("LdapModel"); + registerUncreatableType("RecorderModel"); registerUncreatableType("SearchResultModel"); registerUncreatableType("SipAddressObserver"); registerUncreatableType("VcardModel"); @@ -680,6 +694,7 @@ void App::registerSharedTypes () { registerSharedSingletonType("ContactsImporterListModel"); registerSharedSingletonType("LdapListModel"); registerSharedSingletonType("TimelineListModel"); + registerSharedSingletonType("RecorderManager"); //qmlRegisterSingletonType(Constants::MainQmlUri, 1, 0, "ColorList", mColorListModel); diff --git a/linphone-app/src/components/Components.hpp b/linphone-app/src/components/Components.hpp index 617412500..009de1b9c 100644 --- a/linphone-app/src/components/Components.hpp +++ b/linphone-app/src/components/Components.hpp @@ -63,6 +63,8 @@ #include "participant-imdn/ParticipantImdnStateListModel.hpp" #include "participant-imdn/ParticipantImdnStateProxyModel.hpp" #include "presence/OwnPresenceModel.hpp" +#include "recorder/RecorderModel.hpp" +#include "recorder/RecorderManager.hpp" #include "settings/AccountSettingsModel.hpp" #include "settings/SettingsModel.hpp" #include "search/SearchResultModel.hpp" diff --git a/linphone-app/src/components/chat-room/ChatRoomModel.cpp b/linphone-app/src/components/chat-room/ChatRoomModel.cpp index 47f922b4f..eb79045ae 100644 --- a/linphone-app/src/components/chat-room/ChatRoomModel.cpp +++ b/linphone-app/src/components/chat-room/ChatRoomModel.cpp @@ -52,6 +52,8 @@ #include "components/participant/ParticipantModel.hpp" #include "components/participant/ParticipantListModel.hpp" #include "components/presence/Presence.hpp" +#include "components/recorder/RecorderManager.hpp" +#include "components/recorder/RecorderModel.hpp" #include "components/timeline/TimelineModel.hpp" #include "components/timeline/TimelineListModel.hpp" #include "components/core/event-count-notifier/AbstractEventCountNotifier.hpp" @@ -600,6 +602,7 @@ void ChatRoomModel::setReply(ChatMessageModel * model){ void ChatRoomModel::clearReply(){ mReply = nullptr; } + //------------------------------------------------------------------------------------------------ void ChatRoomModel::deleteChatRoom(){ @@ -639,13 +642,18 @@ void ChatRoomModel::updateParticipants(const QVariantList& participants){ void ChatRoomModel::sendMessage (const QString &message) { shared_ptr _message; - if(mReply){ + if(mReply) _message = mChatRoom->createReplyMessage(mReply); - _message->addUtf8TextContent(message.toUtf8().toStdString()); - }else{ - _message= mChatRoom->createMessageFromUtf8(message.toUtf8().toStdString()); + else + _message= mChatRoom->createEmptyMessage(); + auto recorder = CoreManager::getInstance()->getRecorderManager(); + if(recorder->haveVocalRecorder()) { + auto content = recorder->getVocalRecorder()->getRecorder()->createContent(); + if(content) + _message->addContent(content); } - + if(!message.isEmpty()) + _message->addUtf8TextContent(message.toUtf8().toStdString()); _message->send(); emit messageSent(_message); } @@ -677,6 +685,13 @@ void ChatRoomModel::sendFileMessage (const QString &path) { shared_ptr message = mChatRoom->createFileTransferMessage(content); message->getContents().front()->setFilePath(Utils::appStringToCoreString(path)); + + auto recorder = CoreManager::getInstance()->getRecorderManager(); + if(recorder->haveVocalRecorder()) { + auto content = recorder->getVocalRecorder()->getRecorder()->createContent(); + if(content) + message->addContent(content); + } message->send(); emit messageSent(message); @@ -686,6 +701,12 @@ void ChatRoomModel::forwardMessage(ChatMessageModel * model){ if(model){ shared_ptr _message; _message = mChatRoom->createForwardMessage(model->getChatMessage()); + auto recorder = CoreManager::getInstance()->getRecorderManager(); + if(recorder->haveVocalRecorder()) { + auto content = recorder->getVocalRecorder()->getRecorder()->createContent(); + if(content) + _message->addContent(content); + } _message->send(); emit messageSent(_message); } diff --git a/linphone-app/src/components/core/CoreManager.cpp b/linphone-app/src/components/core/CoreManager.cpp index 84219f480..31836e278 100644 --- a/linphone-app/src/components/core/CoreManager.cpp +++ b/linphone-app/src/components/core/CoreManager.cpp @@ -36,6 +36,7 @@ #include "components/contacts/ContactsImporterListModel.hpp" #include "components/history/HistoryModel.hpp" #include "components/ldap/LdapListModel.hpp" +#include "components/recorder/RecorderManager.hpp" #include "components/settings/AccountSettingsModel.hpp" #include "components/settings/SettingsModel.hpp" #include "components/sip-addresses/SipAddressesModel.hpp" @@ -123,6 +124,14 @@ HistoryModel* CoreManager::getHistoryModel(){ } return mHistoryModel; } + +RecorderManager* CoreManager::getRecorderManager(){ + if(!mRecorderManager){ + mRecorderManager = new RecorderManager(this); + emit recorderManagerCreated(mRecorderManager); + } + return mRecorderManager; +} // ----------------------------------------------------------------------------- void CoreManager::init (QObject *parent, const QString &configPath) { @@ -183,6 +192,7 @@ void CoreManager::cleanLogs () const { mCore->resetLogCollection(); } +// ----------------------------------------------------------------------------- // ----------------------------------------------------------------------------- diff --git a/linphone-app/src/components/core/CoreManager.hpp b/linphone-app/src/components/core/CoreManager.hpp index df73546ae..264ac3c94 100644 --- a/linphone-app/src/components/core/CoreManager.hpp +++ b/linphone-app/src/components/core/CoreManager.hpp @@ -42,6 +42,7 @@ class CoreHandlers; class EventCountNotifier; class HistoryModel; class LdapListModel; +class RecorderManager; class SettingsModel; class SipAddressesModel; class VcardModel; @@ -76,6 +77,7 @@ public: //bool chatRoomModelExists (std::shared_ptr chatRoom); HistoryModel* getHistoryModel(); + RecorderManager* getRecorderManager(); // --------------------------------------------------------------------------- // Video render lock. @@ -179,6 +181,7 @@ signals: void chatRoomModelCreated (const std::shared_ptr &chatRoomModel); void historyModelCreated (HistoryModel *historyModel); + void recorderManagerCreated(RecorderManager *recorderModel); void logsUploaded (const QString &url); @@ -227,6 +230,7 @@ private: //QList, std::weak_ptr>> mChatRoomModels; HistoryModel * mHistoryModel = nullptr; LdapListModel *mLdapListModel = nullptr; + RecorderManager* mRecorderManager = nullptr; QTimer *mCbsTimer = nullptr; diff --git a/linphone-app/src/components/recorder/RecorderManager.cpp b/linphone-app/src/components/recorder/RecorderManager.cpp new file mode 100644 index 000000000..dc0bf9670 --- /dev/null +++ b/linphone-app/src/components/recorder/RecorderManager.cpp @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2021 Belledonne Communications SARL. + * + * This file is part of linphone-desktop + * (see https://www.linphone.org). + * + * 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 . + */ + +#include +#include + +#include "app/App.hpp" +#include "components/core/CoreManager.hpp" + +#include "RecorderManager.hpp" +#include "RecorderModel.hpp" + +// ============================================================================= + +RecorderManager::RecorderManager (QObject * parent) : QObject(parent) { + App::getInstance()->getEngine()->setObjectOwnership(this, QQmlEngine::CppOwnership);// Avoid QML to destroy it when passing by Q_INVOKABLE +} + +RecorderManager::~RecorderManager(){ +} + + +bool RecorderManager::haveVocalRecorder() const{ + return mVocalRecorder != nullptr; +} + +RecorderModel* RecorderManager::getVocalRecorder(){ + if( !mVocalRecorder) { + auto core = CoreManager::getInstance()->getCore(); + std::shared_ptr params = core->createRecorderParams(); + params->setFileFormat(linphone::RecorderFileFormat::Mkv); + params->setVideoCodec(""); + auto recorder = core->createRecorder(params); + if(recorder) + mVocalRecorder = RecorderModel::create(recorder, this); + emit haveVocalRecorderChanged(); + } + return mVocalRecorder.get(); +} + +RecorderModel* RecorderManager::resetVocalRecorder(){ + if(mVocalRecorder) + clearVocalRecorder(); + return getVocalRecorder(); +} + +void RecorderManager::clearVocalRecorder(){ + if( mVocalRecorder){ + mVocalRecorder = nullptr; + emit haveVocalRecorderChanged(); + } +} + +//-------------------------------------------------------------------------------------------------------------------------- diff --git a/linphone-app/src/components/recorder/RecorderManager.hpp b/linphone-app/src/components/recorder/RecorderManager.hpp new file mode 100644 index 000000000..45e5adb19 --- /dev/null +++ b/linphone-app/src/components/recorder/RecorderManager.hpp @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2021 Belledonne Communications SARL. + * + * This file is part of linphone-desktop + * (see https://www.linphone.org). + * + * 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 . + */ + +#ifndef RECORDER_MANAGER_MODEL_H +#define RECORDER_MANAGER_MODEL_H + + +#include +#include + +// ============================================================================= +class RecorderModel; + +class RecorderManager : public QObject { + Q_OBJECT +public: + RecorderManager (QObject * parent = nullptr); + virtual ~RecorderManager(); + + Q_PROPERTY(bool haveVocalRecorder READ haveVocalRecorder NOTIFY haveVocalRecorderChanged) + + bool haveVocalRecorder() const; + Q_INVOKABLE RecorderModel* getVocalRecorder(); + Q_INVOKABLE RecorderModel* resetVocalRecorder(); + Q_INVOKABLE void clearVocalRecorder(); + +signals: + void haveVocalRecorderChanged(); + +private: + std::shared_ptr mVocalRecorder; +}; +#endif diff --git a/linphone-app/src/components/recorder/RecorderModel.cpp b/linphone-app/src/components/recorder/RecorderModel.cpp new file mode 100644 index 000000000..30194e6f2 --- /dev/null +++ b/linphone-app/src/components/recorder/RecorderModel.cpp @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2021 Belledonne Communications SARL. + * + * This file is part of linphone-desktop + * (see https://www.linphone.org). + * + * 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 . + */ + +#include +#include + +#include "app/App.hpp" +#include "app/paths/Paths.hpp" +#include "components/core/CoreManager.hpp" +#include "components/settings/SettingsModel.hpp" +#include "utils/Utils.hpp" + +#include "RecorderModel.hpp" + +// ============================================================================= + +RecorderModel::RecorderModel ( std::shared_ptr recorder, QObject * parent) : QObject(parent) { + App::getInstance()->getEngine()->setObjectOwnership(this, QQmlEngine::CppOwnership);// Avoid QML to destroy it when passing by Q_INVOKABLE + mRecorder= recorder; +} + +RecorderModel::~RecorderModel(){ +} + +std::shared_ptr RecorderModel::create(std::shared_ptr recorder, QObject * parent){ + return std::make_shared(recorder, parent); +} + + +std::shared_ptr RecorderModel::getRecorder(){ + return mRecorder; +} + +int RecorderModel::getDuration()const{ + return mRecorder->getDuration(); +} + +LinphoneEnums::RecorderState RecorderModel::getState() const{ + return LinphoneEnums::fromLinphone(mRecorder->getState()); +} + +QString RecorderModel::getFile()const{ + return Utils::coreStringToAppString(mRecorder->getFile()); +} + +void RecorderModel::start(){ + bool soFarSoGood; + QString filename = QStringLiteral("vocal_%1.mkv") + .arg(QDateTime::currentDateTime().toString("yyyy-MM-dd_hh-mm-ss-zzz")); + const QString safeFilePath = Utils::getSafeFilePath( + QStringLiteral("%1%2") + .arg(Utils::coreStringToAppString(Paths::getCapturesDirPath())) + .arg(filename), + &soFarSoGood + ); + + if (!soFarSoGood) { + qWarning() << QStringLiteral("Unable to create safe file path for: %1.").arg(filename); + }else if(mRecorder->open(Utils::appStringToCoreString(safeFilePath)) < 0) + qWarning() << QStringLiteral("Unable to open safe file path for: %1.").arg(filename); + else if( mRecorder->start() < 0) + qWarning() << QStringLiteral("Unable to start recording to : %1.").arg(filename); + emit stateChanged(); + emit fileChanged(); +} + +void RecorderModel::pause(){ + qWarning() << mRecorder->pause(); + emit stateChanged(); +} + +void RecorderModel::stop(){ + if(mRecorder->pause() == 0) + mRecorder->close(); + emit stateChanged(); +} + +//-------------------------------------------------------------------------------------------------------------------------- diff --git a/linphone-app/src/components/recorder/RecorderModel.hpp b/linphone-app/src/components/recorder/RecorderModel.hpp new file mode 100644 index 000000000..6ea1269fc --- /dev/null +++ b/linphone-app/src/components/recorder/RecorderModel.hpp @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2021 Belledonne Communications SARL. + * + * This file is part of linphone-desktop + * (see https://www.linphone.org). + * + * 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 . + */ + +#ifndef RECORDER_MODEL_H +#define RECORDER_MODEL_H + +#include "utils/LinphoneEnums.hpp" + +// ============================================================================= + + +class RecorderModel : public QObject { + Q_OBJECT + +public: + static std::shared_ptr create(std::shared_ptr recorder,QObject * parent = nullptr);// Call it instead constructor + RecorderModel (std::shared_ptr recorder,QObject * parent = nullptr); + virtual ~RecorderModel(); + + Q_PROPERTY(LinphoneEnums::RecorderState state READ getState NOTIFY stateChanged) + Q_PROPERTY(QString file READ getFile NOTIFY fileChanged) + + std::shared_ptr getRecorder(); + + Q_INVOKABLE int getDuration()const; + LinphoneEnums::RecorderState getState() const; + Q_INVOKABLE QString getFile()const; + + Q_INVOKABLE void start(); + Q_INVOKABLE void pause(); + Q_INVOKABLE void stop(); + +signals: + void stateChanged(); + void fileChanged(); + +private: + std::shared_ptr mRecorder; +}; +Q_DECLARE_METATYPE(std::shared_ptr) +Q_DECLARE_METATYPE(RecorderModel*) +#endif diff --git a/linphone-app/src/components/sound-player/SoundPlayer.hpp b/linphone-app/src/components/sound-player/SoundPlayer.hpp index b7d4aa265..0dc18309c 100644 --- a/linphone-app/src/components/sound-player/SoundPlayer.hpp +++ b/linphone-app/src/components/sound-player/SoundPlayer.hpp @@ -37,11 +37,11 @@ namespace linphone { class SoundPlayer : public QObject { class Handlers; - Q_OBJECT; + Q_OBJECT - Q_PROPERTY(QString source READ getSource WRITE setSource NOTIFY sourceChanged); - Q_PROPERTY(PlaybackState playbackState READ getPlaybackState WRITE setPlaybackState NOTIFY playbackStateChanged); - Q_PROPERTY(int duration READ getDuration NOTIFY sourceChanged); + Q_PROPERTY(QString source READ getSource WRITE setSource NOTIFY sourceChanged) + Q_PROPERTY(PlaybackState playbackState READ getPlaybackState WRITE setPlaybackState NOTIFY playbackStateChanged) + Q_PROPERTY(int duration READ getDuration NOTIFY sourceChanged) public: enum PlaybackState { diff --git a/linphone-app/src/utils/LinphoneEnums.cpp b/linphone-app/src/utils/LinphoneEnums.cpp index b5ee6f0b5..ae8d367b3 100644 --- a/linphone-app/src/utils/LinphoneEnums.cpp +++ b/linphone-app/src/utils/LinphoneEnums.cpp @@ -29,9 +29,11 @@ void LinphoneEnums::registerMetaTypes(){ qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); } - linphone::MediaEncryption LinphoneEnums::toLinphone(const LinphoneEnums::MediaEncryption& data){ return static_cast(data); } @@ -73,3 +75,10 @@ linphone::Tunnel::Mode LinphoneEnums::toLinphone(const LinphoneEnums::TunnelMode LinphoneEnums::TunnelMode LinphoneEnums::fromLinphone(const linphone::Tunnel::Mode& data){ return static_cast(data); } + +linphone::RecorderState LinphoneEnums::toLinphone(const LinphoneEnums::RecorderState& data){ + return static_cast(data); +} +LinphoneEnums::RecorderState LinphoneEnums::fromLinphone(const linphone::RecorderState& data){ + return static_cast(data); +} \ No newline at end of file diff --git a/linphone-app/src/utils/LinphoneEnums.hpp b/linphone-app/src/utils/LinphoneEnums.hpp index 95ee581c3..8da35c46d 100644 --- a/linphone-app/src/utils/LinphoneEnums.hpp +++ b/linphone-app/src/utils/LinphoneEnums.hpp @@ -121,6 +121,16 @@ Q_ENUM_NS(TunnelMode) linphone::Tunnel::Mode toLinphone(const LinphoneEnums::TunnelMode& mode); LinphoneEnums::TunnelMode fromLinphone(const linphone::Tunnel::Mode& mode); + +enum RecorderState{ + RecorderStateClosed = int(linphone::RecorderState::Closed), + RecorderStatePaused = int(linphone::RecorderState::Paused), + RecorderStateRunning = int(linphone::RecorderState::Running) +}; +Q_ENUM_NS(RecorderState) + +linphone::RecorderState toLinphone(const LinphoneEnums::RecorderState& state); +LinphoneEnums::RecorderState fromLinphone(const linphone::RecorderState& state); } Q_DECLARE_METATYPE(LinphoneEnums::MediaEncryption) @@ -129,5 +139,6 @@ Q_DECLARE_METATYPE(LinphoneEnums::EventLogType) Q_DECLARE_METATYPE(LinphoneEnums::ChatMessageState) Q_DECLARE_METATYPE(LinphoneEnums::CallStatus) Q_DECLARE_METATYPE(LinphoneEnums::TunnelMode) +Q_DECLARE_METATYPE(LinphoneEnums::RecorderState) #endif diff --git a/linphone-app/ui/modules/Common/Form/ActionButton.qml b/linphone-app/ui/modules/Common/Form/ActionButton.qml index 8a1b0bef3..f2c09444e 100644 --- a/linphone-app/ui/modules/Common/Form/ActionButton.qml +++ b/linphone-app/ui/modules/Common/Form/ActionButton.qml @@ -1,5 +1,6 @@ import QtQuick 2.7 import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 import QtGraphicalEffects 1.12 import Common 1.0 @@ -33,6 +34,8 @@ Item { property bool useStates: true //property bool autoIcon : false // hovered/pressed : use an automatic layer instead of specific icon image property int iconSize : colorSet.iconSize + property int iconHeight: colorSet.iconHeight ? colorSet.iconHeight : 0 + property int iconWidth: colorSet.iconWidth ? colorSet.iconWidth : 0 readonly property alias hovered: button.hovered property alias text: button.text // Tooltip aliases @@ -44,24 +47,34 @@ Item { property alias backgroundRadius : backgroundColor.radius + property alias horizontalAlignment: icon.horizontalAlignment + property alias verticalAlignment: icon.verticalAlignment + property alias fillMode: icon.fillMode -// AutoColor for hide part alpha /4 - property color foregroundHiddenPartNormalColor : colorSet.foregroundNormalColor ? Qt.rgba(colorSet.foregroundNormalColor.r, colorSet.foregroundNormalColor.g, colorSet.foregroundNormalColor.b, colorSet.foregroundNormalColor.a/4) : 'transparent' - property color foregroundHiddenPartDisabledColor : colorSet.foregroundDisabledColor ? Qt.rgba(colorSet.foregroundDisabledColor.r, colorSet.foregroundDisabledColor.g, colorSet.foregroundDisabledColor.b, colorSet.foregroundDisabledColor.a/4): 'transparent' - property color foregroundHiddenPartHoveredColor : colorSet.foregroundHoveredColor ? Qt.rgba(colorSet.foregroundHoveredColor.r, colorSet.foregroundHoveredColor.g, colorSet.foregroundHoveredColor.b, colorSet.foregroundHoveredColor.a/4): 'transparent' - property color foregroundHiddenPartUpdatingColor : colorSet.foregroundUpdatingColor ? Qt.rgba(colorSet.foregroundUpdatingColor.r, colorSet.foregroundUpdatingColor.g, colorSet.foregroundUpdatingColor.b, colorSet.foregroundUpdatingColor.a/4): 'transparent' - property color foregroundHiddenPartPressedColor : colorSet.foregroundPressedColor ? Qt.rgba(colorSet.foregroundPressedColor.r, colorSet.foregroundPressedColor.g, colorSet.foregroundPressedColor.b, colorSet.foregroundPressedColor.a/4): 'transparent' +// Hidden part : transparent if not specified + property color backgroundHiddenPartNormalColor : colorSet.backgroundHiddenPartNormalColor ? colorSet.backgroundHiddenPartNormalColor : (colorSet.backgroundNormalColor ? colorSet.backgroundNormalColor : 'transparent') + property color backgroundHiddenPartDisabledColor : colorSet.backgroundHiddenPartDisabledColor ? colorSet.backgroundHiddenPartDisabledColor : (colorSet.backgroundDisabledColor ? colorSet.backgroundDisabledColor : 'transparent') + property color backgroundHiddenPartHoveredColor : colorSet.backgroundHiddenPartHoveredColor ? colorSet.backgroundHiddenPartHoveredColor : (colorSet.backgroundHoveredColor ? colorSet.backgroundHoveredColor : 'transparent') + property color backgroundHiddenPartUpdatingColor : colorSet.backgroundHiddenPartUpdatingColor ? colorSet.backgroundHiddenPartUpdatingColor : (colorSet.backgroundUpdatingColor ? colorSet.backgroundUpdatingColor : 'transparent') + property color backgroundHiddenPartPressedColor : colorSet.backgroundHiddenPartPressedColor ? colorSet.backgroundHiddenPartPressedColor : (colorSet.backgroundPressedColor ? colorSet.backgroundPressedColor : 'transparent') + +// AutoColor : alpha /4 for foreground + property color foregroundHiddenPartNormalColor : colorSet.foregroundHiddenPartNormalColor ? colorSet.foregroundHiddenPartNormalColor : (colorSet.foregroundNormalColor ? Qt.rgba(colorSet.foregroundNormalColor.r, colorSet.foregroundNormalColor.g, colorSet.foregroundNormalColor.b, colorSet.foregroundNormalColor.a/4) : 'transparent') + property color foregroundHiddenPartDisabledColor : colorSet.foregroundHiddenPartDisabledColor ? colorSet.foregroundHiddenPartDisabledColor : (colorSet.foregroundDisabledColor ? Qt.rgba(colorSet.foregroundDisabledColor.r, colorSet.foregroundDisabledColor.g, colorSet.foregroundDisabledColor.b, colorSet.foregroundDisabledColor.a/4): 'transparent') + property color foregroundHiddenPartHoveredColor : colorSet.foregroundHiddenPartHoveredColor ? colorSet.foregroundHiddenPartHoveredColor : (colorSet.foregroundHoveredColor ? Qt.rgba(colorSet.foregroundHoveredColor.r, colorSet.foregroundHoveredColor.g, colorSet.foregroundHoveredColor.b, colorSet.foregroundHoveredColor.a/4): 'transparent') + property color foregroundHiddenPartUpdatingColor : colorSet.foregroundHiddenPartUpdatingColor ? colorSet.foregroundHiddenPartUpdatingColor : (colorSet.foregroundUpdatingColor ? Qt.rgba(colorSet.foregroundUpdatingColor.r, colorSet.foregroundUpdatingColor.g, colorSet.foregroundUpdatingColor.b, colorSet.foregroundUpdatingColor.a/4): 'transparent') + property color foregroundHiddenPartPressedColor : colorSet.foregroundHiddenPartPressedColor ? colorSet.foregroundHiddenPartPressedColor : (colorSet.foregroundPressedColor ? Qt.rgba(colorSet.foregroundPressedColor.r, colorSet.foregroundPressedColor.g, colorSet.foregroundPressedColor.b, colorSet.foregroundPressedColor.a/4): 'transparent') +//--------------------------------------------- property int percentageDisplayed : 100 - // If `useStates` = true, the used icons are: // `icon`_pressed, `icon`_hovered and `icon`_normal. property string icon : colorSet.icon // --------------------------------------------------------------------------- - signal clicked + signal clicked(real x, real y) // --------------------------------------------------------------------------- @@ -120,6 +133,21 @@ Item { }else return "black" } + function getBackgroundHiddenPartColor(){ + if(isCustom){ + if(wrappedButton.icon == '') + return wrappedButton.backgroundHiddenPartNormalColor + if (wrappedButton.updating) + return wrappedButton.backgroundHiddenPartUpdatingColor + if (!useStates) + return wrappedButton.backgroundHiddenPartNormalColor + if (!wrappedButton.enabled) + return wrappedButton.backgroundHiddenPartDisabledColor + return button.down ? wrappedButton.backgroundHiddenPartPressedColor + : (button.hovered ? wrappedButton.backgroundHiddenPartHoveredColor: wrappedButton.backgroundHiddenPartNormalColor) + }else + return 'transparent' + } function getForegroundHiddenPartColor(){ if(isCustom){ if(wrappedButton.icon == '') @@ -137,20 +165,31 @@ Item { } // --------------------------------------------------------------------------- - height: iconSize || parent.iconSize || parent.height - width: iconSize || parent.iconSize || parent.width + height: iconHeight || iconSize || parent.iconSize || parent.height + width: iconWidth || iconSize || parent.iconSize || parent.width Button { id: button anchors.fill: parent - background: Rectangle { - id: backgroundColor - color: getBackgroundColor() - } + background: Row{ + anchors.fill: parent + Rectangle { + height: parent.height + width:parent.width * wrappedButton.percentageDisplayed / 100 + id: backgroundColor + color: getBackgroundColor() + } + Rectangle { + height: parent.height + width: parent.width * ( 1 - wrappedButton.percentageDisplayed / 100 ) + id: backgroundHiddenPartColor + color: getBackgroundHiddenPartColor() + } + } hoverEnabled: !wrappedButton.updating//|| wrappedButton.autoIcon - onClicked: !wrappedButton.updating && wrappedButton.enabled && wrappedButton.clicked() + onClicked: !wrappedButton.updating && wrappedButton.enabled && wrappedButton.clicked(pressX, pressY) Rectangle{ id: foregroundColor anchors.fill:parent @@ -179,6 +218,7 @@ Item { id: icon anchors.centerIn: parent + anchors.fill: iconHeight>0 || iconWidth ? parent : undefined icon: { if(!Images[_getIcon()]) console.log("No images for: "+_getIcon()) @@ -187,6 +227,8 @@ Item { iconSize: wrappedButton.iconSize || ( parent.width > parent.height ? parent.height : parent.width ) + iconHeight: wrappedButton.iconHeight + iconWidth: wrappedButton.iconWidth visible: !isCustom } diff --git a/linphone-app/ui/modules/Common/Form/DroppableTextArea.qml b/linphone-app/ui/modules/Common/Form/DroppableTextArea.qml index fb0d13a81..5837a211f 100644 --- a/linphone-app/ui/modules/Common/Form/DroppableTextArea.qml +++ b/linphone-app/ui/modules/Common/Form/DroppableTextArea.qml @@ -4,6 +4,7 @@ import QtQuick.Dialogs 1.2 import QtQuick.Layouts 1.12 import Common 1.0 +import Linphone 1.0 import Common.Styles 1.0 import Utils 1.0 @@ -30,6 +31,7 @@ Item { signal dropped (var files) signal validText (string text) + signal audioRecordRequest() // --------------------------------------------------------------------------- @@ -86,7 +88,7 @@ Item { } // Record audio ActionButton { - visible:false && droppableTextArea.enabled// TODO + visible:droppableTextArea.enabled id: recordAudioButton //anchors.verticalCenter: parent.verticalCenter @@ -97,9 +99,7 @@ Item { backgroundRadius: 8 colorSet: DroppableTextAreaStyle.chatMicro - useStates:false - - onClicked: {console.log('Record audio request')} + onClicked: droppableTextArea.audioRecordRequest() } @@ -137,7 +137,7 @@ Item { } } function handleValidation () { - if (text.length !== 0) { + if (RecorderManager.haveVocalRecorder || text.length !== 0) { validText(text) } } diff --git a/linphone-app/ui/modules/Common/Image/Icon.qml b/linphone-app/ui/modules/Common/Image/Icon.qml index 4890625bd..71d947bfb 100644 --- a/linphone-app/ui/modules/Common/Image/Icon.qml +++ b/linphone-app/ui/modules/Common/Image/Icon.qml @@ -12,40 +12,40 @@ import Utils 1.0 Item { id: mainItem property var iconSize // Required. + property int iconHeight: 0 // Or this + property int iconWidth: 0 // <-- too + property string icon property color overwriteColor + property alias horizontalAlignment: image.horizontalAlignment + property alias verticalAlignment: image.verticalAlignment + property alias fillMode: image.fillMode + + // Use this slot because of testing overwriteColor in layer doesn't seem to work onOverwriteColorChanged: if(overwriteColor) image.colorOverwriteEnabled = true else image.colorOverwriteEnabled = false - height: iconSize - width: iconSize + height: iconHeight > 0 ? iconHeight : iconSize + width: iconWidth > 0 ? iconWidth : iconSize Image { id:image property bool colorOverwriteEnabled : false mipmap: SettingsModel.mipmapEnabled - cache: Images.areReadOnlyImages - function getIconSize () { - Utils.assert( - (icon == null || icon == '' || iconSize != null && iconSize >= 0), - '`iconSize` must be defined and must be positive. (icon=`' + - icon + '`, iconSize=' + iconSize + ')' - ) - - return iconSize - } + cache: Images.areReadOnlyImages - anchors.centerIn: parent + //anchors.centerIn: parent + anchors.fill: parent - width: iconSize - height: iconSize + //width: iconWidth > 0 ? iconWidth : mainItem.width + //height: iconHeight > 0 ? iconHeight : mainItem.height fillMode: Image.PreserveAspectFit source: Utils.resolveImageUri(icon) - sourceSize.width: getIconSize() - sourceSize.height: getIconSize() + sourceSize.width: iconWidth > 0 ? iconWidth : iconSize + sourceSize.height: iconHeight > 0 ? iconHeight : iconSize layer { enabled: image.colorOverwriteEnabled effect: ColorOverlay { diff --git a/linphone-app/ui/modules/Common/Indicators/MediaProgressBar.qml b/linphone-app/ui/modules/Common/Indicators/MediaProgressBar.qml new file mode 100644 index 000000000..a7553aebe --- /dev/null +++ b/linphone-app/ui/modules/Common/Indicators/MediaProgressBar.qml @@ -0,0 +1,107 @@ +import QtQuick 2.7 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 +import QtGraphicalEffects 1.12 + +import Common 1.0 +import Linphone 1.0 +import Utils 1.0 +import Units 1.0 +import Common.Styles 1.0 + +// ============================================================================= + +ProgressBar { + id: progressBar + + property bool stopAtEnd: true + property bool resetAtEnd: false + property int progressDuration // Max duration + property int progressPosition // Position of pregress bar in [0 ; progressDuration] + property alias colorSet: progression.colorSet + + function start(){ + progressBar.value = 0 + animationTest.start() + } + function resume(){ + if(progressBar.value >= 100) + progressBar.value = 0 + animationTest.start() + } + function stop(){ + animationTest.stop() + } + signal endReached() + signal refreshPositionRequested() + signal seekRequested(int ms) + Timer{ + id: animationTest + repeat: true + onTriggered: progressBar.refreshPositionRequested() + interval: 5 + } + to: 101 + value: 0 + onValueChanged:{ + if(value > 100){ + if( progressBar.stopAtEnd) + stop() + if(progressBar.resetAtEnd) { + progressBar.value = 0 + progressPosition = 0 + }else{ + progressBar.value = 100// Stay at 100 + progressPosition = progressDuration + } + + progressBar.endReached() + }else + progression.percentageDisplayed = value + } + + anchors.topMargin: 5 + anchors.bottomMargin: 5 + + background: Rectangle { + color: MediaProgressBarStyle.backgroundColor + radius: 5 + } + + + contentItem: + Rectangle{ + anchors.fill: parent + radius: 5 + RowLayout{ + anchors.fill: parent + + ActionButton{ + id: progression + Layout.fillWidth: true + Layout.fillHeight: true + Layout.topMargin: 5 + Layout.bottomMargin: 5 + Layout.leftMargin: 10 + backgroundRadius: 5 + fillMode: Image.TileHorizontally + verticalAlignment: Image.AlignLeft + isCustom: true + colorSet: MediaProgressBarStyle.progressionWave + percentageDisplayed: 0 + onClicked: progressBar.seekRequested(x * progressBar.progressDuration/width) + } + Text{ + Layout.fillHeight: true + Layout.preferredWidth: 100 + Layout.rightMargin: 10 + horizontalAlignment: Qt.AlignRight + verticalAlignment: Qt.AlignVCenter + text: progressBar.progressPosition >= 0 ? Utils.formatElapsedTime( progressBar.progressPosition / 1000 ) : '-' + property font customFont : SettingsModel.textMessageFont + font.family: customFont.family + font.pointSize: Units.dp * (customFont.pointSize + 2) + } + } + } +} diff --git a/linphone-app/ui/modules/Common/Styles/Indicators/MediaProgressBarStyle.qml b/linphone-app/ui/modules/Common/Styles/Indicators/MediaProgressBarStyle.qml new file mode 100644 index 000000000..6ebbb6af1 --- /dev/null +++ b/linphone-app/ui/modules/Common/Styles/Indicators/MediaProgressBarStyle.qml @@ -0,0 +1,36 @@ +pragma Singleton +import QtQml 2.2 + +import ColorsList 1.0 + +// ============================================================================= + +QtObject { + property string sectionName: 'MediaProgressBar' + + property color backgroundColor: ColorsList.add(sectionName+'_bg', 'k').color + property string gaugeIcon: 'chat_audio_soundwave_custom' + + property QtObject progressionWave: QtObject{ + property int iconSize: 30 + property int iconHeight: 40 + property int iconWidth: 250 + property string name : 'progression_soundwave' + property string icon : 'chat_audio_soundwave_custom' + property color backgroundNormalColor : ColorsList.addImageColor(sectionName+'_'+name+'_bg_n', icon, 'a_n_b_bg').color + property color backgroundHoveredColor : ColorsList.addImageColor(sectionName+'_'+name+'_bg_h', icon, 'a_h_b_bg').color + property color backgroundPressedColor : ColorsList.addImageColor(sectionName+'_'+name+'_bg_p', icon, 'a_p_b_bg').color + property color foregroundNormalColor : ColorsList.addImageColor(sectionName+'_'+name+'_fg_n', icon, 'a_n_b_fg').color + property color foregroundHoveredColor : ColorsList.addImageColor(sectionName+'_'+name+'_fg_h', icon, 'a_h_b_fg').color + property color foregroundPressedColor : ColorsList.addImageColor(sectionName+'_'+name+'_fg_p', icon, 'a_p_b_fg').color + + property color backgroundHiddenPartNormalColor : ColorsList.addImageColor(sectionName+'_'+name+'_hidden_bg_n', icon, 'l_n_b_bg').color + property color backgroundHiddenPartHoveredColor : ColorsList.addImageColor(sectionName+'_'+name+'_hidden_bg_h', icon, 'l_h_b_bg').color + property color backgroundHiddenPartPressedColor : ColorsList.addImageColor(sectionName+'_'+name+'_hidden_bg_p', icon, 'l_p_b_bg').color + + property color foregroundHiddenPartNormalColor : ColorsList.addImageColor(sectionName+'_'+name+'_hidden_fg_n', icon, 'l_n_b_fg').color + property color foregroundHiddenPartHoveredColor : ColorsList.addImageColor(sectionName+'_'+name+'_hidden_fg_h', icon, 'l_h_b_fg').color + property color foregroundHiddenPartPressedColor : ColorsList.addImageColor(sectionName+'_'+name+'_hidden_fg_p', icon, 'l_p_b_fg').color + } + +} diff --git a/linphone-app/ui/modules/Common/Styles/qmldir b/linphone-app/ui/modules/Common/Styles/qmldir index c9480c1c9..db6a3eb2e 100644 --- a/linphone-app/ui/modules/Common/Styles/qmldir +++ b/linphone-app/ui/modules/Common/Styles/qmldir @@ -41,6 +41,7 @@ singleton FormVGroupStyle 1.0 Form/Placements/FormVGroupStyle.qml singleton TabButtonStyle 1.0 Form/Tab/TabButtonStyle.qml singleton TabContainerStyle 1.0 Form/Tab/TabContainerStyle.qml +singleton MediaProgressBarStyle 1.0 Indicators/MediaProgressBarStyle.qml singleton VuMeterStyle 1.0 Indicators/VuMeterStyle.qml singleton ApplicationMenuStyle 1.0 Menus/ApplicationMenuStyle.qml diff --git a/linphone-app/ui/modules/Common/qmldir b/linphone-app/ui/modules/Common/qmldir index ccca1aefd..da4f8190b 100644 --- a/linphone-app/ui/modules/Common/qmldir +++ b/linphone-app/ui/modules/Common/qmldir @@ -65,6 +65,7 @@ InvertedMouseArea 1.0 Helpers/InvertedMouseArea.qml Icon 1.0 Image/Icon.qml RoundedImage 1.0 Image/RoundedImage.qml +MediaProgressBar 1.0 Indicators/MediaProgressBar.qml VuMeter 1.0 Indicators/VuMeter.qml ApplicationMenu 1.0 Menus/ApplicationMenu.qml diff --git a/linphone-app/ui/modules/Linphone/Chat/Chat.qml b/linphone-app/ui/modules/Linphone/Chat/Chat.qml index 11a3022e2..afaa5c112 100644 --- a/linphone-app/ui/modules/Linphone/Chat/Chat.qml +++ b/linphone-app/ui/modules/Linphone/Chat/Chat.qml @@ -426,6 +426,7 @@ Rectangle { chatMessagePreview.hide() } } + onAudioRecordRequest: RecorderManager.resetVocalRecorder() Component.onCompleted: {text = proxyModel.cachedText; cursorPosition=text.length} Rectangle{ anchors.fill:parent diff --git a/linphone-app/ui/modules/Linphone/Chat/ChatAudioPreview.qml b/linphone-app/ui/modules/Linphone/Chat/ChatAudioPreview.qml new file mode 100644 index 000000000..5e1b4cb6a --- /dev/null +++ b/linphone-app/ui/modules/Linphone/Chat/ChatAudioPreview.qml @@ -0,0 +1,155 @@ +import QtQuick 2.7 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 + +import Common 1.0 +import Linphone 1.0 +import Linphone.Styles 1.0 +import Utils 1.0 +import UtilsCpp 1.0 +import LinphoneEnums 1.0 + +import Units 1.0 + +import 'Chat.js' as Logic + +// ============================================================================= + +Rectangle{ + id: audioPreviewBlock + property bool haveRecorder: RecorderManager.haveVocalRecorder + property RecorderModel vocalRecorder : (haveRecorder ? RecorderManager.getVocalRecorder() : null) + property bool isRecording : (vocalRecorder ? vocalRecorder.state != LinphoneEnums.RecorderStateClosed : false) + property bool isPlaying : vocalPlayer.item && vocalPlayer.item.playbackState === SoundPlayer.PlayingState + + onVocalRecorderChanged: if(haveRecorder) + audioPreviewBlock.state = 'showed' + onIsRecordingChanged: if(isRecording) { + mediaProgressBar.start() + }else + mediaProgressBar.stop() + onIsPlayingChanged: isPlaying ? mediaProgressBar.resume() : mediaProgressBar.stop() + + Layout.preferredHeight: 70 + + color: ChatAudioPreviewStyle.backgroundColor + radius: 0 + state: "hidden" + visible: haveRecorder + onVisibleChanged: if(!visible) hide() + clip: false + function hide(){ + state = 'hidden' + } + Loader { + id: vocalPlayer + + active: false + sourceComponent: SoundPlayer { + source: (haveRecorder && vocalRecorder? vocalRecorder.file : '') + onStopped:{ + mediaProgressBar.value = 100 + } + Component.onCompleted: { + play()// This will open the file and allow seeking + pause() + } + } + } + RowLayout{ + id: lineLayout + anchors.fill: parent + spacing: 10 + ActionButton{ + Layout.preferredHeight: iconSize + Layout.preferredWidth: iconSize + Layout.leftMargin: 10 + Layout.alignment: Qt.AlignVCenter + isCustom: true + colorSet: ChatAudioPreviewStyle.deleteAction + onClicked: audioPreviewBlock.hide() + } + Item{ + Layout.fillHeight: true + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + Layout.topMargin: 5 + Layout.bottomMargin: 5 + MediaProgressBar{ + id: mediaProgressBar + anchors.fill: parent + progressDuration: 0 + progressPosition: 0 + stopAtEnd: !audioPreviewBlock.isRecording + resetAtEnd: false + colorSet: isRecording ? ChatAudioPreviewStyle.recordingProgressionWave : ChatAudioPreviewStyle.progressionWave + onEndReached:{ + if(vocalPlayer.item) + vocalPlayer.item.stop() + } + onRefreshPositionRequested: if( vocalPlayer.item){ + progressPosition = vocalPlayer.item.getPosition() + value = 100 * ( progressPosition / vocalPlayer.item.duration) + }else{// Recording + progressDuration = vocalRecorder.getDuration() + progressPosition = progressDuration + value = value + 0.01 + } + onSeekRequested: if( vocalPlayer.item){ + vocalPlayer.item.seek(ms) + progressPosition = vocalPlayer.item.getPosition() + value = 100 * (progressPosition / vocalPlayer.item.duration) + } + } + } + ActionButton{ + Layout.preferredHeight: iconSize + Layout.preferredWidth: iconSize + Layout.rightMargin: 15 + Layout.leftMargin: 5 + Layout.alignment: Qt.AlignVCenter + isCustom: true + colorSet: audioPreviewBlock.isRecording ? ChatAudioPreviewStyle.stopAction + : (audioPreviewBlock.isPlaying ? ChatAudioPreviewStyle.pauseAction + : ChatAudioPreviewStyle.playAction) + onClicked:{ + if(audioPreviewBlock.isRecording){// Stop the record and save the file + audioPreviewBlock.vocalRecorder.stop() + mediaProgressBar.value = 100 + vocalPlayer.active = true + }else if(audioPreviewBlock.isPlaying){// Pause the play + vocalPlayer.item.pause() + }else{// Play the audio + vocalPlayer.item.play() + } + } + } + } + states: [ + State { + name: "hidden" + PropertyChanges { target: audioPreviewBlock; opacity: 0 } + }, + State { + name: "showed" + PropertyChanges { target: audioPreviewBlock; opacity: 1 } + } + ] + transitions: [ + Transition { + from: "*"; to: "showed" + SequentialAnimation{ + ScriptAction{ script: audioPreviewBlock.vocalRecorder.start() } + NumberAnimation{ properties: "opacity"; easing.type: Easing.OutBounce; duration: 250 } + } + }, + Transition { + from: "*"; to: "hidden" + SequentialAnimation{ + ScriptAction{ script: RecorderManager.clearVocalRecorder()} + ScriptAction{ script: vocalPlayer.active = false } + NumberAnimation{ properties: "opacity"; duration: 250 } + } + } + ] +} \ No newline at end of file diff --git a/linphone-app/ui/modules/Linphone/Chat/ChatForwardMessage.qml b/linphone-app/ui/modules/Linphone/Chat/ChatForwardMessage.qml index ed095baf0..b0eb77c90 100644 --- a/linphone-app/ui/modules/Linphone/Chat/ChatForwardMessage.qml +++ b/linphone-app/ui/modules/Linphone/Chat/ChatForwardMessage.qml @@ -49,7 +49,7 @@ Item { id: headerText height: icon.height verticalAlignment: Qt.AlignVCenter - property string forwardInfo: mainChatMessageModel.getForwardInfoDisplayName + property string forwardInfo: mainChatMessageModel ? mainChatMessageModel.getForwardInfoDisplayName : '' //: 'Forwarded' : Header on a message that contains a forward. text: 'Forwarded' + (forwardInfo ? ' : ' +forwardInfo : '') font.family: mainItem.customFont.family diff --git a/linphone-app/ui/modules/Linphone/Chat/ChatMessagePreview.qml b/linphone-app/ui/modules/Linphone/Chat/ChatMessagePreview.qml index b87f7985d..39f0862da 100644 --- a/linphone-app/ui/modules/Linphone/Chat/ChatMessagePreview.qml +++ b/linphone-app/ui/modules/Linphone/Chat/ChatMessagePreview.qml @@ -15,18 +15,22 @@ import 'Chat.js' as Logic // ============================================================================= ColumnLayout{ property alias replyChatMessageModel : replyPreview.replyChatMessageModel - property int maxHeight: parent.height + property int maxHeight: parent.height - ( audioPreview.visible ? audioPreview.height : 0) anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom - height: replyPreview.height + spacing: 0 function hide(){ replyPreview.hide() + audioPreview.hide() } - ChatReplyPreview{ id: replyPreview Layout.fillWidth: true } + ChatAudioPreview{ + id: audioPreview + Layout.fillWidth: true + } } \ No newline at end of file diff --git a/linphone-app/ui/modules/Linphone/Chat/ChatReplyMessage.qml b/linphone-app/ui/modules/Linphone/Chat/ChatReplyMessage.qml index 78e364301..08eecd2d7 100644 --- a/linphone-app/ui/modules/Linphone/Chat/ChatReplyMessage.qml +++ b/linphone-app/ui/modules/Linphone/Chat/ChatReplyMessage.qml @@ -33,7 +33,7 @@ Item { width: maxWidth < 0 || maxWidth > fitWidth ? fitWidth : maxWidth height: fitHeight - onMainChatMessageModelChanged: if( mainChatMessageModel.replyChatMessageModel) chatMessageModel = mainChatMessageModel.replyChatMessageModel + onMainChatMessageModelChanged: if( mainChatMessageModel && mainChatMessageModel.replyChatMessageModel) chatMessageModel = mainChatMessageModel.replyChatMessageModel ColumnLayout{ @@ -118,7 +118,7 @@ Item { font.pointSize: Units.dp * (customFont.pointSize + ChatReplyMessageStyle.replyArea.pointSizeOffset) font.weight: Font.Light color: ChatReplyMessageStyle.replyArea.foregroundColor - text: (visible ? Utils.encodeTextToQmlRichFormat(chatMessageModel.content, { + text: (visible && chatMessageModel? Utils.encodeTextToQmlRichFormat(chatMessageModel.content, { imagesHeight: ChatStyle.entry.message.images.height, imagesWidth: ChatStyle.entry.message.images.width }) diff --git a/linphone-app/ui/modules/Linphone/Chat/ChatReplyPreview.qml b/linphone-app/ui/modules/Linphone/Chat/ChatReplyPreview.qml index 6081ce1bb..72cd3f936 100644 --- a/linphone-app/ui/modules/Linphone/Chat/ChatReplyPreview.qml +++ b/linphone-app/ui/modules/Linphone/Chat/ChatReplyPreview.qml @@ -19,7 +19,7 @@ Rectangle{ property ChatMessageModel replyChatMessageModel onReplyChatMessageModelChanged: if(replyChatMessageModel) replyPreviewBlock.state = "showed" - Layout.preferredHeight: Math.min(replayPreviewText.implicitHeight + replyPreviewHeaderArea.implicitHeight + 10, parent.maxHeight) + Layout.preferredHeight: Math.min(replyPreviewText.implicitHeight + replyPreviewHeaderArea.implicitHeight + 10, parent.maxHeight) property int leftMargin: textArea.textLeftMargin property int rightMargin: textArea.textRightMargin @@ -69,11 +69,11 @@ Rectangle{ } } Flickable { - id: replayPreviewTextArea - ScrollBar.vertical: ForceScrollBar {visible: replayPreviewTextArea.height < replayPreviewText.implicitHeight} + id: replyPreviewTextArea + ScrollBar.vertical: ForceScrollBar {visible: replyPreviewTextArea.height < replyPreviewText.implicitHeight} boundsBehavior: Flickable.StopAtBounds clip: true - contentHeight: replayPreviewText.implicitHeight + contentHeight: replyPreviewText.implicitHeight contentWidth: width - ScrollBar.vertical.width flickableDirection: Flickable.VerticalFlick @@ -81,7 +81,7 @@ Rectangle{ Layout.fillWidth: true TextEdit { - id: replayPreviewText + id: replyPreviewText property font customFont : SettingsModel.textMessageFont anchors.left: parent.left diff --git a/linphone-app/ui/modules/Linphone/Styles/Chat/ChatAudioPreviewStyle.qml b/linphone-app/ui/modules/Linphone/Styles/Chat/ChatAudioPreviewStyle.qml new file mode 100644 index 000000000..f27727b2b --- /dev/null +++ b/linphone-app/ui/modules/Linphone/Styles/Chat/ChatAudioPreviewStyle.qml @@ -0,0 +1,120 @@ +pragma Singleton +import QtQml 2.2 + +import Units 1.0 +import ColorsList 1.0 + +// ============================================================================= + +QtObject { + property string sectionName : 'ChatAudioPreview' + property color color: ColorsList.add(sectionName, 'q').color + property QtObject header: QtObject{ + property color color: ColorsList.add(sectionName+'_header', 'h').color + property int pointSizeOffset: -3 + property QtObject replyIcon: QtObject{ + property string icon : 'menu_reply_custom' + property int iconSize: 22 + } + } + property color backgroundColor: ColorsList.add(sectionName+'_bg', 'aa').color + + property QtObject audioArea: QtObject{ + property color outgoingMarkColor: ColorsList.add(sectionName+'_reply_outgoing_mark', 'm').color + property color incomingMarkColor: ColorsList.add(sectionName+'_reply_incoming_mark', 'r').color + property color backgroundColor: ColorsList.add(sectionName+'_reply_bg', 'q').color + property color foregroundColor: ColorsList.add(sectionName+'_reply_fg', 'h').color + property int usernamePointSizeOffset: -2 + property int pointSizeOffset: -2 + } + property QtObject deleteAction: QtObject { + property int iconSize: 40 + property string name : 'delete' + property string icon : 'delete_custom' + property color backgroundNormalColor : ColorsList.addImageColor(sectionName+'_'+name+'_bg_n', icon, 'me_n_b_bg').color + property color backgroundHoveredColor : ColorsList.addImageColor(sectionName+'_'+name+'_bg_h', icon, 'me_h_b_bg').color + property color backgroundPressedColor : ColorsList.addImageColor(sectionName+'_'+name+'_bg_p', icon, 'me_p_b_bg').color + property color foregroundNormalColor : ColorsList.addImageColor(sectionName+'_'+name+'_fg_n', icon, 'me_n_b_fg').color + property color foregroundHoveredColor : ColorsList.addImageColor(sectionName+'_'+name+'_fg_h', icon, 'me_h_b_fg').color + property color foregroundPressedColor : ColorsList.addImageColor(sectionName+'_'+name+'_fg_p', icon, 'me_p_b_fg').color + } + property QtObject stopAction: QtObject { + property int iconSize: 30 + property string name : 'stop' + property string icon : 'chat_audio_stop_custom' + property color backgroundNormalColor : ColorsList.addImageColor(sectionName+'_'+name+'_bg_n', icon, 'me_n_b_bg').color + property color backgroundHoveredColor : ColorsList.addImageColor(sectionName+'_'+name+'_bg_h', icon, 'me_h_b_bg').color + property color backgroundPressedColor : ColorsList.addImageColor(sectionName+'_'+name+'_bg_p', icon, 'me_p_b_bg').color + property color foregroundNormalColor : ColorsList.addImageColor(sectionName+'_'+name+'_fg_n', icon, 'me_n_b_fg').color + property color foregroundHoveredColor : ColorsList.addImageColor(sectionName+'_'+name+'_fg_h', icon, 'me_h_b_fg').color + property color foregroundPressedColor : ColorsList.addImageColor(sectionName+'_'+name+'_fg_p', icon, 'me_p_b_fg').color + } + property QtObject pauseAction: QtObject { + property int iconSize: 30 + property string name : 'pause' + property string icon : 'chat_audio_pause_custom' + property color backgroundNormalColor : ColorsList.addImageColor(sectionName+'_'+name+'_bg_n', icon, 'me_n_b_bg').color + property color backgroundHoveredColor : ColorsList.addImageColor(sectionName+'_'+name+'_bg_h', icon, 'me_h_b_bg').color + property color backgroundPressedColor : ColorsList.addImageColor(sectionName+'_'+name+'_bg_p', icon, 'me_p_b_bg').color + property color foregroundNormalColor : ColorsList.addImageColor(sectionName+'_'+name+'_fg_n', icon, 'me_n_b_fg').color + property color foregroundHoveredColor : ColorsList.addImageColor(sectionName+'_'+name+'_fg_h', icon, 'me_h_b_fg').color + property color foregroundPressedColor : ColorsList.addImageColor(sectionName+'_'+name+'_fg_p', icon, 'me_p_b_fg').color + } + property QtObject playAction: QtObject { + property int iconSize: 30 + property string name : 'play' + property string icon : 'chat_audio_play_custom' + property color backgroundNormalColor : ColorsList.addImageColor(sectionName+'_'+name+'_bg_n', icon, 'me_n_b_bg').color + property color backgroundHoveredColor : ColorsList.addImageColor(sectionName+'_'+name+'_bg_h', icon, 'me_h_b_bg').color + property color backgroundPressedColor : ColorsList.addImageColor(sectionName+'_'+name+'_bg_p', icon, 'me_p_b_bg').color + property color foregroundNormalColor : ColorsList.addImageColor(sectionName+'_'+name+'_fg_n', icon, 'me_n_b_fg').color + property color foregroundHoveredColor : ColorsList.addImageColor(sectionName+'_'+name+'_fg_h', icon, 'me_h_b_fg').color + property color foregroundPressedColor : ColorsList.addImageColor(sectionName+'_'+name+'_fg_p', icon, 'me_p_b_fg').color + } + + property QtObject progressionWave: QtObject{ + property int iconSize: 30 + property int iconHeight: 40 + property int iconWidth: 250 + property string name : 'progression_soundwave' + property string icon : 'chat_audio_soundwave_custom' + property color backgroundNormalColor : ColorsList.addImageColor(sectionName+'_'+name+'_bg_n', icon, 'a_n_b_bg').color + property color backgroundHoveredColor : ColorsList.addImageColor(sectionName+'_'+name+'_bg_h', icon, 'a_h_b_bg').color + property color backgroundPressedColor : ColorsList.addImageColor(sectionName+'_'+name+'_bg_p', icon, 'a_p_b_bg').color + property color foregroundNormalColor : ColorsList.addImageColor(sectionName+'_'+name+'_fg_n', icon, 'a_n_b_fg').color + property color foregroundHoveredColor : ColorsList.addImageColor(sectionName+'_'+name+'_fg_h', icon, 'a_h_b_fg').color + property color foregroundPressedColor : ColorsList.addImageColor(sectionName+'_'+name+'_fg_p', icon, 'a_p_b_fg').color + + property color backgroundHiddenPartNormalColor : ColorsList.addImageColor(sectionName+'_'+name+'_hidden_bg_n', icon, 'l_n_b_bg').color + property color backgroundHiddenPartHoveredColor : ColorsList.addImageColor(sectionName+'_'+name+'_hidden_bg_h', icon, 'l_h_b_bg').color + property color backgroundHiddenPartPressedColor : ColorsList.addImageColor(sectionName+'_'+name+'_hidden_bg_p', icon, 'l_p_b_bg').color + + property color foregroundHiddenPartNormalColor : ColorsList.addImageColor(sectionName+'_'+name+'_hidden_fg_n', icon, 'l_n_b_fg').color + property color foregroundHiddenPartHoveredColor : ColorsList.addImageColor(sectionName+'_'+name+'_hidden_fg_h', icon, 'l_h_b_fg').color + property color foregroundHiddenPartPressedColor : ColorsList.addImageColor(sectionName+'_'+name+'_hidden_fg_p', icon, 'l_p_b_fg').color + } + + property QtObject recordingProgressionWave: QtObject{ + property int iconSize: 30 + property int iconHeight: 40 + property int iconWidth: 250 + property string name : 'recording_progression_soundwave' + property string icon : 'chat_audio_soundwave_custom' + property color backgroundNormalColor : ColorsList.addImageColor(sectionName+'_'+name+'_bg_n', icon, 'r_n_b_bg').color + property color backgroundHoveredColor : ColorsList.addImageColor(sectionName+'_'+name+'_bg_h', icon, 'r_h_b_bg').color + property color backgroundPressedColor : ColorsList.addImageColor(sectionName+'_'+name+'_bg_p', icon, 'r_p_b_bg').color + property color foregroundNormalColor : ColorsList.addImageColor(sectionName+'_'+name+'_fg_n', icon, 'r_n_b_fg').color + property color foregroundHoveredColor : ColorsList.addImageColor(sectionName+'_'+name+'_fg_h', icon, 'r_h_b_fg').color + property color foregroundPressedColor : ColorsList.addImageColor(sectionName+'_'+name+'_fg_p', icon, 'r_p_b_fg').color + + property color backgroundHiddenPartNormalColor : ColorsList.addImageColor(sectionName+'_'+name+'_hidden_bg_n', icon, 'l_n_b_bg').color + property color backgroundHiddenPartHoveredColor : ColorsList.addImageColor(sectionName+'_'+name+'_hidden_bg_h', icon, 'l_h_b_bg').color + property color backgroundHiddenPartPressedColor : ColorsList.addImageColor(sectionName+'_'+name+'_hidden_bg_p', icon, 'l_p_b_bg').color + + property color foregroundHiddenPartNormalColor : ColorsList.addImageColor(sectionName+'_'+name+'_hidden_fg_n', icon, 'l_n_b_fg').color + property color foregroundHiddenPartHoveredColor : ColorsList.addImageColor(sectionName+'_'+name+'_hidden_fg_h', icon, 'l_h_b_fg').color + property color foregroundHiddenPartPressedColor : ColorsList.addImageColor(sectionName+'_'+name+'_hidden_fg_p', icon, 'l_p_b_fg').color + } + + property int padding: 8 +} diff --git a/linphone-app/ui/modules/Linphone/Styles/qmldir b/linphone-app/ui/modules/Linphone/Styles/qmldir index 98d31211f..1b421a0e3 100644 --- a/linphone-app/ui/modules/Linphone/Styles/qmldir +++ b/linphone-app/ui/modules/Linphone/Styles/qmldir @@ -10,6 +10,7 @@ singleton CardBlockStyle 1.0 Blocks/CardBlockStyle.qml singleton RequestBlockStyle 1.0 Blocks/RequestBlockStyle.qml singleton ChatStyle 1.0 Chat/ChatStyle.qml +singleton ChatAudioPreviewStyle 1.0 Chat/ChatAudioPreviewStyle.qml singleton ChatForwardMessageStyle 1.0 Chat/ChatForwardMessageStyle.qml singleton ChatReplyMessageStyle 1.0 Chat/ChatReplyMessageStyle.qml diff --git a/linphone-app/ui/modules/Linphone/qmldir b/linphone-app/ui/modules/Linphone/qmldir index 5fd04e4e4..8d0b973b4 100644 --- a/linphone-app/ui/modules/Linphone/qmldir +++ b/linphone-app/ui/modules/Linphone/qmldir @@ -15,6 +15,7 @@ Calls 1.0 Calls/Calls.qml CallStatistics 1.0 Calls/CallStatistics.qml Chat 1.0 Chat/Chat.qml +ChatAudioPreview 1.0 Chat/ChatAudioPreview.qml ChatMessagePreview 1.0 Chat/ChatMessagePreview.qml ChatForwardMessage 1.0 Chat/ChatForwardMessage.qml ChatReplyMessage 1.0 Chat/ChatReplyMessage.qml diff --git a/linphone-sdk b/linphone-sdk index 3ea9277c8..b1171bf5f 160000 --- a/linphone-sdk +++ b/linphone-sdk @@ -1 +1 @@ -Subproject commit 3ea9277c8035deb632e47519971122550ca8f64d +Subproject commit b1171bf5faf08237ddc6e074b71dd6319c0470af