From 6aadc2f292f594dac6e07424ea96c934261b952e Mon Sep 17 00:00:00 2001 From: Gaelle Braud Date: Wed, 11 Jun 2025 15:00:06 +0200 Subject: [PATCH] sound player --- .gitmodules | 2 +- CMakeLists.txt | 2 +- Linphone/core/App.cpp | 2 + Linphone/core/CMakeLists.txt | 3 + .../core/sound-player/SoundPlayerCore.cpp | 218 ++++++++++++++++++ .../core/sound-player/SoundPlayerCore.hpp | 115 +++++++++ Linphone/core/sound-player/SoundPlayerGui.cpp | 53 +++++ Linphone/core/sound-player/SoundPlayerGui.hpp | 52 +++++ Linphone/data/image/pause-fill.svg | 3 + Linphone/data/image/stop-filled.svg | 3 + Linphone/main.cpp | 2 +- Linphone/model/CMakeLists.txt | 1 + .../model/sound-player/SoundPlayerModel.cpp | 133 +++++++++++ .../model/sound-player/SoundPlayerModel.hpp | 80 +++++++ .../VideoSourceDescriptorModel.cpp | 3 +- Linphone/tool/CMakeLists.txt | 1 + Linphone/tool/LinphoneEnums.cpp | 1 + Linphone/tool/LinphoneEnums.hpp | 5 +- Linphone/tool/file/TemporaryFile.cpp | 95 ++++++++ Linphone/tool/file/TemporaryFile.hpp | 65 ++++++ Linphone/view/CMakeLists.txt | 1 + .../Control/Display/Chat/ChatAudioContent.qml | 146 +++++------- .../Display/Chat/ChatMessageContent.qml | 44 ++-- .../view/Control/Display/MediaProgressBar.qml | 165 +++++++++++++ Linphone/view/Control/Display/ProgressBar.qml | 4 +- .../Input/Chat/ChatDroppableTextArea.qml | 7 +- .../view/Page/Main/Contact/ContactPage.qml | 10 +- Linphone/view/Style/AppIcons.qml | 2 + 28 files changed, 1085 insertions(+), 133 deletions(-) create mode 100644 Linphone/core/sound-player/SoundPlayerCore.cpp create mode 100644 Linphone/core/sound-player/SoundPlayerCore.hpp create mode 100644 Linphone/core/sound-player/SoundPlayerGui.cpp create mode 100644 Linphone/core/sound-player/SoundPlayerGui.hpp create mode 100644 Linphone/data/image/pause-fill.svg create mode 100644 Linphone/data/image/stop-filled.svg create mode 100644 Linphone/model/sound-player/SoundPlayerModel.cpp create mode 100644 Linphone/model/sound-player/SoundPlayerModel.hpp create mode 100644 Linphone/tool/file/TemporaryFile.cpp create mode 100644 Linphone/tool/file/TemporaryFile.hpp create mode 100644 Linphone/view/Control/Display/MediaProgressBar.qml diff --git a/.gitmodules b/.gitmodules index 6a826be5d..bd3d3a5dd 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,4 +3,4 @@ path = external/linphone-sdk url = https://gitlab.linphone.org/BC/public/linphone-sdk.git [submodule "external/qtkeychain"] path = external/qtkeychain - url = https://github.com/frankosterfeld/qtkeychain.git + url = https://gitlab.linphone.org/BC/public/external/qtkeychain.git \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index e2d9be890..1652a1f3c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -206,7 +206,7 @@ set(ENABLE_CSHARP_WRAPPER OFF CACHE BOOL "Build the CSharp wrapper for Liblinpho set(ENABLE_THEORA OFF) set(ENABLE_QT_GL ${ENABLE_VIDEO}) -find_package(Qt6 REQUIRED COMPONENTS Core Widgets) +find_package(Qt6 REQUIRED COMPONENTS Core Widgets Core5Compat) if(NOT Qt6_FOUND) message(FATAL_ERROR "Minimum supported Qt6!") diff --git a/Linphone/core/App.cpp b/Linphone/core/App.cpp index 945d6273d..431e101a8 100644 --- a/Linphone/core/App.cpp +++ b/Linphone/core/App.cpp @@ -84,6 +84,7 @@ #include "core/search/MagicSearchProxy.hpp" #include "core/setting/SettingsCore.hpp" #include "core/singleapplication/singleapplication.h" +#include "core/sound-player/SoundPlayerGui.hpp" #include "core/timezone/TimeZoneProxy.hpp" #include "core/translator/DefaultTranslatorCore.hpp" #include "core/variant/VariantList.hpp" @@ -684,6 +685,7 @@ void App::initCppInterfaces() { qmlRegisterType(Constants::MainQmlUri, 1, 0, "CameraGui"); qmlRegisterType(Constants::MainQmlUri, 1, 0, "FPSCounter"); qmlRegisterType(Constants::MainQmlUri, 1, 0, "EmojiModel"); + qmlRegisterType(Constants::MainQmlUri, 1, 0, "SoundPlayerGui"); qmlRegisterType(Constants::MainQmlUri, 1, 0, "TimeZoneProxy"); diff --git a/Linphone/core/CMakeLists.txt b/Linphone/core/CMakeLists.txt index e95725b88..268bbead7 100644 --- a/Linphone/core/CMakeLists.txt +++ b/Linphone/core/CMakeLists.txt @@ -83,6 +83,9 @@ list(APPEND _LINPHONEAPP_SOURCES core/screen/ScreenList.cpp core/screen/ScreenProxy.cpp + core/sound-player/SoundPlayerCore.cpp + core/sound-player/SoundPlayerGui.cpp + core/videoSource/VideoSourceDescriptorCore.cpp core/videoSource/VideoSourceDescriptorGui.cpp diff --git a/Linphone/core/sound-player/SoundPlayerCore.cpp b/Linphone/core/sound-player/SoundPlayerCore.cpp new file mode 100644 index 000000000..f5b0829bb --- /dev/null +++ b/Linphone/core/sound-player/SoundPlayerCore.cpp @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2010-2020 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 "core/App.hpp" +#include "core/setting/SettingsCore.hpp" +#include "tool/Utils.hpp" + +#include "SoundPlayerCore.hpp" + +DEFINE_ABSTRACT_OBJECT(SoundPlayerCore) + +// ============================================================================= + +using namespace std; + +namespace { +int ForceCloseTimerInterval = 20; +} + +// ----------------------------------------------------------------------------- + +QSharedPointer SoundPlayerCore::create() { + auto sharedPointer = QSharedPointer(new SoundPlayerCore(), &QObject::deleteLater); + sharedPointer->setSelf(sharedPointer); + sharedPointer->moveToThread(App::getInstance()->thread()); + return sharedPointer; +} + +SoundPlayerCore::SoundPlayerCore(QObject *parent) { + // connect(mForceCloseTimer, &QTimer::timeout, this, &SoundPlayerCore::handleEof); +} + +SoundPlayerCore::~SoundPlayerCore() { + // mForceCloseTimer->stop(); +} + +void SoundPlayerCore::setSelf(QSharedPointer me) { + auto settingsModel = SettingsModel::getInstance(); + auto coreModel = CoreModel::getInstance(); + + mCoreModelConnection = SafeConnection::create(me, coreModel); + mSettingsModelConnection = SafeConnection::create(me, settingsModel); + mSettingsModelConnection->makeConnectToModel(&SettingsModel::ringerDeviceChanged, [this, me] { + if (mSoundPlayerModel) mSoundPlayerModel->stop(true); + else mSettingsModelConnection->invokeToCore([this, me] { buildInternalPlayer(me); }); + }); + + mCoreModelConnection->invokeToModel([this, me, settingsModel, coreModel] { buildInternalPlayer(me); }); + // mCoreModelConnection->makeConnectToCore(&SoundPlayerCore::sourceChanged, [this, me] { + // mCoreModelConnection->invokeToModel([this, me] { buildInternalPlayer(me); }); + // }); +} + +// ----------------------------------------------------------------------------- + +void SoundPlayerCore::buildInternalPlayer(QSharedPointer me) { + mustBeInLinphoneThread(log().arg(Q_FUNC_INFO)); + auto coreModel = CoreModel::getInstance(); + auto settingsModel = SettingsModel::getInstance(); + + if (mSoundPlayerModelConnection) mSoundPlayerModelConnection->disconnect(); + + auto player = coreModel->getCore()->createLocalPlayer( + Utils::appStringToCoreString(mIsRinger ? settingsModel->getRingerDevice()["display_name"].toString() + : settingsModel->getPlaybackDevice()["display_name"].toString()), + "", nullptr); + + mHasVideo = player->getIsVideoAvailable(); + mDuration = player->getDuration(); + + mSoundPlayerModel = Utils::makeQObject_ptr(player); + mSoundPlayerModel->setSelf(mSoundPlayerModel); + mSoundPlayerModelConnection = SafeConnection::create(me, mSoundPlayerModel); + mSoundPlayerModelConnection->makeConnectToCore(&SoundPlayerCore::lStop, [this](bool force) { + mSoundPlayerModelConnection->invokeToModel([this, force] { + if (mPlaybackState == LinphoneEnums::PlaybackState::StoppedState && !force) return; + mSoundPlayerModel->stop(force); + }); + }); + mSoundPlayerModelConnection->makeConnectToModel(&SoundPlayerModel::stopped, [this, me](bool force) { + if (force) buildInternalPlayer(me); + }); + mSoundPlayerModelConnection->makeConnectToCore(&SoundPlayerCore::lPause, [this]() { + mSoundPlayerModelConnection->invokeToModel([this] { mSoundPlayerModel->pause(); }); + }); + mSoundPlayerModelConnection->makeConnectToModel( + &SoundPlayerModel::playbackStateChanged, [this](LinphoneEnums::PlaybackState state) { + mSoundPlayerModelConnection->invokeToCore([this, state] { setPlaybackState(state); }); + }); + mSoundPlayerModelConnection->makeConnectToCore(&SoundPlayerCore::lOpen, [this]() { + mSoundPlayerModelConnection->invokeToModel([this] { mSoundPlayerModel->open(mSource); }); + }); + mSoundPlayerModelConnection->makeConnectToCore(&SoundPlayerCore::lPlay, [this]() { + mSoundPlayerModelConnection->invokeToModel([this] { mSoundPlayerModel->play(mSource); }); + }); + mSoundPlayerModelConnection->makeConnectToCore(&SoundPlayerCore::lSeek, [this](int offset) { + mSoundPlayerModelConnection->invokeToModel([this, offset] { mSoundPlayerModel->seek(offset); }); + }); + mSoundPlayerModelConnection->makeConnectToModel(&SoundPlayerModel::positionChanged, [this](int pos) { + mSoundPlayerModelConnection->invokeToCore([this, pos] { setPosition(pos); }); + }); + mSoundPlayerModelConnection->makeConnectToCore(&SoundPlayerCore::lRefreshPosition, [this]() { + mSoundPlayerModelConnection->invokeToModel([this] { + auto pos = mSoundPlayerModel->getPosition(); + mSoundPlayerModelConnection->invokeToModel([this, pos] { setPosition(pos); }); + }); + }); + mSoundPlayerModelConnection->makeConnectToModel(&SoundPlayerModel::eofReached, + [this](const shared_ptr &player) { + mSoundPlayerModelConnection->invokeToCore([this] { + mForceClose = true; + handleEof(); + }); + }); +} + +// ----------------------------------------------------------------------------- + +int SoundPlayerCore::getPosition() const { + return mPosition; +} + +void SoundPlayerCore::setPosition(int position) { + if (mPosition != position) { + mPosition = position; + emit positionChanged(); + } +} + +bool SoundPlayerCore::hasVideo() const { + return mHasVideo; +} + +void SoundPlayerCore::handleEof() { + if (mForceClose) { + mForceClose = false; + lStop(); + } +} + +void SoundPlayerCore::setError(const QString &message) { + qWarning() << message; + mError = message; + emit errorChanged(message); +} + +QString SoundPlayerCore::getSource() const { + return mSource; +} + +void SoundPlayerCore::setSource(const QString &source) { + if (source == mSource) return; + + lStop(true); + mSource = source; + + emit sourceChanged(source); +} + +LinphoneEnums::PlaybackState SoundPlayerCore::getPlaybackState() const { + return mPlaybackState; +} + +void SoundPlayerCore::setPlaybackState(LinphoneEnums::PlaybackState playbackState) { + if (mPlaybackState != playbackState) { + mPlaybackState = playbackState; + emit playbackStateChanged(playbackState); + } + // switch (playbackState) { + // case PlayingState: + // lPlay(); + // break; + // case PausedState: + // lPause(); + // break; + // case StoppedState: + // lStop(); + // break; + // case ErrorState: + // break; + // } +} + +// ----------------------------------------------------------------------------- + +int SoundPlayerCore::getDuration() const { + return mDuration; +} + +QDateTime SoundPlayerCore::getCreationDateTime() const { + QFileInfo fileInfo(mSource); + QDateTime creationDate = fileInfo.birthTime(); + return creationDate.isValid() ? creationDate : fileInfo.lastModified(); +} + +QString SoundPlayerCore::getBaseName() const { + return QFileInfo(mSource).baseName(); +} \ No newline at end of file diff --git a/Linphone/core/sound-player/SoundPlayerCore.hpp b/Linphone/core/sound-player/SoundPlayerCore.hpp new file mode 100644 index 000000000..90e1b3c37 --- /dev/null +++ b/Linphone/core/sound-player/SoundPlayerCore.hpp @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2010-2020 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 SOUND_PLAYER_MODEL_H_ +#define SOUND_PLAYER_MODEL_H_ + +#include "model/core/CoreModel.hpp" +#include "model/setting/SettingsModel.hpp" +#include "model/sound-player/SoundPlayerModel.hpp" +#include "tool/AbstractObject.hpp" +#include "tool/thread/SafeConnection.hpp" + +#include +#include +#include + +// ============================================================================= + +class QTimer; + +class SoundPlayerCore : public QObject, public AbstractObject { + Q_OBJECT + + Q_PROPERTY(QString source READ getSource WRITE setSource NOTIFY sourceChanged) + Q_PROPERTY(QString baseName READ getBaseName NOTIFY sourceChanged) + Q_PROPERTY(LinphoneEnums::PlaybackState playbackState READ getPlaybackState WRITE setPlaybackState NOTIFY + playbackStateChanged) + Q_PROPERTY(int duration READ getDuration NOTIFY sourceChanged) + Q_PROPERTY(int position READ getPosition WRITE setPosition NOTIFY positionChanged) + Q_PROPERTY(bool isRinger MEMBER mIsRinger) + Q_PROPERTY(QDateTime creationDateTime READ getCreationDateTime NOTIFY sourceChanged) + +public: + static QSharedPointer create(); + SoundPlayerCore(QObject *parent = Q_NULLPTR); + ~SoundPlayerCore(); + void setSelf(QSharedPointer me); + + int getPosition() const; + void setPosition(int position); + bool hasVideo() const; // Call it after playing a video because the detection is not outside this scope. + + int getDuration() const; + QDateTime getCreationDateTime() const; + QString getBaseName() const; + + QString getSource() const; + void setSource(const QString &source); + + LinphoneEnums::PlaybackState getPlaybackState() const; + void setPlaybackState(LinphoneEnums::PlaybackState playbackState); + +signals: + bool lOpen(); + void lPause(); + bool lPlay(); + void lStop(bool force = false); + void lSeek(int offset); + void lRefreshPosition(); + + void paused(); + void playing(); + void stopped(); + void errorChanged(QString error); + + void sourceChanged(const QString &source); + void playbackStateChanged(LinphoneEnums::PlaybackState playbackState); + void positionChanged(); + +private: + DECLARE_ABSTRACT_OBJECT + + void buildInternalPlayer(QSharedPointer me); + + void handleEof(); + void setError(const QString &message); + + QString mSource; + LinphoneEnums::PlaybackState mPlaybackState = LinphoneEnums::PlaybackState::StoppedState; + bool mIsRinger = false; + + bool mHasVideo = false; + int mPosition = 0; + int mDuration = 0; + QString mError; + + bool mForceClose = false; + QMutex mForceCloseMutex; + + QTimer *mForceCloseTimer = nullptr; + + std::shared_ptr mSoundPlayerModel; + QSharedPointer> mSoundPlayerModelConnection; + QSharedPointer> mCoreModelConnection; + QSharedPointer> mSettingsModelConnection; +}; + +#endif // SOUND_PLAYER_MODEL_H_ diff --git a/Linphone/core/sound-player/SoundPlayerGui.cpp b/Linphone/core/sound-player/SoundPlayerGui.cpp new file mode 100644 index 000000000..c4a7a11b7 --- /dev/null +++ b/Linphone/core/sound-player/SoundPlayerGui.cpp @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2010-2024 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 "SoundPlayerGui.hpp" +#include "core/App.hpp" + +DEFINE_ABSTRACT_OBJECT(SoundPlayerGui) + +SoundPlayerGui::SoundPlayerGui(QObject *parent) : QObject(parent) { + mustBeInMainThread(getClassName()); + mCore = SoundPlayerCore::create(); + if (mCore) connect(mCore.get(), &SoundPlayerCore::sourceChanged, this, &SoundPlayerGui::sourceChanged); + if (mCore) connect(mCore.get(), &SoundPlayerCore::stopped, this, &SoundPlayerGui::stopped); + if (mCore) connect(mCore.get(), &SoundPlayerCore::positionChanged, this, &SoundPlayerGui::positionChanged); +} +SoundPlayerGui::SoundPlayerGui(QSharedPointer core) { + App::getInstance()->mEngine->setObjectOwnership(this, QQmlEngine::JavaScriptOwnership); + mCore = core; + if (isInLinphoneThread()) moveToThread(App::getInstance()->thread()); +} + +SoundPlayerGui::~SoundPlayerGui() { + mustBeInMainThread("~" + getClassName()); +} + +SoundPlayerCore *SoundPlayerGui::getCore() const { + return mCore.get(); +} + +QString SoundPlayerGui::getSource() const { + return mCore ? mCore->getSource() : QString(); +} + +void SoundPlayerGui::setSource(QString source) { + if (mCore) mCore->setSource(source); +} diff --git a/Linphone/core/sound-player/SoundPlayerGui.hpp b/Linphone/core/sound-player/SoundPlayerGui.hpp new file mode 100644 index 000000000..7bf4f5a23 --- /dev/null +++ b/Linphone/core/sound-player/SoundPlayerGui.hpp @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2010-2024 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 SOUND_PLAYER_GUI_H_ +#define SOUND_PLAYER_GUI_H_ + +#include "SoundPlayerCore.hpp" +#include +#include + +class SoundPlayerGui : public QObject, public AbstractObject { + Q_OBJECT + + Q_PROPERTY(SoundPlayerCore *core READ getCore CONSTANT) + Q_PROPERTY(QString source READ getSource WRITE setSource NOTIFY sourceChanged) + +public: + SoundPlayerGui(QObject *parent = nullptr); + SoundPlayerGui(QSharedPointer core); + ~SoundPlayerGui(); + SoundPlayerCore *getCore() const; + QString getSource() const; + void setSource(QString source); + QSharedPointer mCore; + +signals: + void sourceChanged(); + void stopped(); + void positionChanged(); + +private: + DECLARE_ABSTRACT_OBJECT +}; + +#endif diff --git a/Linphone/data/image/pause-fill.svg b/Linphone/data/image/pause-fill.svg new file mode 100644 index 000000000..28c637404 --- /dev/null +++ b/Linphone/data/image/pause-fill.svg @@ -0,0 +1,3 @@ + + + diff --git a/Linphone/data/image/stop-filled.svg b/Linphone/data/image/stop-filled.svg new file mode 100644 index 000000000..ef5c8e84b --- /dev/null +++ b/Linphone/data/image/stop-filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/Linphone/main.cpp b/Linphone/main.cpp index 85c70da7c..9b3e65df2 100644 --- a/Linphone/main.cpp +++ b/Linphone/main.cpp @@ -90,7 +90,7 @@ int main(int argc, char *argv[]) { result = app->exec(); } while (result == (int)App::StatusCode::gRestartCode); QString message = "[Main] Exiting app with the code : " + QString::number(result); - if (result) qInfo() << message; + if (!result) qInfo() << message; else qWarning() << message; app->clean(); app = nullptr; diff --git a/Linphone/model/CMakeLists.txt b/Linphone/model/CMakeLists.txt index 4bed192b9..07de11305 100644 --- a/Linphone/model/CMakeLists.txt +++ b/Linphone/model/CMakeLists.txt @@ -41,6 +41,7 @@ list(APPEND _LINPHONEAPP_SOURCES model/setting/SettingsModel.cpp model/setting/MediastreamerUtils.cpp + model/sound-player/SoundPlayerModel.cpp model/tool/ToolModel.cpp model/tool/VfsUtils.cpp diff --git a/Linphone/model/sound-player/SoundPlayerModel.cpp b/Linphone/model/sound-player/SoundPlayerModel.cpp new file mode 100644 index 000000000..0baba87e5 --- /dev/null +++ b/Linphone/model/sound-player/SoundPlayerModel.cpp @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2010-2020 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 "model/core/CoreModel.hpp" +#include "tool/Utils.hpp" + +#include "SoundPlayerModel.hpp" + +DEFINE_ABSTRACT_OBJECT(SoundPlayerModel) + +// ============================================================================= + +using namespace std; + +namespace { +int ForceCloseTimerInterval = 20; +} + +class SoundPlayerModel::Handlers : public linphone::PlayerListener { +public: + Handlers(SoundPlayerModel *soundPlayerModel) { + mSoundPlayerModel = soundPlayerModel; + } + +private: + SoundPlayerModel *mSoundPlayerModel; +}; + +// ----------------------------------------------------------------------------- + +SoundPlayerModel::SoundPlayerModel(const std::shared_ptr &player, QObject *parent) + : ::Listener(player, parent) { + mustBeInLinphoneThread(getClassName()); +} + +SoundPlayerModel::~SoundPlayerModel() { + if (mMonitor) mMonitor->close(); +} + +// ----------------------------------------------------------------------------- + +void SoundPlayerModel::pause() { + if (mMonitor->pause()) { + //: Unable to pause + emit errorChanged("sound_player_pause_error"); + emit playbackStateChanged(LinphoneEnums::PlaybackState::ErrorState); + return; + } + emit paused(); + emit playbackStateChanged(LinphoneEnums::PlaybackState::PausedState); +} + +bool SoundPlayerModel::open(QString source) { + mMonitor->open(Utils::appStringToCoreString(source)); + emit open(); + return true; + // } + // return false; +} + +bool SoundPlayerModel::play(QString source) { + if (source == "") return false; + if (!open(source)) { + qWarning() << QStringLiteral("Unable to open: `%1`").arg(source); + //: Unable to open: `%1` + emit errorChanged(QString("sound_player_open_error").arg(source)); + return false; + } + if (mMonitor->start()) { + //: Unable to play %1 + emit errorChanged(QString("sound_player_play_error").arg(source)); + emit playbackStateChanged(LinphoneEnums::PlaybackState::ErrorState); + return false; + } + emit playing(); + emit playbackStateChanged(LinphoneEnums::PlaybackState::PlayingState); + return true; +} + +// ----------------------------------------------------------------------------- + +void SoundPlayerModel::seek(int offset) { + mMonitor->seek(offset); + emit positionChanged(mMonitor->getCurrentPosition()); +} + +// ----------------------------------------------------------------------------- + +int SoundPlayerModel::getPosition() const { + return mMonitor->getCurrentPosition(); +} + +bool SoundPlayerModel::hasVideo() const { + return mMonitor->getIsVideoAvailable(); +} + +void SoundPlayerModel::stop(bool force) { + if (mMonitor) mMonitor->close(); + emit stopped(force); + emit playbackStateChanged(LinphoneEnums::PlaybackState::StoppedState); +} + +// ----------------------------------------------------------------------------- + +int SoundPlayerModel::getDuration() const { + return mMonitor->getDuration(); +} + +/*------------------------------------------------------------------*/ + +void SoundPlayerModel::onEofReached(const shared_ptr &player) { + if (player == mMonitor) emit eofReached(player); +} \ No newline at end of file diff --git a/Linphone/model/sound-player/SoundPlayerModel.hpp b/Linphone/model/sound-player/SoundPlayerModel.hpp new file mode 100644 index 000000000..acaa77d9a --- /dev/null +++ b/Linphone/model/sound-player/SoundPlayerModel.hpp @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2010-2020 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 SOUND_PLAYER_H_ +#define SOUND_PLAYER_H_ + +#include "tool/AbstractObject.hpp" +#include + +#include +#include + +// ============================================================================= + +class QTimer; + +class SoundPlayerModel : public ::Listener, + public linphone::PlayerListener, + public AbstractObject { + class Handlers; + + Q_OBJECT + +public: + SoundPlayerModel(const std::shared_ptr &player, QObject *parent = nullptr); + ~SoundPlayerModel(); + + bool open(QString source); + void pause(); + bool play(QString source); + void stop(bool force = false); + void seek(int offset); + + int getPosition() const; + bool hasVideo() const; // Call it after playing a video because the detection is not outside this scope. + + int getDuration() const; + + void handleEof(); + + void setError(const QString &message); + +signals: + void sourceChanged(const QString &source); + + void paused(); + void open(); + void playing(); + void stopped(bool force); + void positionChanged(int position); + void errorChanged(QString error); + + void playbackStateChanged(LinphoneEnums::PlaybackState playbackState); + + void eofReached(const std::shared_ptr &player); + +private: + DECLARE_ABSTRACT_OBJECT + + void onEofReached(const std::shared_ptr &player); +}; + +#endif // SOUND_PLAYER_H_ diff --git a/Linphone/model/videoSource/VideoSourceDescriptorModel.cpp b/Linphone/model/videoSource/VideoSourceDescriptorModel.cpp index bcc0527a1..cb81e4a2d 100644 --- a/Linphone/model/videoSource/VideoSourceDescriptorModel.cpp +++ b/Linphone/model/videoSource/VideoSourceDescriptorModel.cpp @@ -41,7 +41,8 @@ void VideoSourceDescriptorModel::setScreenSharingDisplay(int index) { void VideoSourceDescriptorModel::setScreenSharingWindow(void *window) { // Get data from DesktopTools. if (!mDesc) mDesc = linphone::Factory::get()->createVideoSourceDescriptor(); - else if (getVideoSourceType() == LinphoneEnums::VideoSourceScreenSharingTypeWindow && window == getScreenSharing()) + else if (getVideoSourceType() == LinphoneEnums::VideoSourceScreenSharingType::VideoSourceScreenSharingTypeWindow && + window == getScreenSharing()) return; mDesc->setScreenSharing(linphone::VideoSourceScreenSharingType::Window, window); emit videoDescriptorChanged(); diff --git a/Linphone/tool/CMakeLists.txt b/Linphone/tool/CMakeLists.txt index 221983684..254fb63a7 100644 --- a/Linphone/tool/CMakeLists.txt +++ b/Linphone/tool/CMakeLists.txt @@ -24,6 +24,7 @@ list(APPEND _LINPHONEAPP_SOURCES tool/file/FileDownloader.cpp tool/file/FileExtractor.cpp + tool/file/TemporaryFile.cpp tool/ui/DashRectangle.cpp diff --git a/Linphone/tool/LinphoneEnums.cpp b/Linphone/tool/LinphoneEnums.cpp index 06ef49838..8bebda391 100644 --- a/Linphone/tool/LinphoneEnums.cpp +++ b/Linphone/tool/LinphoneEnums.cpp @@ -39,6 +39,7 @@ void LinphoneEnums::registerMetaTypes() { qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); + qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); diff --git a/Linphone/tool/LinphoneEnums.hpp b/Linphone/tool/LinphoneEnums.hpp index b192ce998..8e1293401 100644 --- a/Linphone/tool/LinphoneEnums.hpp +++ b/Linphone/tool/LinphoneEnums.hpp @@ -383,7 +383,7 @@ Q_ENUM_NS(AccountManagerServicesRequestType) // &type); LinphoneEnums::AccountManagerServicesRequestType fromLinphone(const // linphone::AccountManagerServicesRequest::Type &type); -enum VideoSourceScreenSharingType { +enum class VideoSourceScreenSharingType { VideoSourceScreenSharingTypeArea = int(linphone::VideoSourceScreenSharingType::Area), VideoSourceScreenSharingTypeDisplay = int(linphone::VideoSourceScreenSharingType::Display), VideoSourceScreenSharingTypeWindow = int(linphone::VideoSourceScreenSharingType::Window) @@ -393,6 +393,9 @@ Q_ENUM_NS(VideoSourceScreenSharingType) linphone::VideoSourceScreenSharingType toLinphone(const LinphoneEnums::VideoSourceScreenSharingType &type); LinphoneEnums::VideoSourceScreenSharingType fromLinphone(const linphone::VideoSourceScreenSharingType &type); +enum class PlaybackState { PlayingState = 0, PausedState = 1, StoppedState = 2, ErrorState = 3 }; +Q_ENUM_NS(PlaybackState); + } // namespace LinphoneEnums /* Q_DECLARE_METATYPE(LinphoneEnums::CallState) diff --git a/Linphone/tool/file/TemporaryFile.cpp b/Linphone/tool/file/TemporaryFile.cpp new file mode 100644 index 000000000..cb68353db --- /dev/null +++ b/Linphone/tool/file/TemporaryFile.cpp @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2010-2020 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 "TemporaryFile.hpp" + +#include "core/chat/message/content/ChatMessageContentCore.hpp" +#include "model/core/CoreModel.hpp" +#include "model/setting/SettingsModel.hpp" +#include "tool/Utils.hpp" + +#include + +// ============================================================================= + +using namespace std; + +DEFINE_ABSTRACT_OBJECT(TemporaryFile) + +TemporaryFile::TemporaryFile(QObject *parent) : QObject(parent) { +} + +TemporaryFile::~TemporaryFile() { + deleteFile(); +} + +void TemporaryFile::createFileFromContent(std::shared_ptr content, const bool &exportPlainFile) { + mustBeInLinphoneThread(log().arg(Q_FUNC_INFO)); + if (content) { + QString filePath; + if (exportPlainFile || (SettingsModel::getInstance()->getVfsEncrypted() && content->isFileEncrypted())) + filePath = Utils::coreStringToAppString(content->exportPlainFile()); + bool toDelete = true; + if (filePath.isEmpty()) { + filePath = Utils::coreStringToAppString(content->getFilePath()); + toDelete = false; + if (content->isFileEncrypted()) // filePath was empty while the file is encrypted : it couldn't be decoded. + setIsReadable(false); + else setIsReadable(true); + } else setIsReadable(true); + setFilePath(filePath, toDelete); + } +} + +void TemporaryFile::createFile(const QString &openFilePath, const bool &exportPlainFile) { + createFileFromContent(linphone::Factory::get()->createContentFromFile(Utils::appStringToCoreString(openFilePath)), + exportPlainFile); +} + +QString TemporaryFile::getFilePath() const { + return mFilePath; +} + +bool TemporaryFile::isReadable() const { + return mIsReadable; +} + +void TemporaryFile::setFilePath(const QString &path, const bool &toDelete) { + if (path != mFilePath) { + deleteFile(); + mFilePath = path; + mDeleteFile = toDelete; + emit filePathChanged(); + } +} + +void TemporaryFile::setIsReadable(const bool &isReadable) { + if (isReadable != mIsReadable) { + mIsReadable = isReadable; + emit isReadableChanged(); + } +} + +void TemporaryFile::deleteFile() { + if (mDeleteFile && !mFilePath.isEmpty()) QFile::remove(mFilePath); + mFilePath = ""; +} diff --git a/Linphone/tool/file/TemporaryFile.hpp b/Linphone/tool/file/TemporaryFile.hpp new file mode 100644 index 000000000..c590a38c6 --- /dev/null +++ b/Linphone/tool/file/TemporaryFile.hpp @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2010-2020 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 TEMPORARY_FILE_H_ +#define TEMPORARY_FILE_H_ + +#include "tool/AbstractObject.hpp" +#include +#include + +// ============================================================================= + +class ContentModel; + +class TemporaryFile : public QObject, public AbstractObject { + Q_OBJECT +public: + TemporaryFile(QObject *parent = nullptr); + ~TemporaryFile(); + + Q_PROPERTY(QString filePath READ getFilePath NOTIFY + filePathChanged) // not changeable from QML as it comes from a ContentModel + Q_PROPERTY(bool isReadable READ isReadable NOTIFY isReadableChanged) + + void createFileFromContent(std::shared_ptr content, const bool &exportPlainFile = true); + Q_INVOKABLE void createFile(const QString &filePath, const bool &exportPlainFile = true); + + QString getFilePath() const; + bool isReadable() const; + + void setFilePath(const QString &path, const bool &toDelete); + void setIsReadable(const bool &isReadable); + + void deleteFile(); + +signals: + void filePathChanged(); + void isReadableChanged(); + +private: + DECLARE_ABSTRACT_OBJECT + + QString mFilePath; + bool mDeleteFile = false; + bool mIsReadable = false; +}; + +#endif diff --git a/Linphone/view/CMakeLists.txt b/Linphone/view/CMakeLists.txt index 5e909404c..da253ba93 100644 --- a/Linphone/view/CMakeLists.txt +++ b/Linphone/view/CMakeLists.txt @@ -46,6 +46,7 @@ list(APPEND _LINPHONEAPP_QML_FILES view/Control/Display/Flickable.qml view/Control/Display/GradientRectangle.qml view/Control/Display/TemporaryText.qml + view/Control/Display/MediaProgressBar.qml view/Control/Display/ProgressBar.qml view/Control/Display/RoundedPane.qml view/Control/Display/RoundProgressBar.qml diff --git a/Linphone/view/Control/Display/Chat/ChatAudioContent.qml b/Linphone/view/Control/Display/Chat/ChatAudioContent.qml index 8726e5045..1a3be1191 100644 --- a/Linphone/view/Control/Display/Chat/ChatAudioContent.qml +++ b/Linphone/view/Control/Display/Chat/ChatAudioContent.qml @@ -11,117 +11,81 @@ Loader{ id: mainItem property ChatMessageContentGui chatMessageContentGui property int availableWidth : parent.width - property int width: active ? Math.max(availableWidth - ChatAudioMessageStyle.emptySpace, ChatAudioMessageStyle.minWidth) : 0 - property int fitHeight: active ? 60 : 0 - property font customFont : SettingsModel.textMessageFont - property bool isActive: active + // property string filePath : tempFile.filePath - property string filePath : tempFile.filePath + active: chatMessageContentGui && chatMessageContentGui.core.isVoiceRecording - active: chatMessageContentGui && chatMessageContentGui.core.isVoiceRecording() + // onChatMessageContentGuiChanged: if(chatMessageContentGui){ + // tempFile.createFileFromContentModel(chatMessageContentGui, false); + // } - onChatMessageContentGuiChanged: if(chatMessageContentGui){ - tempFile.createFileFromContentModel(chatMessageContentGui, false); - } + // TemporaryFile { + // id: tempFile + // } - TemporaryFile { - id: tempFile - } - - sourceComponent: Item{ + sourceComponent: Item { id: loadedItem - property bool isPlaying : vocalPlayer.item && vocalPlayer.item.playbackState === SoundPlayer.PlayingState + property bool isPlaying : soundPlayerGui && soundPlayerGui.core.playbackState === LinphoneEnums.PlaybackState.PlayingState onIsPlayingChanged: isPlaying ? mediaProgressBar.resume() : mediaProgressBar.stop() - width: availableWidth < 0 || availableWidth > mainItem.width ? mainItem.width : availableWidth - height: mainItem.fitHeight + width: mainItem.width + height: mainItem.height clip: false - Loader { - id: vocalPlayer - - active: false + + SoundPlayerGui { + id: soundPlayerGui + property int duration: mainItem.chatMessageContentGui ? mainItem.chatMessageContentGui.core.fileDuration : core.duration + property int position: core.position + source: mainItem.chatMessageContentGui && mainItem.chatMessageContentGui.core.filePath + function play(){ - if(!vocalPlayer.active) - vocalPlayer.active = true - else { - if(loadedItem.isPlaying){// Pause the play - vocalPlayer.item.pause() - }else{// Play the audio - vocalPlayer.item.play() - } + if(loadedItem.isPlaying){// Pause the play + soundPlayerGui.core.lPause() + }else{// Play the audio + soundPlayerGui.core.lPlay() } } - sourceComponent: SoundPlayer { - source: mainItem.chatMessageContentGui && mainItem.filePath - onStopped:{ - mediaProgressBar.value = 101 - } - Component.onCompleted: { - play()// This will open the file and allow seeking - pause() - mediaProgressBar.value = 0 - mediaProgressBar.refresh() - } + onStopped: { + mediaProgressBar.value = 101 + } + onPositionChanged: { + mediaProgressBar.progressPosition = position + mediaProgressBar.value = 100 * ( mediaProgressBar.progressPosition / duration) + } + onSourceChanged: if (source != "") { + // core.lPlay()// This will open the file and allow seeking + // core.lPause() + core.lOpen() + mediaProgressBar.value = 0 + mediaProgressBar.refresh() } - onStatusChanged: if (loader.status == Loader.Ready) play() } - RowLayout{ - id: lineLayout + + + MediaProgressBar{ + id: mediaProgressBar anchors.fill: parent - spacing: 5 - ActionButton{ - id: playButton - Layout.preferredHeight: iconSize - Layout.preferredWidth: iconSize - Layout.rightMargin: 5 - Layout.leftMargin: 15 - Layout.alignment: Qt.AlignVCenter - isCustom: true - backgroundRadius: width - colorSet: (loadedItem.isPlaying ? ChatAudioMessageStyle.pauseAction - : ChatAudioMessageStyle.playAction) - onClicked:{ - vocalPlayer.play() + progressDuration: soundPlayerGui ? soundPlayerGui.duration : chatMessageContentGui.core.fileDuration + progressPosition: 0 + value: 0 + function refresh(){ + if(soundPlayerGui){ + soundPlayerGui.core.lRefreshPosition() } } - Item{ - Layout.fillHeight: true - Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter - Layout.rightMargin: 10 - Layout.topMargin: 10 - Layout.bottomMargin: 10 - MediaProgressBar{ - id: mediaProgressBar - anchors.fill: parent - progressDuration: vocalPlayer.item ? vocalPlayer.item.duration : chatMessageContentGui.core.getFileDuration() - progressPosition: 0 - value: 0 - stopAtEnd: true - resetAtEnd: false - backgroundColor: ChatAudioMessageStyle.backgroundColor.color - colorSet: ChatAudioMessageStyle.progressionWave - function refresh(){ - if( vocalPlayer.item){ - progressPosition = vocalPlayer.item.getPosition() - value = 100 * ( progressPosition / vocalPlayer.item.duration) - } - } - onEndReached:{ - if(vocalPlayer.item) - vocalPlayer.item.stop() - } - onRefreshPositionRequested: refresh() - onSeekRequested: if( vocalPlayer.item){ - vocalPlayer.item.seek(ms) - progressPosition = vocalPlayer.item.getPosition() - value = 100 * (progressPosition / vocalPlayer.item.duration) - } + onEndReached:{ + if(soundPlayerGui) + soundPlayerGui.core.lStop() + } + onPlayStopButtonToggled: soundPlayerGui.play() + onRefreshPositionRequested: refresh() + onSeekRequested: (ms) => { + if(soundPlayerGui) { + soundPlayerGui.core.lSeek(ms) } } - } } } diff --git a/Linphone/view/Control/Display/Chat/ChatMessageContent.qml b/Linphone/view/Control/Display/Chat/ChatMessageContent.qml index ca0c80039..590ed7d4d 100644 --- a/Linphone/view/Control/Display/Chat/ChatMessageContent.qml +++ b/Linphone/view/Control/Display/Chat/ChatMessageContent.qml @@ -26,33 +26,23 @@ ColumnLayout { property int padding: Math.round(10 * DefaultStyle.dp) // VOICE MESSAGES - // ListView { - // id: messagesVoicesList - // width: parent.width-2*mainItem.padding - // visible: count > 0 - // spacing: 0 - // clip: false - // model: ChatMessageContentProxy { - // filterType: ChatMessageContentProxy.FilterContentType.Voice - // chatMessageGui: mainItem.chatMessageGui - // } - // height: contentHeight - // boundsBehavior: Flickable.StopAtBounds - // interactive: false - // function updateBestWidth(){ - // var newWidth = mainItem.updateListBestWidth(messagesVoicesList) - // mainItem.voicesCount = newWidth[0] - // mainItem.voicesBestWidth = newWidth[1] - // } - // delegate: ChatAudioMessage{ - // id: audioMessage - // contentModel: $modelData - // visible: contentModel - // z: 1 - // Component.onCompleted: messagesVoicesList.updateBestWidth() - // } - // Component.onCompleted: messagesVoicesList.updateBestWidth - // } + Repeater { + id: messagesVoicesList + visible: mainItem.chatMessageGui.core.isVoiceRecording && count > 0 + model: ChatMessageContentProxy{ + filterType: ChatMessageContentProxy.FilterContentType.Voice + chatMessageGui: mainItem.chatMessageGui + } + delegate: ChatAudioContent { + // Layout.fillWidth: true + width: Math.round(269 * DefaultStyle.dp) + height: Math.round(48 * DefaultStyle.dp) + Layout.preferredHeight: height + chatMessageContentGui: modelData + // width: conferenceList.width + // onMouseEvent: (event) => mainItem.mouseEvent(event) + } + } // CONFERENCE Repeater { id: conferenceList diff --git a/Linphone/view/Control/Display/MediaProgressBar.qml b/Linphone/view/Control/Display/MediaProgressBar.qml new file mode 100644 index 000000000..78323702c --- /dev/null +++ b/Linphone/view/Control/Display/MediaProgressBar.qml @@ -0,0 +1,165 @@ +import QtQuick +import QtQuick.Controls as Control +import QtQuick.Layouts +import QtQuick.Shapes +import QtQuick.Effects +import Qt5Compat.GraphicalEffects + +import Linphone +import UtilsCpp + +import 'qrc:/qt/qml/Linphone/view/Style/buttonStyle.js' as ButtonStyle + +// ============================================================================= + +ProgressBar { + id: mainItem + + property bool stopAtEnd: true + property bool resetAtEnd: true + property int progressDuration // Max duration + property int progressPosition // Position of progress bar in [0 ; progressDuration] + property bool blockValueAtEnd: true + + property bool recording: false + padding: 0 + clip: true + + function start(){ + mainItem.value = 0 + animationTest.start() + } + function resume(){ + if(mainItem.value >= 100) + mainItem.value = 0 + animationTest.start() + } + function stop(){ + animationTest.stop() + } + signal playStopButtonToggled() + signal endReached() + signal refreshPositionRequested() + signal seekRequested(int ms) + Timer{ + id: animationTest + repeat: true + onTriggered: mainItem.refreshPositionRequested() + interval: 5 + } + to: 101 + value: progressPosition * to / progressDuration + onValueChanged:{ + if(value > 100) { + if( mainItem.stopAtEnd) + stop() + if(mainItem.resetAtEnd) { + mainItem.value = 0 + progressPosition = 0 + } else if(mainItem.blockValueAtEnd){ + mainItem.value = 100// Stay at 100 + progressPosition = progressDuration + } + console.log("end reached") + mainItem.endReached() + } + } + + background: Item { + anchors.fill: parent + Rectangle { + id: backgroundArea + anchors.fill: parent + gradient: Gradient { + orientation: Gradient.Horizontal + GradientStop { position: 0.0; color: "#FF9E79" } + GradientStop { position: 1.0; color: "#FE5E00" } + } + radius: Math.round(70 * DefaultStyle.dp) + } + Rectangle { + id: mask + anchors.fill: parent + visible: false + radius: backgroundArea.radius + } + Item { + anchors.fill: parent + id: progressRectangle + visible: false + Rectangle { + color: DefaultStyle.grey_0 + width: mainItem.barWidth + height: backgroundArea.height + opacity: 0.5 + } + } + OpacityMask { + anchors.fill: progressRectangle + source: progressRectangle + maskSource: mask + } + + MouseArea { + id: progression + anchors.fill: parent + onClicked: (mouse) => { + mainItem.seekRequested(mouse.x * mainItem.progressDuration/width) + } + } + } + + + contentItem: Item { + id: contentRect + + RoundButton { + z: parent.z + 1 + anchors.left: parent.left + anchors.leftMargin: Math.round(9 * DefaultStyle.dp) + anchors.verticalCenter: parent.verticalCenter + icon.width: Math.round(14 * DefaultStyle.dp) + icon.height: Math.round(14 * DefaultStyle.dp) + icon.source: animationTest.running + ? mainItem.recording + ? AppIcons.stopFill + : AppIcons.pauseFill + : AppIcons.playFill + onClicked: { + mainItem.playStopButtonToggled() + } + borderColor: "transparent" + style: ButtonStyle.secondary + } + Control.Control { + anchors.right: parent.right + anchors.rightMargin: Math.round(9 * DefaultStyle.dp) + anchors.verticalCenter: parent.verticalCenter + leftPadding: Math.round(18 * DefaultStyle.dp) + rightPadding: Math.round(18 * DefaultStyle.dp) + topPadding: Math.round(5 * DefaultStyle.dp) + bottomPadding: Math.round(5 * DefaultStyle.dp) + background: Rectangle { + anchors.fill: parent + color: DefaultStyle.grey_0 + radius: Math.round(50 * DefaultStyle.dp) + } + contentItem: RowLayout { + spacing: mainItem.recording ? Math.round(5 * DefaultStyle.dp) : 0 + EffectImage { + visible: mainItem.recording + colorizationColor: DefaultStyle.danger_500main + imageSource: AppIcons.recordFill + } + Text { + id: durationText + text: mainItem.progressPosition > 0 ? UtilsCpp.formatElapsedTime(mainItem.progressPosition / 1000 ) : UtilsCpp.formatElapsedTime(mainItem.progressDuration/1000) + font { + pixelSize: Typography.p1.pixelSize + weight: Typography.p1.weight + } + } + } + } + } +} diff --git a/Linphone/view/Control/Display/ProgressBar.qml b/Linphone/view/Control/Display/ProgressBar.qml index 19fb799e1..5fd61c216 100644 --- a/Linphone/view/Control/Display/ProgressBar.qml +++ b/Linphone/view/Control/Display/ProgressBar.qml @@ -1,8 +1,8 @@ import QtQuick -import QtQuick.Controls.Basic +import QtQuick.Controls.Basic as Control import Linphone -ProgressBar { +Control.ProgressBar { id: mainItem padding: Math.round(3 * DefaultStyle.dp) diff --git a/Linphone/view/Control/Input/Chat/ChatDroppableTextArea.qml b/Linphone/view/Control/Input/Chat/ChatDroppableTextArea.qml index 6ef11ae5b..972288629 100644 --- a/Linphone/view/Control/Input/Chat/ChatDroppableTextArea.qml +++ b/Linphone/view/Control/Input/Chat/ChatDroppableTextArea.qml @@ -111,7 +111,6 @@ Control.Control { Flickable { id: sendingAreaFlickable Layout.fillWidth: true - Layout.preferredWidth: parent.width - stackButton.width Layout.preferredHeight: Math.min(Math.round(60 * DefaultStyle.dp), contentHeight) Binding { target: sendingAreaFlickable @@ -159,10 +158,11 @@ Control.Control { } } } - StackLayout { + RowLayout { id: stackButton - currentIndex: sendingTextArea.text.length === 0 ? 0 : 1 + spacing: 0 BigButton { + visible: sendingTextArea.text.length === 0 style: ButtonStyle.noBackground icon.source: AppIcons.microphone onClicked: { @@ -170,6 +170,7 @@ Control.Control { } } BigButton { + visible: sendingTextArea.text.length !== 0 style: ButtonStyle.noBackgroundOrange icon.source: AppIcons.paperPlaneRight onClicked: { diff --git a/Linphone/view/Page/Main/Contact/ContactPage.qml b/Linphone/view/Page/Main/Contact/ContactPage.qml index 0878a9591..2ef904f46 100644 --- a/Linphone/view/Page/Main/Contact/ContactPage.qml +++ b/Linphone/view/Page/Main/Contact/ContactPage.qml @@ -863,11 +863,11 @@ FriendGui{ ContactEdition { property string objectName: "contactEdition" onCloseEdition: redirectAddress => { - goToContactDetails() - if (redirectAddress) { - initialFriendToDisplay = redirectAddress - } - } + goToContactDetails() + if (redirectAddress) { + initialFriendToDisplay = redirectAddress + } + } } } } diff --git a/Linphone/view/Style/AppIcons.qml b/Linphone/view/Style/AppIcons.qml index d55d0acf9..cb165ad0e 100644 --- a/Linphone/view/Style/AppIcons.qml +++ b/Linphone/view/Style/AppIcons.qml @@ -74,8 +74,10 @@ QtObject { property string trustedMask: "image://internal/trusted-mask.svg" property string avatar: "image://internal/randomAvatar.png" property string pause: "image://internal/pause.svg" + property string pauseFill: "image://internal/pause-fill.svg" property string play: "image://internal/play.svg" property string playFill: "image://internal/play-fill.svg" + property string stopFill: "image://internal/stop-filled.svg" property string filePdf: "image://internal/file-pdf.svg" property string fileText: "image://internal/file-text.svg" property string fileImage: "image://internal/file-image.svg"