diff --git a/CMakeLists.txt b/CMakeLists.txt index 51888ab72..ec73ec796 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -263,16 +263,16 @@ else() include(cmake/TasksMacos.cmake) endif() -if (ENABLE_QT_KEYCHAIN) - target_include_directories(${TARGET_NAME} SYSTEM PUBLIC ${PROJECT_SOURCE_DIR}/external/qtkeychain) - target_link_libraries(${TARGET_NAME} PUBLIC ${QTKEYCHAIN_TARGET_NAME}) - message(STATUS "link libraries: ${TARGET_NAME} ${QTKEYCHAIN_TARGET_NAME}") -endif() +# if (ENABLE_QT_KEYCHAIN) +# target_include_directories(${TARGET_NAME} SYSTEM PUBLIC ${PROJECT_SOURCE_DIR}/external/qtkeychain) +# target_link_libraries(${TARGET_NAME} PUBLIC ${QTKEYCHAIN_TARGET_NAME}) +# message(STATUS "link libraries: ${TARGET_NAME} ${QTKEYCHAIN_TARGET_NAME}") +# message(STATUS "CMAKE_CURRENT_SOURCE_DIR: ${CMAKE_CURRENT_SOURCE_DIR}") +# message(STATUS "PROJECT_SOURCE_DIR: ${PROJECT_SOURCE_DIR}") +# message(STATUS "Contents of qtkeychain:") +# file(GLOB QTKEYCHAIN_HEADERS "${PROJECT_SOURCE_DIR}/external/qtkeychain/qtkeychain/*.h") +# message(STATUS "Found headers: ${QTKEYCHAIN_HEADERS}") +# endif() -message(STATUS "CMAKE_CURRENT_SOURCE_DIR: ${CMAKE_CURRENT_SOURCE_DIR}") -message(STATUS "PROJECT_SOURCE_DIR: ${PROJECT_SOURCE_DIR}") -message(STATUS "Contents of qtkeychain:") -file(GLOB QTKEYCHAIN_HEADERS "${PROJECT_SOURCE_DIR}/external/qtkeychain/qtkeychain/*.h") -message(STATUS "Found headers: ${QTKEYCHAIN_HEADERS}") file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/cmake/hook/pre-commit" DESTINATION "${CMAKE_CURRENT_SOURCE_DIR}/.git/hooks/") diff --git a/Linphone/core/App.cpp b/Linphone/core/App.cpp index 6f18d7dd5..945d6273d 100644 --- a/Linphone/core/App.cpp +++ b/Linphone/core/App.cpp @@ -54,10 +54,12 @@ #include "core/call/CallProxy.hpp" #include "core/camera/CameraGui.hpp" #include "core/chat/ChatProxy.hpp" -#include "core/chat/message/EventLogGui.hpp" #include "core/chat/message/ChatMessageGui.hpp" +#include "core/chat/message/EventLogGui.hpp" #include "core/chat/message/EventLogList.hpp" #include "core/chat/message/EventLogProxy.hpp" +#include "core/chat/message/content/ChatMessageContentGui.hpp" +#include "core/chat/message/content/ChatMessageContentProxy.hpp" #include "core/conference/ConferenceGui.hpp" #include "core/conference/ConferenceInfoGui.hpp" #include "core/conference/ConferenceInfoProxy.hpp" @@ -96,9 +98,11 @@ #include "tool/providers/EmojiProvider.hpp" #include "tool/providers/ImageProvider.hpp" #include "tool/providers/ScreenProvider.hpp" +#include "tool/providers/ThumbnailProvider.hpp" #include "tool/request/CallbackHelper.hpp" #include "tool/request/RequestDialog.hpp" #include "tool/thread/Thread.hpp" +#include "tool/ui/DashRectangle.hpp" DEFINE_ABSTRACT_OBJECT(App) @@ -495,6 +499,7 @@ void App::initCore() { mEngine->addImageProvider(ScreenProvider::ProviderId, new ScreenProvider()); mEngine->addImageProvider(WindowProvider::ProviderId, new WindowProvider()); mEngine->addImageProvider(WindowIconProvider::ProviderId, new WindowIconProvider()); + mEngine->addImageProvider(ThumbnailProvider::ProviderId, new ThumbnailProvider()); // Enable notifications. mNotifier = new Notifier(mEngine); @@ -551,7 +556,7 @@ void App::initCore() { static bool firstOpen = true; if (!firstOpen || !mParser->isSet("minimized")) { lDebug() << log().arg("Openning window"); - window->show(); + if (window) window->show(); } else lInfo() << log().arg("Stay minimized"); firstOpen = false; } @@ -639,6 +644,7 @@ void App::initCppInterfaces() { "SettingsCpp", 1, 0, "SettingsCpp", [this](QQmlEngine *engine, QJSEngine *) -> QObject * { return mSettings.get(); }); + qmlRegisterType(Constants::MainQmlUri, 1, 0, "DashRectangle"); qmlRegisterType(Constants::MainQmlUri, 1, 0, "PhoneNumberProxy"); qmlRegisterType(Constants::MainQmlUri, 1, 0, "VariantObject"); qmlRegisterType(Constants::MainQmlUri, 1, 0, "VariantList"); @@ -663,9 +669,11 @@ void App::initCppInterfaces() { qmlRegisterType(Constants::MainQmlUri, 1, 0, "ChatProxy"); qmlRegisterType(Constants::MainQmlUri, 1, 0, "ChatGui"); qmlRegisterType(Constants::MainQmlUri, 1, 0, "EventLogGui"); - qmlRegisterType(Constants::MainQmlUri, 1, 0, "ChatMessageGui"); + qmlRegisterType(Constants::MainQmlUri, 1, 0, "ChatMessageGui"); qmlRegisterType(Constants::MainQmlUri, 1, 0, "EventLogList"); qmlRegisterType(Constants::MainQmlUri, 1, 0, "EventLogProxy"); + qmlRegisterType(Constants::MainQmlUri, 1, 0, "ChatMessageContentProxy"); + qmlRegisterType(Constants::MainQmlUri, 1, 0, "ChatMessageContentGui"); qmlRegisterUncreatableType(Constants::MainQmlUri, 1, 0, "ConferenceCore", QLatin1String("Uncreatable")); qmlRegisterType(Constants::MainQmlUri, 1, 0, "ConferenceGui"); @@ -1284,3 +1292,11 @@ QString App::getSdkVersion() { return tr('unknown'); #endif } + +float App::getScreenRatio() const { + return mScreenRatio; +} + +void App::setScreenRatio(float ratio) { + mScreenRatio = ratio; +} \ No newline at end of file diff --git a/Linphone/core/App.hpp b/Linphone/core/App.hpp index 2655b220a..1b4bf5b43 100644 --- a/Linphone/core/App.hpp +++ b/Linphone/core/App.hpp @@ -154,6 +154,9 @@ public: QString getGitBranchName(); QString getSdkVersion(); + float getScreenRatio() const; + Q_INVOKABLE void setScreenRatio(float ratio); + #ifdef Q_OS_LINUX Q_INVOKABLE void exportDesktopFile(); @@ -200,6 +203,7 @@ private: DefaultTranslatorCore *mDefaultTranslatorCore = nullptr; QTimer mDateUpdateTimer; QDate mCurrentDate; + float mScreenRatio = 1; DECLARE_ABSTRACT_OBJECT }; diff --git a/Linphone/core/CMakeLists.txt b/Linphone/core/CMakeLists.txt index e449cfba4..e95725b88 100644 --- a/Linphone/core/CMakeLists.txt +++ b/Linphone/core/CMakeLists.txt @@ -29,6 +29,10 @@ list(APPEND _LINPHONEAPP_SOURCES core/chat/message/EventLogGui.cpp core/chat/message/EventLogList.cpp core/chat/message/EventLogProxy.cpp + core/chat/message/content/ChatMessageContentCore.cpp + core/chat/message/content/ChatMessageContentGui.cpp + core/chat/message/content/ChatMessageContentList.cpp + core/chat/message/content/ChatMessageContentProxy.cpp core/emoji/EmojiModel.cpp core/fps-counter/FPSCounter.cpp core/friend/FriendCore.cpp diff --git a/Linphone/core/chat/message/ChatMessageCore.cpp b/Linphone/core/chat/message/ChatMessageCore.cpp index 6e8094d18..c69091805 100644 --- a/Linphone/core/chat/message/ChatMessageCore.cpp +++ b/Linphone/core/chat/message/ChatMessageCore.cpp @@ -68,10 +68,11 @@ ChatMessageCore::ChatMessageCore(const std::shared_ptr &c App::getInstance()->mEngine->setObjectOwnership(this, QQmlEngine::CppOwnership); mChatMessageModel = Utils::makeQObject_ptr(chatmessage); mChatMessageModel->setSelf(mChatMessageModel); - mText = mChatMessageModel->getText(); + mText = ToolModel::getMessageFromContent(chatmessage->getContents()); mUtf8Text = mChatMessageModel->getUtf8Text(); mHasTextContent = mChatMessageModel->getHasTextContent(); mTimestamp = QDateTime::fromSecsSinceEpoch(chatmessage->getTime()); + mIsOutgoing = chatmessage->isOutgoing(); mIsRemoteMessage = !chatmessage->isOutgoing(); mPeerAddress = Utils::coreStringToAppString(chatmessage->getPeerAddress()->asStringUriOnly()); mPeerName = ToolModel::getDisplayName(chatmessage->getPeerAddress()->clone()); @@ -87,10 +88,8 @@ ChatMessageCore::ChatMessageCore(const std::shared_ptr &c mMessageState = LinphoneEnums::fromLinphone(chatmessage->getState()); mMessageId = Utils::coreStringToAppString(chatmessage->getMessageId()); for (auto content : chatmessage->getContents()) { - if (content->isIcalendar()) { - auto conferenceInfo = linphone::Factory::get()->createConferenceInfoFromIcalendarContent(content); - mConferenceInfo = ConferenceInfoCore::create(conferenceInfo); - } + auto contentCore = ChatMessageContentCore::create(content, mChatMessageModel); + mChatMessageContentList.push_back(contentCore); } auto reac = chatmessage->getOwnReaction(); mOwnReaction = reac ? Utils::coreStringToAppString(reac->getBody()) : QString(); @@ -201,6 +200,61 @@ void ChatMessageCore::setSelf(QSharedPointer me) { auto msgState = LinphoneEnums::fromLinphone(state); mChatMessageModelConnection->invokeToCore([this, msgState] { setMessageState(msgState); }); }); + mChatMessageModelConnection->makeConnectToModel( + &ChatMessageModel::fileTransferProgressIndication, + [this](const std::shared_ptr &message, const std::shared_ptr &content, + size_t offset, size_t total) { + mChatMessageModelConnection->invokeToCore([this, content, offset, total] { + auto it = + std::find_if(mChatMessageContentList.begin(), mChatMessageContentList.end(), + [content](QSharedPointer item) { + return item->getContentModel()->getContent()->getName() == content->getName(); + }); + if (it != mChatMessageContentList.end()) { + auto contentCore = mChatMessageContentList.at(std::distance(mChatMessageContentList.begin(), it)); + assert(contentCore); + contentCore->setFileOffset(offset); + } + }); + }); + + mChatMessageModelConnection->makeConnectToModel( + &ChatMessageModel::fileTransferTerminated, [this](const std::shared_ptr &message, + const std::shared_ptr &content) { + mChatMessageModelConnection->invokeToCore([this, content] { + auto it = + std::find_if(mChatMessageContentList.begin(), mChatMessageContentList.end(), + [content](QSharedPointer item) { + return item->getContentModel()->getContent()->getName() == content->getName(); + }); + if (it != mChatMessageContentList.end()) { + auto contentCore = mChatMessageContentList.at(std::distance(mChatMessageContentList.begin(), it)); + assert(contentCore); + contentCore->setWasDownloaded(true); + } + }); + }); + mChatMessageModelConnection->makeConnectToModel( + &ChatMessageModel::fileTransferRecv, + [this](const std::shared_ptr &message, const std::shared_ptr &content, + const std::shared_ptr &buffer) { qDebug() << "transfer received"; }); + mChatMessageModelConnection->makeConnectToModel( + &ChatMessageModel::fileTransferSend, + [this](const std::shared_ptr &message, const std::shared_ptr &content, + size_t offset, size_t size) { qDebug() << "transfer send"; }); + mChatMessageModelConnection->makeConnectToModel( + &ChatMessageModel::fileTransferSendChunk, + [this](const std::shared_ptr &message, const std::shared_ptr &content, + size_t offset, size_t size, + const std::shared_ptr &buffer) { qDebug() << "transfer send chunk"; }); + mChatMessageModelConnection->makeConnectToModel( + &ChatMessageModel::participantImdnStateChanged, + [this](const std::shared_ptr &message, + const std::shared_ptr &state) {}); + mChatMessageModelConnection->makeConnectToModel(&ChatMessageModel::ephemeralMessageTimerStarted, + [this](const std::shared_ptr &message) {}); + mChatMessageModelConnection->makeConnectToModel(&ChatMessageModel::ephemeralMessageDeleted, + [this](const std::shared_ptr &message) {}); } QDateTime ChatMessageCore::getTimestamp() const { @@ -286,6 +340,10 @@ QList ChatMessageCore::getReactionsSingleton() const { return mReactionsSingletonMap; } +QList> ChatMessageCore::getChatMessageContentList() const { + return mChatMessageContentList; +} + void ChatMessageCore::setReactions(const QList &reactions) { mustBeInMainThread(log().arg(Q_FUNC_INFO)); mReactions = reactions; @@ -370,6 +428,6 @@ std::shared_ptr ChatMessageCore::getModel() const { return mChatMessageModel; } -ConferenceInfoGui *ChatMessageCore::getConferenceInfoGui() const { - return mConferenceInfo ? new ConferenceInfoGui(mConferenceInfo) : nullptr; -} +// ConferenceInfoGui *ChatMessageCore::getConferenceInfoGui() const { +// return mConferenceInfo ? new ConferenceInfoGui(mConferenceInfo) : nullptr; +// } diff --git a/Linphone/core/chat/message/ChatMessageCore.hpp b/Linphone/core/chat/message/ChatMessageCore.hpp index 4d408a1bd..c61666912 100644 --- a/Linphone/core/chat/message/ChatMessageCore.hpp +++ b/Linphone/core/chat/message/ChatMessageCore.hpp @@ -22,6 +22,8 @@ #define CHATMESSAGECORE_H_ #include "EventLogCore.hpp" +#include "core/chat/message/content/ChatMessageContentCore.hpp" +#include "core/chat/message/content/ChatMessageContentProxy.hpp" #include "core/conference/ConferenceInfoCore.hpp" #include "core/conference/ConferenceInfoGui.hpp" #include "model/chat/message/ChatMessageModel.hpp" @@ -67,7 +69,6 @@ class ChatMessageCore : public QObject, public AbstractObject { Q_PROPERTY(bool isRemoteMessage READ isRemoteMessage CONSTANT) Q_PROPERTY(bool isFromChatGroup READ isFromChatGroup CONSTANT) Q_PROPERTY(bool isRead READ isRead WRITE setIsRead NOTIFY isReadChanged) - Q_PROPERTY(ConferenceInfoGui *conferenceInfo READ getConferenceInfoGui CONSTANT) Q_PROPERTY(QString ownReaction READ getOwnReaction WRITE setOwnReaction NOTIFY messageReactionChanged) Q_PROPERTY(QList reactions READ getReactions WRITE setReactions NOTIFY messageReactionChanged) Q_PROPERTY(QList reactionsSingleton READ getReactionsSingleton NOTIFY singletonReactionMapChanged) @@ -106,6 +107,7 @@ public: void setOwnReaction(const QString &reaction); QList getReactions() const; QList getReactionsSingleton() const; + QList> getChatMessageContentList() const; void removeOneReactionFromSingletonMap(const QString &body); void resetReactionsSingleton(); void setReactions(const QList &reactions); @@ -116,7 +118,7 @@ public: void setMessageState(LinphoneEnums::ChatMessageState state); std::shared_ptr getModel() const; - ConferenceInfoGui *getConferenceInfoGui() const; + // ConferenceInfoGui *getConferenceInfoGui() const; signals: void timestampChanged(QDateTime timestamp); @@ -136,7 +138,8 @@ signals: void lRemoveReaction(); private: - DECLARE_ABSTRACT_OBJECT QString mText; + DECLARE_ABSTRACT_OBJECT + QString mText; QString mUtf8Text; bool mHasTextContent; QString mPeerAddress; @@ -158,8 +161,10 @@ private: bool mIsCalendarInvite = false; bool mIsVoiceRecording = false; + bool mIsOutgoing = false; LinphoneEnums::ChatMessageState mMessageState; - QSharedPointer mConferenceInfo = nullptr; + QList> mChatMessageContentList; + // QSharedPointer mConferenceInfo = nullptr; std::shared_ptr mChatMessageModel; QSharedPointer> mChatMessageModelConnection; diff --git a/Linphone/core/chat/message/content/ChatMessageContentCore.cpp b/Linphone/core/chat/message/content/ChatMessageContentCore.cpp new file mode 100644 index 000000000..944f5edee --- /dev/null +++ b/Linphone/core/chat/message/content/ChatMessageContentCore.cpp @@ -0,0 +1,259 @@ +/* + * 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 "ChatMessageContentCore.hpp" +#include "core/App.hpp" +#include "core/chat/ChatCore.hpp" +#include "model/tool/ToolModel.hpp" +#include "tool/providers/ThumbnailProvider.hpp" + +DEFINE_ABSTRACT_OBJECT(ChatMessageContentCore) + +QSharedPointer +ChatMessageContentCore::create(const std::shared_ptr &content, + std::shared_ptr chatMessageModel) { + auto sharedPointer = QSharedPointer(new ChatMessageContentCore(content, chatMessageModel), + &QObject::deleteLater); + sharedPointer->setSelf(sharedPointer); + sharedPointer->moveToThread(App::getInstance()->thread()); + return sharedPointer; +} + +ChatMessageContentCore::ChatMessageContentCore(const std::shared_ptr &content, + std::shared_ptr chatMessageModel) { + if (content) { + mName = Utils::coreStringToAppString(content->getName()); + if (mName.isEmpty()) { // Try to find the name from file Path + QString fileName = Utils::coreStringToAppString(content->getFilePath()); + if (!fileName.isEmpty()) { + mName = QFileInfo(fileName).baseName(); + } + } + mFilePath = Utils::coreStringToAppString(content->getFilePath()); + mIsFile = content->isFile(); + mIsFileEncrypted = content->isFileEncrypted(); + mIsFileTransfer = content->isFileTransfer(); + mIsCalendar = content->isIcalendar(); + if (content->isIcalendar()) { + auto conferenceInfo = linphone::Factory::get()->createConferenceInfoFromIcalendarContent(content); + mConferenceInfo = ConferenceInfoCore::create(conferenceInfo); + } + mIsMultipart = content->isMultipart(); + mIsText = content->isText(); + mIsVoiceRecording = content->isVoiceRecording(); + mIsVideo = Utils::isVideo(mFilePath); + mFileSize = (quint64)content->getFileSize(); + mFileDuration = content->getFileDuration(); + mFileOffset = 0; + mUtf8Text = Utils::coreStringToAppString(content->getUtf8Text()); + mWasDownloaded = !mFilePath.isEmpty() && QFileInfo(mFilePath).isFile(); + mThumbnail = mFilePath.isEmpty() + ? QString() + : QStringLiteral("image://%1/%2").arg(ThumbnailProvider::ProviderId).arg(mFilePath); + mChatMessageContentModel = Utils::makeQObject_ptr(content, chatMessageModel); + } +} + +ChatMessageContentCore ::~ChatMessageContentCore() { +} + +void ChatMessageContentCore::setSelf(QSharedPointer me) { + mChatMessageContentModelConnection = + SafeConnection::create(me, mChatMessageContentModel); + + auto updateThumbnailType = [this] { + if (Utils::isVideo(mFilePath)) mIsVideo = true; + emit isVideoChanged(); + }; + + mChatMessageContentModelConnection->makeConnectToCore( + &ChatMessageContentCore::lCreateThumbnail, [this](const bool &force = false) { + mChatMessageContentModelConnection->invokeToModel( + [this, force] { mChatMessageContentModel->createThumbnail(); }); + }); + mChatMessageContentModelConnection->makeConnectToModel( + &ChatMessageContentModel::thumbnailChanged, [this, updateThumbnailType](QString thumbnail) { + mChatMessageContentModelConnection->invokeToCore([this, thumbnail] { setThumbnail(thumbnail); }); + }); + + mChatMessageContentModelConnection->makeConnectToCore(&ChatMessageContentCore::lDownloadFile, [this]() { + mChatMessageContentModelConnection->invokeToModel([this] { mChatMessageContentModel->downloadFile(mName); }); + }); + mChatMessageContentModelConnection->makeConnectToModel( + &ChatMessageContentModel::wasDownloadedChanged, + [this](const std::shared_ptr &content, bool downloaded) { + mChatMessageContentModelConnection->invokeToCore([this, downloaded] { setWasDownloaded(downloaded); }); + }); + mChatMessageContentModelConnection->makeConnectToModel( + &ChatMessageContentModel::filePathChanged, + [this](const std::shared_ptr &content, QString filePath) { + auto isFile = content->isFile(); + auto isFileTransfer = content->isFileTransfer(); + auto isFileEncrypted = content->isFileEncrypted(); + mChatMessageContentModelConnection->invokeToCore([this, filePath, isFile, isFileTransfer, isFileEncrypted] { + setIsFile(isFile || QFileInfo(filePath).isFile()); + setIsFileTransfer(isFileTransfer); + setIsFileEncrypted(isFileEncrypted); + setFilePath(filePath); + }); + }); + + mChatMessageContentModelConnection->makeConnectToCore(&ChatMessageContentCore::lCancelDownloadFile, [this]() { + mChatMessageContentModelConnection->invokeToModel([this] { mChatMessageContentModel->cancelDownloadFile(); }); + }); + mChatMessageContentModelConnection->makeConnectToCore( + &ChatMessageContentCore::lOpenFile, [this](bool showDirectory = false) { + if (!QFileInfo(mFilePath).exists()) { + //: Error + Utils::showInformationPopup(tr("popup_error_title"), + //: Could not open file : unknown path %1 + tr("popup_open_file_error_does_not_exist_message").arg(mFilePath), false); + } else { + mChatMessageContentModelConnection->invokeToModel([this, showDirectory] { + mChatMessageContentModel->openFile(mName, mWasDownloaded, showDirectory); + }); + } + }); + mChatMessageContentModelConnection->makeConnectToModel( + &ChatMessageContentModel::messageStateChanged, [this](linphone::ChatMessage::State state) { + mChatMessageContentModelConnection->invokeToCore( + [this, msgState = LinphoneEnums::fromLinphone(state)] { emit msgStateChanged(msgState); }); + }); +} + +bool ChatMessageContentCore::isFile() const { + return mIsFile; +} + +void ChatMessageContentCore::setIsFile(bool isFile) { + if (mIsFile != isFile) { + mIsFile = isFile; + emit isFileChanged(); + } +} + +bool ChatMessageContentCore::isVideo() const { + return mIsVideo; +} + +bool ChatMessageContentCore::isFileEncrypted() const { + return mIsFileEncrypted; +} + +void ChatMessageContentCore::setIsFileEncrypted(bool isFileEncrypted) { + if (mIsFileEncrypted != isFileEncrypted) { + mIsFileEncrypted = isFileEncrypted; + emit isFileEncryptedChanged(); + } +} + +bool ChatMessageContentCore::isFileTransfer() const { + return mIsFileTransfer; +} + +void ChatMessageContentCore::setIsFileTransfer(bool isFileTransfer) { + if (mIsFileTransfer != isFileTransfer) { + mIsFileTransfer = isFileTransfer; + emit isFileTransferChanged(); + } +} + +bool ChatMessageContentCore::isCalendar() const { + return mIsCalendar; +} + +bool ChatMessageContentCore::isMultipart() const { + return mIsMultipart; +} + +bool ChatMessageContentCore::isText() const { + return mIsText; +} + +bool ChatMessageContentCore::isVoiceRecording() const { + return mIsVoiceRecording; +} + +QString ChatMessageContentCore::getFilePath() const { + return mFilePath; +} + +void ChatMessageContentCore::setFilePath(QString path) { + if (mFilePath != path) { + mFilePath = path; + emit filePathChanged(); + } +} + +QString ChatMessageContentCore::getUtf8Text() const { + return mUtf8Text; +} + +QString ChatMessageContentCore::getName() const { + return mName; +} + +quint64 ChatMessageContentCore::getFileSize() const { + return mFileSize; +} + +quint64 ChatMessageContentCore::getFileOffset() const { + return mFileOffset; +} + +void ChatMessageContentCore::setFileOffset(quint64 fileOffset) { + if (mFileOffset != fileOffset) { + mFileOffset = fileOffset; + emit fileOffsetChanged(); + } +} + +int ChatMessageContentCore::getFileDuration() const { + return mFileDuration; +} + +ConferenceInfoGui *ChatMessageContentCore::getConferenceInfoGui() const { + return mConferenceInfo ? new ConferenceInfoGui(mConferenceInfo) : nullptr; +} + +bool ChatMessageContentCore::wasDownloaded() const { + return mWasDownloaded; +} + +QString ChatMessageContentCore::getThumbnail() const { + return mThumbnail; +} + +void ChatMessageContentCore::setThumbnail(const QString &data) { + if (mThumbnail != data) { + mThumbnail = data; + emit thumbnailChanged(); + } +} +void ChatMessageContentCore::setWasDownloaded(bool wasDownloaded) { + if (mWasDownloaded != wasDownloaded) { + mWasDownloaded = wasDownloaded; + emit wasDownloadedChanged(wasDownloaded); + } +} + +const std::shared_ptr &ChatMessageContentCore::getContentModel() const { + return mChatMessageContentModel; +} diff --git a/Linphone/core/chat/message/content/ChatMessageContentCore.hpp b/Linphone/core/chat/message/content/ChatMessageContentCore.hpp new file mode 100644 index 000000000..d60832fdc --- /dev/null +++ b/Linphone/core/chat/message/content/ChatMessageContentCore.hpp @@ -0,0 +1,136 @@ +/* + * 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 CHAT_MESSAGE_CONTENT_CORE_H_ +#define CHAT_MESSAGE_CONTENT_CORE_H_ + +#include "core/conference/ConferenceInfoCore.hpp" +#include "core/conference/ConferenceInfoGui.hpp" +#include "model/chat/message/content/ChatMessageContentModel.hpp" +#include "tool/AbstractObject.hpp" +#include "tool/thread/SafeConnection.hpp" +#include +#include + +#include + +class ChatMessageContentCore : public QObject, public AbstractObject { + Q_OBJECT + Q_PROPERTY(QString name READ getName CONSTANT) + Q_PROPERTY(quint64 fileOffset READ getFileOffset WRITE setFileOffset NOTIFY fileOffsetChanged) + + Q_PROPERTY(QString thumbnail READ getThumbnail WRITE setThumbnail NOTIFY thumbnailChanged) + Q_PROPERTY(bool wasDownloaded READ wasDownloaded WRITE setWasDownloaded NOTIFY wasDownloadedChanged) + Q_PROPERTY(QString filePath READ getFilePath WRITE setFilePath NOTIFY filePathChanged) + Q_PROPERTY(QString utf8Text READ getUtf8Text CONSTANT) + Q_PROPERTY(bool isFile READ isFile WRITE setIsFile NOTIFY isFileChanged) + Q_PROPERTY(bool isFileEncrypted READ isFileEncrypted WRITE setIsFileEncrypted NOTIFY isFileEncryptedChanged) + Q_PROPERTY(bool isFileTransfer READ isFileTransfer WRITE setIsFileTransfer NOTIFY isFileTransferChanged) + Q_PROPERTY(bool isCalendar READ isCalendar CONSTANT) + Q_PROPERTY(ConferenceInfoGui *conferenceInfo READ getConferenceInfoGui CONSTANT) + Q_PROPERTY(bool isMultipart READ isMultipart CONSTANT) + Q_PROPERTY(bool isText READ isText CONSTANT) + Q_PROPERTY(bool isVideo READ isVideo NOTIFY isVideoChanged) + Q_PROPERTY(bool isVoiceRecording READ isVoiceRecording CONSTANT) + Q_PROPERTY(int fileDuration READ getFileDuration CONSTANT) + Q_PROPERTY(quint64 fileSize READ getFileSize CONSTANT) + +public: + static QSharedPointer create(const std::shared_ptr &content, + std::shared_ptr chatMessageModel); + ChatMessageContentCore(const std::shared_ptr &content, + std::shared_ptr chatMessageModel); + ~ChatMessageContentCore(); + void setSelf(QSharedPointer me); + + bool isFile() const; + void setIsFile(bool isFile); + bool isFileEncrypted() const; + void setIsFileEncrypted(bool isFileEncrypted); + bool isFileTransfer() const; + void setIsFileTransfer(bool isFileTransfer); + + bool isVideo() const; + bool isCalendar() const; + bool isMultipart() const; + bool isText() const; + bool isVoiceRecording() const; + + QString getUtf8Text() const; + QString getName() const; + quint64 getFileSize() const; + quint64 getFileOffset() const; + void setFileOffset(quint64 fileOffset); + QString getFilePath() const; + void setFilePath(QString path); + int getFileDuration() const; + ConferenceInfoGui *getConferenceInfoGui() const; + + void setThumbnail(const QString &data); + QString getThumbnail() const; + + bool wasDownloaded() const; + void setWasDownloaded(bool downloaded); + + const std::shared_ptr &getContentModel() const; + +signals: + void msgStateChanged(LinphoneEnums::ChatMessageState state); + void thumbnailChanged(); + void fileOffsetChanged(); + void filePathChanged(); + void isFileChanged(); + void isFileTransferChanged(); + void isFileEncryptedChanged(); + void wasDownloadedChanged(bool downloaded); + void isVideoChanged(); + + void lCreateThumbnail(const bool &force = false); + void lRemoveDownloadedFile(); + void lDownloadFile(); + void lCancelDownloadFile(); + void lOpenFile(bool showDirectory = false); + bool lSaveAs(const QString &path); + +private: + DECLARE_ABSTRACT_OBJECT + bool mIsFile; + bool mIsVideo; + bool mIsFileEncrypted; + bool mIsFileTransfer; + bool mIsCalendar; + bool mIsMultipart; + bool mIsText; + bool mIsVoiceRecording; + int mFileDuration; + QString mThumbnail; + QString mUtf8Text; + QString mFilePath; + QString mName; + quint64 mFileSize; + quint64 mFileOffset; + bool mWasDownloaded; + QSharedPointer mConferenceInfo = nullptr; + + std::shared_ptr mChatMessageContentModel; + QSharedPointer> mChatMessageContentModelConnection; +}; + +#endif // CHAT_MESSAGE_CONTENT_CORE_H_ diff --git a/Linphone/core/chat/message/content/ChatMessageContentGui.cpp b/Linphone/core/chat/message/content/ChatMessageContentGui.cpp new file mode 100644 index 000000000..e40668d3d --- /dev/null +++ b/Linphone/core/chat/message/content/ChatMessageContentGui.cpp @@ -0,0 +1,38 @@ +/* + * 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 "ChatMessageContentGui.hpp" +#include "core/App.hpp" + +DEFINE_ABSTRACT_OBJECT(ChatMessageContentGui) + +ChatMessageContentGui::ChatMessageContentGui(QSharedPointer core) { + App::getInstance()->mEngine->setObjectOwnership(this, QQmlEngine::JavaScriptOwnership); + mCore = core; + if (isInLinphoneThread()) moveToThread(App::getInstance()->thread()); +} + +ChatMessageContentGui::~ChatMessageContentGui() { + mustBeInMainThread("~" + getClassName()); +} + +ChatMessageContentCore *ChatMessageContentGui::getCore() const { + return mCore.get(); +} diff --git a/Linphone/core/chat/message/content/ChatMessageContentGui.hpp b/Linphone/core/chat/message/content/ChatMessageContentGui.hpp new file mode 100644 index 000000000..2d4e3b428 --- /dev/null +++ b/Linphone/core/chat/message/content/ChatMessageContentGui.hpp @@ -0,0 +1,41 @@ +/* + * 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 CHAT_MESSAGE_CONTENT_GUI_H_ +#define CHAT_MESSAGE_CONTENT_GUI_H_ + +#include "core/chat/message/content/ChatMessageContentCore.hpp" +#include +#include + +class ChatMessageContentGui : public QObject, public AbstractObject { + Q_OBJECT + + Q_PROPERTY(ChatMessageContentCore *core READ getCore CONSTANT) + +public: + ChatMessageContentGui(QSharedPointer core); + ~ChatMessageContentGui(); + ChatMessageContentCore *getCore() const; + QSharedPointer mCore; + DECLARE_ABSTRACT_OBJECT +}; + +#endif diff --git a/Linphone/core/chat/message/content/ChatMessageContentList.cpp b/Linphone/core/chat/message/content/ChatMessageContentList.cpp new file mode 100644 index 000000000..af49fb9cc --- /dev/null +++ b/Linphone/core/chat/message/content/ChatMessageContentList.cpp @@ -0,0 +1,164 @@ +/* + * 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 "ChatMessageContentList.hpp" +#include "core/App.hpp" +#include "core/chat/ChatCore.hpp" +#include "core/chat/message/content/ChatMessageContentGui.hpp" + +#include +#include + +#include + +// ============================================================================= + +DEFINE_ABSTRACT_OBJECT(ChatMessageContentList) + +QSharedPointer ChatMessageContentList::create() { + auto model = QSharedPointer(new ChatMessageContentList(), &QObject::deleteLater); + model->moveToThread(App::getInstance()->thread()); + model->setSelf(model); + return model; +} + +ChatMessageContentList::ChatMessageContentList(QObject *parent) : ListProxy(parent) { + mustBeInMainThread(getClassName()); + App::getInstance()->mEngine->setObjectOwnership(this, QQmlEngine::CppOwnership); +} + +ChatMessageContentList::~ChatMessageContentList() { + mustBeInMainThread("~" + getClassName()); + mModelConnection = nullptr; +} + +ChatMessageGui *ChatMessageContentList::getChatMessage() const { + if (mChatMessageCore) return new ChatMessageGui(mChatMessageCore); + else return nullptr; +} + +QSharedPointer ChatMessageContentList::getChatMessageCore() const { + return mChatMessageCore; +} + +void ChatMessageContentList::setChatMessageCore(QSharedPointer core) { + if (mChatMessageCore != core) { + // if (mChatMessageCore) disconnect(mChatMessageCore.get(), &ChatCore::, this, nullptr); + mChatMessageCore = core; + // if (mChatMessageCore) + // connect(mChatMessageCore.get(), &ChatCore::messageListChanged, this, &ChatMessageContentList::lUpdate); + emit chatMessageChanged(); + lUpdate(); + } +} + +void ChatMessageContentList::setChatMessageGui(ChatMessageGui *chat) { + auto chatCore = chat ? chat->mCore : nullptr; + setChatMessageCore(chatCore); +} + +int ChatMessageContentList::findFirstUnreadIndex() { + auto chatList = getSharedList(); + auto it = std::find_if(chatList.begin(), chatList.end(), + [](const QSharedPointer item) { return !item->isRead(); }); + return it == chatList.end() ? -1 : std::distance(chatList.begin(), it); +} + +void ChatMessageContentList::setSelf(QSharedPointer me) { + mModelConnection = SafeConnection::create(me, CoreModel::getInstance()); + + mModelConnection->makeConnectToCore(&ChatMessageContentList::lUpdate, [this]() { + for (auto &content : getSharedList()) { + if (content) disconnect(content.get()); + } + if (!mChatMessageCore) return; + auto contents = mChatMessageCore->getChatMessageContentList(); + for (auto &content : contents) { + connect(content.get(), &ChatMessageContentCore::wasDownloadedChanged, this, + [this, content](bool wasDownloaded) { + if (wasDownloaded) { + content->lCreateThumbnail(); + } + }); + connect(content.get(), &ChatMessageContentCore::thumbnailChanged, this, [this] { emit lUpdate(); }); + } + resetData(contents); + }); + mModelConnection->makeConnectToCore(&ChatMessageContentList::lAddFile, [this](const QString &path) { + QFile file(path); + // #ifdef _WIN32 + // // A bug from FileDialog suppose that the file is local and overwrite the uri by removing "\\". + // if (!file.exists()) { + // path.prepend("\\\\"); + // file.setFileName(path); + // } + // #endif + if (!file.exists()) return; + if (rowCount() >= 12) { + //: Error + Utils::showInformationPopup(tr("popup_error_title"), + //: You can add 12 files maximum + tr("popup_error_max_files_count_message"), false); + return; + } + + qint64 fileSize = file.size(); + if (fileSize > Constants::FileSizeLimit) { + qWarning() << QStringLiteral("Unable to send file. (Size limit=%1)").arg(Constants::FileSizeLimit); + return; + } + auto name = QFileInfo(file).fileName().toStdString(); + mModelConnection->invokeToModel([this, path, fileSize, name] { + std::shared_ptr content = CoreModel::getInstance()->getCore()->createContent(); + { + QStringList mimeType = QMimeDatabase().mimeTypeForFile(path).name().split('/'); + if (mimeType.length() != 2) { + qWarning() << QStringLiteral("Unable to get supported mime type for: `%1`.").arg(path); + return; + } + content->setType(Utils::appStringToCoreString(mimeType[0])); + content->setSubtype(Utils::appStringToCoreString(mimeType[1])); + } + content->setSize(size_t(fileSize)); + content->setName(name); + content->setFilePath(Utils::appStringToCoreString(path)); + auto contentCore = ChatMessageContentCore::create(content, nullptr); + mModelConnection->invokeToCore([this, contentCore] { + connect(contentCore.get(), &ChatMessageContentCore::isFileChanged, this, [this, contentCore] { + int i = -1; + get(contentCore.get(), &i); + emit dataChanged(index(i), index(i)); + }); + add(contentCore); + contentCore->lCreateThumbnail( + true); // Was not created because linphone::Content is not considered as a file (yet) + }); + }); + }); + emit lUpdate(); +} + +QVariant ChatMessageContentList::data(const QModelIndex &index, int role) const { + int row = index.row(); + if (!index.isValid() || row < 0 || row >= mList.count()) return QVariant(); + if (role == Qt::DisplayRole) + return QVariant::fromValue(new ChatMessageContentGui(mList[row].objectCast())); + return QVariant(); +} \ No newline at end of file diff --git a/Linphone/core/chat/message/content/ChatMessageContentList.hpp b/Linphone/core/chat/message/content/ChatMessageContentList.hpp new file mode 100644 index 000000000..4d5fa9fa4 --- /dev/null +++ b/Linphone/core/chat/message/content/ChatMessageContentList.hpp @@ -0,0 +1,62 @@ +/* + * 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 CHAT_MESSAGE_CONTENT_LIST_H_ +#define CHAT_MESSAGE_CONTENT_LIST_H_ + +#include "core/proxy/ListProxy.hpp" +#include "tool/AbstractObject.hpp" +#include "tool/thread/SafeConnection.hpp" +#include + +class ChatMessageGui; +class ChatMessageCore; +// ============================================================================= + +class ChatMessageContentList : public ListProxy, public AbstractObject { + Q_OBJECT +public: + static QSharedPointer create(); + ChatMessageContentList(QObject *parent = Q_NULLPTR); + ~ChatMessageContentList(); + + QSharedPointer getChatMessageCore() const; + ChatMessageGui *getChatMessage() const; + void setChatMessageCore(QSharedPointer core); + void setChatMessageGui(ChatMessageGui *chat); + + int findFirstUnreadIndex(); + + void setSelf(QSharedPointer me); + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + +signals: + void lAddFile(QString path); + void isFileChanged(); + void lUpdate(); + void chatMessageChanged(); + +private: + QSharedPointer mChatMessageCore; + QSharedPointer> mModelConnection; + DECLARE_ABSTRACT_OBJECT +}; + +#endif diff --git a/Linphone/core/chat/message/content/ChatMessageContentProxy.cpp b/Linphone/core/chat/message/content/ChatMessageContentProxy.cpp new file mode 100644 index 000000000..34e822124 --- /dev/null +++ b/Linphone/core/chat/message/content/ChatMessageContentProxy.cpp @@ -0,0 +1,106 @@ +/* + * 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 "ChatMessageContentProxy.hpp" +#include "ChatMessageContentGui.hpp" +#include "core/App.hpp" +#include "core/chat/message/ChatMessageGui.hpp" + +DEFINE_ABSTRACT_OBJECT(ChatMessageContentProxy) + +ChatMessageContentProxy::ChatMessageContentProxy(QObject *parent) : LimitProxy(parent) { + mList = ChatMessageContentList::create(); + setSourceModel(mList.get()); +} + +ChatMessageContentProxy::~ChatMessageContentProxy() { +} + +void ChatMessageContentProxy::setSourceModel(QAbstractItemModel *model) { + auto oldChatMessageContentList = getListModel(); + if (oldChatMessageContentList) { + // disconnect(oldChatMessageContentList); + } + auto newChatMessageContentList = dynamic_cast(model); + if (newChatMessageContentList) { + // connect(newChatMessageContentList, &ChatMessageContentList::chatChanged, this, + // &ChatMessageContentProxy::chatChanged); + } + setSourceModels(new SortFilterList(model)); + sort(0); +} + +ChatMessageGui *ChatMessageContentProxy::getChatMessageGui() { + auto model = getListModel(); + if (!mChatMessageGui && model) mChatMessageGui = model->getChatMessage(); + return mChatMessageGui; +} + +void ChatMessageContentProxy::setChatMessageGui(ChatMessageGui *chat) { + getListModel()->setChatMessageGui(chat); +} + +// ChatMessageGui *ChatMessageContentProxy::getChatMessageAtIndex(int i) { +// auto model = getListModel(); +// auto sourceIndex = mapToSource(index(i, 0)).row(); +// if (model) { +// auto chat = model->getAt(sourceIndex); +// if (chat) return new ChatMessageGui(chat); +// else return nullptr; +// } +// return nullptr; +// } + +void ChatMessageContentProxy::addFile(const QString &path) { + auto model = getListModel(); + if (model) emit model->lAddFile(path.toUtf8()); +} + +void ChatMessageContentProxy::removeContent(ChatMessageContentGui *contentGui) { + auto model = getListModel(); + if (model && contentGui) model->remove(contentGui->mCore); +} + +void ChatMessageContentProxy::clear() { + auto model = getListModel(); + if (model) model->clearData(); +} + +bool ChatMessageContentProxy::SortFilterList::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { + auto contentCore = getItemAtSource(sourceRow); + if (contentCore) { + if (mFilterType == (int)FilterContentType::Unknown) return false; + else if (mFilterType == (int)FilterContentType::File) { + return contentCore->isFile() || contentCore->isFileTransfer(); + } else if (mFilterType == (int)FilterContentType::Text) return contentCore->isText(); + else if (mFilterType == (int)FilterContentType::Voice) return contentCore->isVoiceRecording(); + else if (mFilterType == (int)FilterContentType::Conference) return contentCore->isCalendar(); + else if (mFilterType == (int)FilterContentType::All) return true; + } + return false; +} + +bool ChatMessageContentProxy::SortFilterList::lessThan(const QModelIndex &sourceLeft, + const QModelIndex &sourceRight) const { + auto l = getItemAtSource(sourceLeft.row()); + auto r = getItemAtSource(sourceRight.row()); + if (l && r) return l->getTimestamp() <= r->getTimestamp(); + else return true; +} diff --git a/Linphone/core/chat/message/content/ChatMessageContentProxy.hpp b/Linphone/core/chat/message/content/ChatMessageContentProxy.hpp new file mode 100644 index 000000000..8544b3622 --- /dev/null +++ b/Linphone/core/chat/message/content/ChatMessageContentProxy.hpp @@ -0,0 +1,66 @@ +/* + * 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 CHAT_MESSAGE_CONENT_PROXY_H_ +#define CHAT_MESSAGE_CONENT_PROXY_H_ + +#include "ChatMessageContentList.hpp" +#include "core/proxy/LimitProxy.hpp" +#include "tool/AbstractObject.hpp" + +// ============================================================================= + +class ChatMessageGui; +class ChatMessageContentGui; + +class ChatMessageContentProxy : public LimitProxy, public AbstractObject { + Q_OBJECT + Q_PROPERTY(ChatMessageGui *chatMessageGui READ getChatMessageGui WRITE setChatMessageGui NOTIFY chatChanged) + +public: + enum class FilterContentType { Unknown = 0, File = 1, Text = 2, Voice = 3, Conference = 4, All = 5 }; + Q_ENUM(FilterContentType) + + DECLARE_SORTFILTER_CLASS(ChatMessageContentProxy *mHideListProxy = nullptr;) + ChatMessageContentProxy(QObject *parent = Q_NULLPTR); + ~ChatMessageContentProxy(); + + ChatMessageGui *getChatMessageGui(); + void setChatMessageGui(ChatMessageGui *chat); + + void setSourceModel(QAbstractItemModel *sourceModel) override; + + Q_INVOKABLE void addFile(const QString &path); + Q_INVOKABLE void removeContent(ChatMessageContentGui *contentGui); + Q_INVOKABLE void clear(); + +signals: + void chatChanged(); + void filterChanged(); + void messageInserted(int index, ChatMessageGui *message); + +protected: + QSharedPointer mList; + ChatMessageGui *mChatMessageGui = nullptr; + + DECLARE_ABSTRACT_OBJECT +}; + +#endif diff --git a/Linphone/core/path/Paths.cpp b/Linphone/core/path/Paths.cpp index ecb1c8d68..4f168430b 100644 --- a/Linphone/core/path/Paths.cpp +++ b/Linphone/core/path/Paths.cpp @@ -269,7 +269,8 @@ QString Paths::getFriendsListFilePath() { } QString Paths::getDownloadDirPath() { - return getWritableDirPath(QStandardPaths::writableLocation(QStandardPaths::DownloadLocation) + QDir::separator()); + return getWritableDirPath(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + + Constants::PathFiles); } QString Paths::getLimeDatabasePath() { diff --git a/Linphone/core/proxy/AbstractListProxy.hpp b/Linphone/core/proxy/AbstractListProxy.hpp index db56b5264..2e5b9d0df 100644 --- a/Linphone/core/proxy/AbstractListProxy.hpp +++ b/Linphone/core/proxy/AbstractListProxy.hpp @@ -123,7 +123,9 @@ public: } virtual void clearData() override { + beginResetModel(); mList.clear(); + endResetModel(); } virtual void resetData(QList newData = QList()) { diff --git a/Linphone/core/setting/SettingsCore.cpp b/Linphone/core/setting/SettingsCore.cpp index 99aaa89cf..9c79f37af 100644 --- a/Linphone/core/setting/SettingsCore.cpp +++ b/Linphone/core/setting/SettingsCore.cpp @@ -95,6 +95,7 @@ SettingsCore::SettingsCore(QObject *parent) : QObject(parent) { // Chat mEmojiFont = settingsModel->getEmojiFont(); + mTextMessageFont = settingsModel->getTextMessageFont(); // Ui INIT_CORE_MEMBER(DisableChatFeature, settingsModel) @@ -539,6 +540,26 @@ void SettingsCore::setVfsEnabled(bool enabled) { } } +bool SettingsCore::getVfsEncrypted() { + mAppSettings.beginGroup("keychain"); + return mAppSettings.value("enabled", false).toBool(); +} + +void SettingsCore::setVfsEncrypted(bool encrypted, const bool deleteUserData) { +#ifdef ENABLE_QT_KEYCHAIN + if (getVfsEncrypted() != encrypted) { + if (encrypted) { + mVfsUtils.newEncryptionKeyAsync(); + shared_ptr factory = linphone::Factory::get(); + factory->setDownloadDir(Utils::appStringToCoreString(getDownloadFolder())); + } else { // Remove key, stop core, delete data and initiate reboot + mVfsUtils.needToDeleteUserData(deleteUserData); + mVfsUtils.deleteKey(mVfsUtils.getApplicationVfsEncryptionKey()); + } + } +#endif +} + void SettingsCore::setVideoEnabled(bool enabled) { if (mVideoEnabled != enabled) { mVideoEnabled = enabled; diff --git a/Linphone/core/setting/SettingsCore.hpp b/Linphone/core/setting/SettingsCore.hpp index 1f49823a8..20af22833 100644 --- a/Linphone/core/setting/SettingsCore.hpp +++ b/Linphone/core/setting/SettingsCore.hpp @@ -112,6 +112,9 @@ public: } void setVfsEnabled(bool enabled); + bool getVfsEncrypted(); + void setVfsEncrypted(bool encrypted, const bool deleteUserData); + // Call. -------------------------------------------------------------------- bool getVideoEnabled() { @@ -234,6 +237,7 @@ public: DECLARE_CORE_GETSET_MEMBER(bool, disableCallForward, DisableCallForward) DECLARE_CORE_GETSET_MEMBER(QString, callForwardToAddress, CallForwardToAddress) DECLARE_CORE_GET_CONSTANT(QFont, emojiFont, EmojiFont) + DECLARE_CORE_GET_CONSTANT(QFont, textMessageFont, TextMessageFont) signals: diff --git a/Linphone/core/variant/VariantList.cpp b/Linphone/core/variant/VariantList.cpp index 602033638..e7674722c 100644 --- a/Linphone/core/variant/VariantList.cpp +++ b/Linphone/core/variant/VariantList.cpp @@ -43,6 +43,10 @@ void VariantList::setModel(QList list) { emit modelChanged(); } +QList VariantList::getModel() const { + return mList; +} + void VariantList::replace(int index, QVariant newValue) { mList.replace(index, newValue); } diff --git a/Linphone/core/variant/VariantList.hpp b/Linphone/core/variant/VariantList.hpp index cbc75c642..e1c2a837c 100644 --- a/Linphone/core/variant/VariantList.hpp +++ b/Linphone/core/variant/VariantList.hpp @@ -38,6 +38,7 @@ public: ~VariantList(); void setModel(QList list); + QList getModel() const; void replace(int index, QVariant newValue); diff --git a/Linphone/data/image/download-simple.svg b/Linphone/data/image/download-simple.svg new file mode 100644 index 000000000..60d202b45 --- /dev/null +++ b/Linphone/data/image/download-simple.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Linphone/data/image/file-image.svg b/Linphone/data/image/file-image.svg new file mode 100644 index 000000000..3a3c6e90e --- /dev/null +++ b/Linphone/data/image/file-image.svg @@ -0,0 +1,3 @@ + + + diff --git a/Linphone/data/image/file-pdf.svg b/Linphone/data/image/file-pdf.svg new file mode 100644 index 000000000..63fc1ae2e --- /dev/null +++ b/Linphone/data/image/file-pdf.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Linphone/data/image/file-plus.svg b/Linphone/data/image/file-plus.svg new file mode 100644 index 000000000..a47d96646 --- /dev/null +++ b/Linphone/data/image/file-plus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Linphone/data/image/file.svg b/Linphone/data/image/file.svg new file mode 100644 index 000000000..85a754433 --- /dev/null +++ b/Linphone/data/image/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Linphone/data/image/play-fill.svg b/Linphone/data/image/play-fill.svg new file mode 100644 index 000000000..709e65d4e --- /dev/null +++ b/Linphone/data/image/play-fill.svg @@ -0,0 +1,3 @@ + + + diff --git a/Linphone/data/languages/de.ts b/Linphone/data/languages/de.ts index 40d23dd52..88beed79e 100644 --- a/Linphone/data/languages/de.ts +++ b/Linphone/data/languages/de.ts @@ -523,74 +523,74 @@ App - + remote_provisioning_dialog Voulez-vous télécharger et appliquer la configuration depuis cette adresse ? Möchten Sie die Remote-Konfiguration von dieser Adresse herunterladen und anwenden? - + application_description "A free and open source SIP video-phone." Ein kostenloses Open-Source SIP Video-Telefon. - + command_line_arg_order "Send an order to the application towards a command line" Kommandozeilen-Befehl an die Anwendung schicken - + command_line_option_show_help Zeige Hilfe - + command_line_option_show_app_version Zeige App-Version - + command_line_option_config_to_fetch "Specify the linphone configuration file to be fetched. It will be merged with the current configuration." Abzurufende Linphone-Konfigurationsdatei angeben. Sie wird mit der aktuellen Konfiguration zusammengeführt. - + command_line_option_config_to_fetch_arg "URL, path or file" URL, Pfad oder Datei - + command_line_option_minimized - + command_line_option_log_to_stdout Debug-Informationen auf der Standardausgabe ausgeben - + command_line_option_print_app_logs_only "Print only logs from the application" Nur Anwendungs-Logs ausgeben - + hide_action "Cacher" "Afficher" Ausblenden - + show_action Zeigen - + quit_action "Quitter" Beenden @@ -1727,31 +1727,30 @@ ChatListView - + chat_message_is_writing_info %1 is writing… - + chat_message_draft_sending_text - Draft : %1 - + chat_room_delete "Supprimer" - + chat_list_delete_chat_popup_title Delete the chat ? - + chat_list_delete_chat_popup_message This chat and all its messages will be deleted. Do You want to continue ? @@ -1760,25 +1759,31 @@ ChatMessage - + + chat_message_copy_selection + "Copy selection" + + + + chat_message_copy "Copy" - + chat_message_copied_to_clipboard_title Copied - + chat_message_copied_to_clipboard_toast "to clipboard" - + chat_message_delete "Delete" @@ -1787,13 +1792,13 @@ ChatMessageCore - + info_toast_deleted_title Deleted - + info_toast_deleted_message The message has been deleted @@ -2023,13 +2028,13 @@ Fehler - + information_popup_error_title Erreur Fehler - + information_popup_voicemail_address_undefined_message L'URI de messagerie vocale n'est pas définie. Die Voicemail-URI ist nicht definiert. @@ -2730,6 +2735,15 @@ Verschlüsselungsvalidierung + + FileView + + + fileTransferCancel + 'Cancel' : Message link to cancel a transfer (upload/download) + + + FriendCore @@ -4468,22 +4482,22 @@ Pour les activer dans un projet commercial, merci de nous contacter. - + conference_invitation - + conference_invitation_updated - + conference_invitation_cancelled - + unknown_audio_device_name Unbekannter Gerätename @@ -4491,16 +4505,16 @@ Pour les activer dans un projet commercial, merci de nous contacter. Utils - + information_popup_call_not_created_message "L'appel n'a pas pu être créé" Anruf konnte nicht erstellt werden - - - - + + + + information_popup_error_title Error ---------- @@ -4508,12 +4522,12 @@ Failed to create 1-1 conversation with %1 ! - + information_popup_group_call_not_created_message - + number_of_years %n an(s) @@ -4522,7 +4536,7 @@ Failed to create 1-1 conversation with %1 ! - + number_of_month "%n mois" @@ -4531,7 +4545,7 @@ Failed to create 1-1 conversation with %1 ! - + number_of_weeks %n semaine(s) @@ -4540,7 +4554,7 @@ Failed to create 1-1 conversation with %1 ! - + number_of_days %n jour(s) @@ -4549,52 +4563,52 @@ Failed to create 1-1 conversation with %1 ! - + today "Aujourd'hui" Heute - + yesterday "Hier Gestern - + call_zrtp_token_verification_possible_characters "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 - - + + information_popup_chatroom_creation_error_message Failed to create 1-1 conversation with %1 ! - + contact_presence_status_available - + contact_presence_status_away - + contact_presence_status_busy Beschäftigt - + contact_presence_status_do_not_disturb Nicht stören - + contact_presence_status_offline Offline @@ -4761,1117 +4775,1117 @@ Failed to create 1-1 conversation with %1 ! country - + Afghanistan Afghanistan - + Albania Albanien - + Algeria Algerien - + AmericanSamoa Amerikanisch-Samoa - + Andorra Andorra - + Angola Angola - + Anguilla Anguilla - + AntiguaAndBarbuda Antigua und Barbuda - + Argentina Argentinien - + Armenia Armenien - + Aruba Aruba - + Australia Australien - + Austria Österreich - + Azerbaijan Aserbaidschan - + Bahamas Bahamas - + Bahrain Bahrain - + Bangladesh Bangladesch - + Barbados Barbados - + Belarus Belarus - + Belgium Belgien - + Belize Belize - + Benin Benin - + Bermuda Bermuda - + Bhutan Bhutan - + Bolivia Bolivien - + BosniaAndHerzegowina Bosnien und Herzegowina - + Botswana Botswana - + Brazil Brasilien - + Brunei Brunei - + Bulgaria Bulgarien - + BurkinaFaso Burkina Faso - + Burundi Burundi - + Cambodia Kambodscha - + Cameroon Kamerun - + Canada Kanada - + CapeVerde Kap Verde - + CaymanIslands Caymaninseln - + CentralAfricanRepublic Zentralafrikanische Republik - + Chad Tschad - + Chile Chile - + China China - + Colombia Kolumbien - + Comoros Komoren - + PeoplesRepublicOfCongo Volksrepublik Kongo - + CookIslands Cookinseln - + CostaRica Kosta Rica - + IvoryCoast Elfenbeinküste - + Croatia Kroatien - + Cuba Kuba - + Cyprus Zypern - + CzechRepublic Tschechische Republik - + Denmark Dänemark - + Djibouti Dschibuti - + Dominica Dominica - + DominicanRepublic Dominikanische Republik - + Ecuador Ecuador - + Egypt Ägypten - + ElSalvador El Salvador - + EquatorialGuinea Äquatorialguinea - + Eritrea Eritrea - + Estonia Estland - + Ethiopia Äthiopien - + FalklandIslands Falklandinseln - + FaroeIslands Färöer-Inseln - + Fiji Fidschi - + Finland Finnland - + France Frankreich - + FrenchGuiana Französisch-Guayana - + FrenchPolynesia Französisch-Polynesien - + Gabon Gabon - + Gambia Gambia - + Georgia Georgien - + Germany Deutschland - + Ghana Ghana - + Gibraltar Gibraltar - + Greece Griechenland - + Greenland Grönland - + Grenada Grenada - + Guadeloupe Guadeloupe - + Guam Guam - + Guatemala Guatemala - + Guinea Guinea - + GuineaBissau Guinea-Bissau - + Guyana Guyana - + Haiti Haiti - + Honduras Honduras - + DemocraticRepublicOfCongo Demokratische Republik Kongo - + HongKong Hongkong - + Hungary Ungarn - + Iceland Island - + India Indien - + Indonesia Indonesien - + Iran Iran - + Iraq Irak - + Ireland Irland - + Israel Israel - + Italy Italien - + Jamaica Jamaika - + Japan Japan - + Jordan Jordanien - + Kazakhstan Kasachstan - + Kenya Kenia - + Kiribati Kiribati - + DemocraticRepublicOfKorea Demokratische Volksrepublik Korea - + RepublicOfKorea Republik Korea - + Kuwait Kuwait - + Kyrgyzstan Kirgisistan - + Laos Laos - + Latvia Lettland - + Lebanon Libanon - + Lesotho Lesotho - + Liberia Liberien - + Libya Libyen - + Liechtenstein Liechtenstein - + Lithuania Litauen - + Luxembourg Luxemburg - + Macau Macau - + Macedonia Mazedonien - + Madagascar Madagaskar - + Malawi Malawi - + Malaysia Malaysien - + Maldives Malediven - + Mali Mali - + Malta Malta - + MarshallIslands Marshallinseln - + Martinique Martinique - + Mauritania Mauretanien - + Mauritius Mauritius - + Mayotte Mayotte - + Mexico Mexiko - + Micronesia Föderierte Staaten von Mikronesien - + Moldova Moldawien - + Monaco Monaco - + Mongolia Mongolei - + Montenegro Montenegro - + Montserrat Montserrat - + Morocco Marokko - + Mozambique Mosambik - + Myanmar Myanmar - + Namibia Namibia - + NauruCountry Nauru - + Nepal Nepal - + Netherlands Niederlande - + NewCaledonia Neukaledonien - + NewZealand Neuseeland - + Nicaragua Nicaragua - + Niger Niger - + Nigeria Nigeria - + Niue Niue - + NorfolkIsland Norfolkinsel - + NorthernMarianaIslands Nördliche Marianeninseln - + Norway Norwegen - + Oman Oman - + Pakistan Pakistan - + Palau Palau - + PalestinianTerritories Palästinensische Gebiete - + Panama Panama - + PapuaNewGuinea Papua-Neuguinea - + Paraguay Paraguay - + Peru Peru - + Philippines Philippinen - + Poland Polen - + Portugal Portugal - + PuertoRico Puerto Rico - + Qatar Katar - + Reunion Réunion - + Romania Rumänien - + RussianFederation Russische Föderation - + Rwanda Ruanda - + SaintHelena Sankt Helena - + SaintKittsAndNevis Sankt Kitts und Nevis - + SaintLucia Sankt Lucia - + SaintPierreAndMiquelon Sankt Pierre und Miquelon - + SaintVincentAndTheGrenadines Sankt Vincent und die Grenadinen - + Samoa Samoa - + SanMarino San Marino - + SaoTomeAndPrincipe São Tomé und Príncipe - + SaudiArabia Saudi-Arabien - + Senegal Senegal - + Serbia Serbien - + Seychelles Seychellen - + SierraLeone Sierra Leone - + Singapore Singapur - + Slovakia Slowakei - + Slovenia Slowenien - + SolomonIslands Salomonen - + Somalia Somalia - + SouthAfrica Südafrika - + Spain Spanien - + SriLanka Sri Lanka - + Sudan Sudan - + Suriname Suriname - + Swaziland Eswatini - + Sweden Schweden - + Switzerland Schweiz - + Syria Syrien - + Taiwan Taiwan - + Tajikistan Tadschikistan - + Tanzania Tansania - + Thailand Thailand - + Togo Togo - + Tokelau Tokelau - + Tonga Tonga - + TrinidadAndTobago Trinidad und Tobago - + Tunisia Tunesien - + Turkey Türkei - + Turkmenistan Turkmenistan - + TurksAndCaicosIslands Turks- und Caicosinseln - + Tuvalu Tuvalu - + Uganda Uganda - + Ukraine Ukraine - + UnitedArabEmirates Vereinigte Arabische Emirate - + UnitedKingdom Vereinigtes Königreich - + UnitedStates Vereinigte Staaten - + Uruguay Uruguay - + Uzbekistan Usbekistan - + Vanuatu Vanuatu - + Venezuela Venezuela - + Vietnam Vietnam - + WallisAndFutunaIslands Wallis und Futuna Inseln - + Yemen Jemen - + Zambia Sambia - + Zimbabwe Simbabwe diff --git a/Linphone/data/languages/en.ts b/Linphone/data/languages/en.ts index 76939785a..0f99ef390 100644 --- a/Linphone/data/languages/en.ts +++ b/Linphone/data/languages/en.ts @@ -523,74 +523,74 @@ App - + remote_provisioning_dialog Voulez-vous télécharger et appliquer la configuration depuis cette adresse ? Do you want to download and apply remote provisioning from this address ? - + application_description "A free and open source SIP video-phone." A free and open source SIP video-phone. - + command_line_arg_order "Send an order to the application towards a command line" Send an order to the application towards a command line - + command_line_option_show_help Show this help - + command_line_option_show_app_version Show app version - + command_line_option_config_to_fetch "Specify the linphone configuration file to be fetched. It will be merged with the current configuration." Specify the linphone configuration file to be fetched. It will be merged with the current configuration. - + command_line_option_config_to_fetch_arg "URL, path or file" URL, path or file - + command_line_option_minimized Minimize - + command_line_option_log_to_stdout Log to stdout some debug information while running - + command_line_option_print_app_logs_only "Print only logs from the application" Print only logs from the application - + hide_action "Cacher" "Afficher" Hide - + show_action Show - + quit_action "Quitter" Quit @@ -1689,31 +1689,30 @@ ChatListView - + chat_message_is_writing_info %1 is writing… %1 is writing… - + chat_message_draft_sending_text - Draft : %1 Draft : %1 - + chat_room_delete "Supprimer" Delete - + chat_list_delete_chat_popup_title Delete the chat ? Delete the chat ? - + chat_list_delete_chat_popup_message This chat and all its messages will be deleted. Do You want to continue ? This chat and all its messages will be deleted. Do You want to continue ? @@ -1722,25 +1721,31 @@ ChatMessage - + + chat_message_copy_selection + "Copy selection" + Copy selection + + + chat_message_copy "Copy" Copy - + chat_message_copied_to_clipboard_title Copied Copied - + chat_message_copied_to_clipboard_toast "to clipboard" in clipboard - + chat_message_delete "Delete" Delete @@ -1749,13 +1754,13 @@ ChatMessageCore - + info_toast_deleted_title Deleted Deleted - + info_toast_deleted_message The message has been deleted The message has been deleted @@ -1966,13 +1971,13 @@ Only your correspondent can decrypt them. Contact - + information_popup_error_title Erreur Error - + information_popup_voicemail_address_undefined_message L'URI de messagerie vocale n'est pas définie. The voicemail URI is not defined. @@ -2653,6 +2658,15 @@ Only your correspondent can decrypt them. Encryption validation + + FileView + + + fileTransferCancel + 'Cancel' : Message link to cancel a transfer (upload/download) + Cancel + + FriendCore @@ -4387,22 +4401,22 @@ To enable them in a commercial project, please contact us. Group call couldn't be created - + unknown_audio_device_name Unknown device name - + conference_invitation Meeting invitation - + conference_invitation_cancelled Meeting cancellation - + conference_invitation_updated Meeting modification @@ -4410,41 +4424,41 @@ To enable them in a commercial project, please contact us. Utils - + contact_presence_status_available Available - + contact_presence_status_busy Busy - + contact_presence_status_do_not_disturb Do not disturb - + contact_presence_status_offline Offline - + contact_presence_status_away Idle/Away - + information_popup_call_not_created_message "L'appel n'a pas pu être créé" Call could not be created - - - - + + + + information_popup_error_title Error ---------- @@ -4452,12 +4466,12 @@ Failed to create 1-1 conversation with %1 ! Error - + information_popup_group_call_not_created_message Group call couldn't be created - + number_of_years %n an(s) @@ -4466,7 +4480,7 @@ Failed to create 1-1 conversation with %1 ! - + number_of_month "%n mois" @@ -4475,7 +4489,7 @@ Failed to create 1-1 conversation with %1 ! - + number_of_weeks %n semaine(s) @@ -4484,7 +4498,7 @@ Failed to create 1-1 conversation with %1 ! - + number_of_days %n jour(s) @@ -4493,26 +4507,26 @@ Failed to create 1-1 conversation with %1 ! - + today "Aujourd'hui" Today - + yesterday "Hier Yesterday - + call_zrtp_token_verification_possible_characters "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 - - + + information_popup_chatroom_creation_error_message Failed to create 1-1 conversation with %1 ! Failed to create 1-1 conversation with %1 ! @@ -4680,1117 +4694,1117 @@ Failed to create 1-1 conversation with %1 ! country - + Afghanistan Afghanistan - + Albania Albania - + Algeria Algeria - + AmericanSamoa American Samoa - + Andorra Andorra - + Angola Angola - + Anguilla Anguilla - + AntiguaAndBarbuda Antigua-et-Barbuda - + Argentina Argentina - + Armenia Armenia - + Aruba Aruba - + Australia Australia - + Austria Austria - + Azerbaijan Azerbaijan - + Bahamas Bahamas - + Bahrain Bahrain - + Bangladesh Bangladesh - + Barbados Barbados - + Belarus Belarus - + Belgium Belgium - + Belize Belize - + Benin Benin - + Bermuda Bermuda - + Bhutan Bhutan - + Bolivia Bolivia - + BosniaAndHerzegowina Bosnia And Herzegowina - + Botswana Botswana - + Brazil Brazil - + Brunei Brunei - + Bulgaria Bulgaria - + BurkinaFaso Burkina Faso - + Burundi Burundi - + Cambodia Cambodia - + Cameroon Cameroon - + Canada Canada - + CapeVerde Cape Verde - + CaymanIslands Cayman Islands - + CentralAfricanRepublic Central African Republic - + Chad Chad - + Chile Chile - + China China - + Colombia Colombia - + Comoros Comoros - + PeoplesRepublicOfCongo Peoples Republic Of Congo - + CookIslands Cook Islands - + CostaRica Costa Rica - + IvoryCoast Ivory Coast - + Croatia Croatia - + Cuba Cuba - + Cyprus Cyprus - + CzechRepublic Czech Republic - + Denmark Denmark - + Djibouti Djibouti - + Dominica Dominica - + DominicanRepublic Dominican Republic - + Ecuador Ecuador - + Egypt Egypt - + ElSalvador El Salvador - + EquatorialGuinea Equatorial Guinea - + Eritrea Eritrea - + Estonia Estonia - + Ethiopia Ethiopia - + FalklandIslands Falkland Islands - + FaroeIslands Faroe Islands - + Fiji Fiji - + Finland Finland - + France France - + FrenchGuiana French Guiana - + FrenchPolynesia French Polynesia - + Gabon Gabon - + Gambia Gambia - + Georgia Georgia - + Germany Germany - + Ghana Ghana - + Gibraltar Gibraltar - + Greece Greece - + Greenland Greenland - + Grenada Grenada - + Guadeloupe Guadeloupe - + Guam Guam - + Guatemala Guatemala - + Guinea Guinea - + GuineaBissau Guinea-Bissau - + Guyana Guyana - + Haiti Haiti - + Honduras Honduras - + DemocraticRepublicOfCongo Democratic Republic Of Congo - + HongKong Hong Kong - + Hungary Hungary - + Iceland Iceland - + India India - + Indonesia Indonesia - + Iran Iran - + Iraq Iraq - + Ireland Ireland - + Israel Israel - + Italy Italie - + Jamaica Jamaica - + Japan Japan - + Jordan Jordan - + Kazakhstan Kazakhstan - + Kenya Kenya - + Kiribati Kiribati - + DemocraticRepublicOfKorea Democratic Republic Of Korea - + RepublicOfKorea Republic Of Korea - + Kuwait Kuwait - + Kyrgyzstan Kyrgyzstan - + Laos Laos - + Latvia Latvia - + Lebanon Lebanon - + Lesotho Lesotho - + Liberia Liberia - + Libya Libya - + Liechtenstein Liechtenstein - + Lithuania Lithuania - + Luxembourg Luxembourg - + Macau Macau - + Macedonia Macedonia - + Madagascar Madagascar - + Malawi Malawi - + Malaysia Malaysia - + Maldives Maldives - + Mali Mali - + Malta Malta - + MarshallIslands Marshall Islands - + Martinique Martinique - + Mauritania Mauritania - + Mauritius Mauritius - + Mayotte Mayotte - + Mexico Mexico - + Micronesia Micronesia - + Moldova Moldova - + Monaco Monaco - + Mongolia Mongolia - + Montenegro Montenegro - + Montserrat Montserrat - + Morocco Morocco - + Mozambique Mozambique - + Myanmar Myanmar - + Namibia Namibia - + NauruCountry Nauru Country - + Nepal Nepal - + Netherlands Netherlands - + NewCaledonia New-Caledonia - + NewZealand New-Zealand - + Nicaragua Nicaragua - + Niger Niger - + Nigeria Nigeria - + Niue Niue - + NorfolkIsland Norfolk Island - + NorthernMarianaIslands Northern Mariana Islands - + Norway Norway - + Oman Oman - + Pakistan Pakistan - + Palau Palau - + PalestinianTerritories Palestinian Territories - + Panama Panama - + PapuaNewGuinea Papua-New-Guinea - + Paraguay Paraguay - + Peru Peru - + Philippines Philippines - + Poland Poland - + Portugal Portugal - + PuertoRico Puerto Rico - + Qatar Qatar - + Reunion Reunion - + Romania Romania - + RussianFederation Russian Federation - + Rwanda Rwanda - + SaintHelena Saint-Helena - + SaintKittsAndNevis Saint-Kitts-And-Nevis - + SaintLucia Saint-Lucia - + SaintPierreAndMiquelon Saint-Pierre-And-Miquelon - + SaintVincentAndTheGrenadines Saint-Vincent And The Grenadines - + Samoa Samoa - + SanMarino San-Marino - + SaoTomeAndPrincipe Sao Tome-And-Principe - + SaudiArabia Saudi Arabia - + Senegal Senegal - + Serbia Serbia - + Seychelles Seychelles - + SierraLeone Sierra Leone - + Singapore Singapore - + Slovakia Slovakia - + Slovenia Slovenia - + SolomonIslands Solomon Islands - + Somalia Somalia - + SouthAfrica South Africa - + Spain Spain - + SriLanka Sri Lanka - + Sudan Sudan - + Suriname Suriname - + Swaziland Swaziland - + Sweden Sweden - + Switzerland Switzerland - + Syria Syria - + Taiwan Taiwan - + Tajikistan Tajikistan - + Tanzania Tanzania - + Thailand Thailand - + Togo Togo - + Tokelau Tokelau - + Tonga Tonga - + TrinidadAndTobago Trinidad-And-Tobago - + Tunisia Tunisia - + Turkey Turkey - + Turkmenistan Turkmenistan - + TurksAndCaicosIslands Turks And Caicos Islands - + Tuvalu Tuvalu - + Uganda Uganda - + Ukraine Ukraine - + UnitedArabEmirates United Arab Emirates - + UnitedKingdom United-Kingdom - + UnitedStates United-States - + Uruguay Uruguay - + Uzbekistan Uzbekistan - + Vanuatu Vanuatu - + Venezuela Venezuela - + Vietnam Vietnam - + WallisAndFutunaIslands Wallis And Futuna Islands - + Yemen Yemen - + Zambia Zambia - + Zimbabwe Zimbabwe diff --git a/Linphone/data/languages/fr_FR.ts b/Linphone/data/languages/fr_FR.ts index 4d003ee95..eb5776f14 100644 --- a/Linphone/data/languages/fr_FR.ts +++ b/Linphone/data/languages/fr_FR.ts @@ -523,74 +523,74 @@ App - + remote_provisioning_dialog Voulez-vous télécharger et appliquer la configuration depuis cette adresse ? Voulez-vous télécharger et appliquer la configuration depuis cette adresse ? - + application_description "A free and open source SIP video-phone." A free and open source SIP video-phone. - + command_line_arg_order "Send an order to the application towards a command line" Send an order to the application towards a command line - + command_line_option_show_help Show this help - + command_line_option_show_app_version Afficher la version de l'application - + command_line_option_config_to_fetch "Specify the linphone configuration file to be fetched. It will be merged with the current configuration." Specify the linphone configuration file to be fetched. It will be merged with the current configuration. - + command_line_option_config_to_fetch_arg "URL, path or file" URL, path or file - + command_line_option_minimized Minimiser - + command_line_option_log_to_stdout Log to stdout some debug information while running - + command_line_option_print_app_logs_only "Print only logs from the application" Print only logs from the application - + hide_action "Cacher" "Afficher" Cacher - + show_action Afficher - + quit_action "Quitter" Quitter @@ -1699,31 +1699,30 @@ ChatListView - + chat_message_is_writing_info %1 is writing… %1 est en train d'écrire… - + chat_message_draft_sending_text - Draft : %1 Brouillon : %1 - + chat_room_delete "Supprimer" Supprimer - + chat_list_delete_chat_popup_title Delete the chat ? Supprimer la conversation ? - + chat_list_delete_chat_popup_message This chat and all its messages will be deleted. Do You want to continue ? La conversation et tous ses messages seront supprimés. Souhaitez-vous continuer ? @@ -1732,25 +1731,31 @@ ChatMessage - + + chat_message_copy_selection + "Copy selection" + Copier la sélection + + + chat_message_copy "Copy" Copier - + chat_message_copied_to_clipboard_title Copied Copié - + chat_message_copied_to_clipboard_toast "to clipboard" dans le presse-papiers - + chat_message_delete "Delete" Supprimer @@ -1759,13 +1764,13 @@ ChatMessageCore - + info_toast_deleted_title Deleted Supprimé - + info_toast_deleted_message The message has been deleted Le message a été supprimé @@ -1976,13 +1981,13 @@ en bout. Seul votre correspondant peut les déchiffrer. Contact - + information_popup_error_title Erreur Erreur - + information_popup_voicemail_address_undefined_message L'URI de messagerie vocale n'est pas définie. L'URI de messagerie vocale n'est pas définie. @@ -2663,6 +2668,15 @@ en bout. Seul votre correspondant peut les déchiffrer. Validation chiffrement + + FileView + + + fileTransferCancel + 'Cancel' : Message link to cancel a transfer (upload/download) + Annuler + + FriendCore @@ -4389,22 +4403,22 @@ Pour les activer dans un projet commercial, merci de nous contacter.L'appel de groupe n'a pas pu être créé - + unknown_audio_device_name Appareil inconnu - + conference_invitation Invitation à une réunion - + conference_invitation_cancelled Annulation d'une réunion - + conference_invitation_updated Modification d'une réunion @@ -4412,41 +4426,41 @@ Pour les activer dans un projet commercial, merci de nous contacter. Utils - + contact_presence_status_available Disponible - + contact_presence_status_busy Occupé - + contact_presence_status_do_not_disturb Ne pas déranger - + contact_presence_status_offline Hors ligne - + contact_presence_status_away Inactif/Absent - + information_popup_call_not_created_message "L'appel n'a pas pu être créé" L'appel n'a pas pu être créé - - - - + + + + information_popup_error_title Error ---------- @@ -4454,12 +4468,12 @@ Failed to create 1-1 conversation with %1 ! Erreur - + information_popup_group_call_not_created_message L'appel de groupe n'a pas pu être créé - + number_of_years %n an(s) @@ -4468,7 +4482,7 @@ Failed to create 1-1 conversation with %1 ! - + number_of_month "%n mois" @@ -4477,7 +4491,7 @@ Failed to create 1-1 conversation with %1 ! - + number_of_weeks %n semaine(s) @@ -4486,7 +4500,7 @@ Failed to create 1-1 conversation with %1 ! - + number_of_days %n jour(s) @@ -4495,26 +4509,26 @@ Failed to create 1-1 conversation with %1 ! - + today "Aujourd'hui" Aujourd'hui - + yesterday "Hier Hier - + call_zrtp_token_verification_possible_characters "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 - - + + information_popup_chatroom_creation_error_message Failed to create 1-1 conversation with %1 ! Erreur lors de la création de la conversation avec %1 @@ -4682,1117 +4696,1117 @@ Failed to create 1-1 conversation with %1 ! country - + Afghanistan Afghanistan - + Albania Albanie - + Algeria Algérie - + AmericanSamoa Samoa américaines - + Andorra Andorre - + Angola Angola - + Anguilla Anguilla - + AntiguaAndBarbuda Antigua-et-Barbuda - + Argentina Argentine - + Armenia Arménie - + Aruba Aruba - + Australia Australie - + Austria Autriche - + Azerbaijan Azerbaïdjan - + Bahamas Bahamas - + Bahrain Bahreïn - + Bangladesh Bangladesh - + Barbados Barbade - + Belarus Biélorussie - + Belgium Belgique - + Belize Belize - + Benin Bénin - + Bermuda Bermudes - + Bhutan Bhoutan - + Bolivia Bolivie - + BosniaAndHerzegowina Bosnie-Herzégovine - + Botswana Botswana - + Brazil Brésil - + Brunei Brunéi - + Bulgaria Bulgarie - + BurkinaFaso Burkina Faso - + Burundi Burundi - + Cambodia Cambodge - + Cameroon Cameroun - + Canada Canada - + CapeVerde Cap-Vert - + CaymanIslands Îles Caïmans - + CentralAfricanRepublic République centrafricaine - + Chad Tchad - + Chile Chili - + China Chine - + Colombia Colombie - + Comoros Comores - + PeoplesRepublicOfCongo République populaire du Congo - + CookIslands Îles Cook - + CostaRica Costa Rica - + IvoryCoast Côte d'Ivoire - + Croatia Croatie - + Cuba Cuba - + Cyprus Chypre - + CzechRepublic République Tchèque - + Denmark Danemark - + Djibouti Djibouti - + Dominica Dominique - + DominicanRepublic République dominicaine - + Ecuador Équateur - + Egypt Égypte - + ElSalvador El Salvador - + EquatorialGuinea Guinée équatoriale - + Eritrea Érythrée - + Estonia Estonie - + Ethiopia Éthiopie - + FalklandIslands Îles Falkland - + FaroeIslands Îles Féroé - + Fiji Fidji - + Finland Finlande - + France France - + FrenchGuiana Guyane française - + FrenchPolynesia Polynésie française - + Gabon Gabon - + Gambia Gambie - + Georgia Géorgie - + Germany Allemagne - + Ghana Ghana - + Gibraltar Gibraltar - + Greece Grèce - + Greenland Groenland - + Grenada Grenade - + Guadeloupe Guadeloupe - + Guam Guam - + Guatemala Guatemala - + Guinea Guinée - + GuineaBissau Guinée-Bissau - + Guyana Guyana - + Haiti Haïti - + Honduras Honduras - + DemocraticRepublicOfCongo République démocratique du Congo - + HongKong Hong Kong - + Hungary Hongrie - + Iceland Islande - + India Inde - + Indonesia Indonésie - + Iran Iran - + Iraq Irak - + Ireland Irlande - + Israel Israël - + Italy Italie - + Jamaica Jamaïque - + Japan Japon - + Jordan Jordanie - + Kazakhstan Kazakhstan - + Kenya Kenya - + Kiribati Kiribati - + DemocraticRepublicOfKorea Corée du Nord - + RepublicOfKorea Corée du Sud - + Kuwait Koweït - + Kyrgyzstan Kirghizistan - + Laos Laos - + Latvia Lettonie - + Lebanon Liban - + Lesotho Lesotho - + Liberia Libéria - + Libya Libye - + Liechtenstein Liechtenstein - + Lithuania Lituanie - + Luxembourg Luxembourg - + Macau Macao - + Macedonia Macédoine - + Madagascar Madagascar - + Malawi Malawi - + Malaysia Malaisie - + Maldives Maldives - + Mali Mali - + Malta Malte - + MarshallIslands Îles Marshall - + Martinique Martinique - + Mauritania Mauritanie - + Mauritius Maurice - + Mayotte Mayotte - + Mexico Mexique - + Micronesia Micronésie - + Moldova Moldavie - + Monaco Monaco - + Mongolia Mongolie - + Montenegro Montenegro - + Montserrat Montserrat - + Morocco Maroc - + Mozambique Mozambique - + Myanmar Myanmar - + Namibia Namibie - + NauruCountry Nauru - + Nepal Népal - + Netherlands Pays-Bas - + NewCaledonia Nouvelle-Calédonie - + NewZealand Nouvelle-Zélande - + Nicaragua Nicaragua - + Niger Niger - + Nigeria Nigeria - + Niue Niué - + NorfolkIsland Île Norfolk - + NorthernMarianaIslands Îles Mariannes du Nord - + Norway Norvège - + Oman Oman - + Pakistan Pakistan - + Palau Palaos - + PalestinianTerritories Palestine - + Panama Panama - + PapuaNewGuinea Papouasie-Nouvelle-Guinée - + Paraguay Paraguay - + Peru Pérou - + Philippines Philippines - + Poland Pologne - + Portugal Portugal - + PuertoRico Porto Rico - + Qatar Qatar - + Reunion La Réunion - + Romania Roumanie - + RussianFederation Russie - + Rwanda Rwanda - + SaintHelena Sainte-Hélène - + SaintKittsAndNevis Saint-Christophe-et-Niévès - + SaintLucia Sainte-Lucie - + SaintPierreAndMiquelon Saint-Pierre-et-Miquelon - + SaintVincentAndTheGrenadines Saint-Vincent et les Grenadines - + Samoa Samoa - + SanMarino Saint-Marin - + SaoTomeAndPrincipe Sao Tomé-et-Principe - + SaudiArabia Arabie saoudite - + Senegal Sénégal - + Serbia Serbie - + Seychelles Seychelles - + SierraLeone Sierra Leone - + Singapore Singapour - + Slovakia Slovaquie - + Slovenia Slovénie - + SolomonIslands Îles Salomon - + Somalia Somalie - + SouthAfrica Afrique du Sud - + Spain Espagne - + SriLanka Sri Lanka - + Sudan Soudan - + Suriname Suriname - + Swaziland Eswatini - + Sweden Suède - + Switzerland Suisse - + Syria Syrie - + Taiwan Taïwan - + Tajikistan Tadjikistan - + Tanzania Tanzanie - + Thailand Thaïlande - + Togo Togo - + Tokelau Tokelau - + Tonga Tonga - + TrinidadAndTobago Trinité-et-Tobago - + Tunisia Tunisie - + Turkey Turquie - + Turkmenistan Turkménistan - + TurksAndCaicosIslands Îles Turks et Caïques - + Tuvalu Tuvalu - + Uganda Ouganda - + Ukraine Ukraine - + UnitedArabEmirates Émirats arabes unis - + UnitedKingdom Royaume-Uni - + UnitedStates États-Unis - + Uruguay Uruguay - + Uzbekistan Ouzbékistan - + Vanuatu Vanuatu - + Venezuela Venezuela - + Vietnam Vietnam - + WallisAndFutunaIslands Wallis et Futuna - + Yemen Yémen - + Zambia Zambie - + Zimbabwe Zimbabwe diff --git a/Linphone/model/CMakeLists.txt b/Linphone/model/CMakeLists.txt index 0d4c6e0c1..4bed192b9 100644 --- a/Linphone/model/CMakeLists.txt +++ b/Linphone/model/CMakeLists.txt @@ -13,6 +13,7 @@ list(APPEND _LINPHONEAPP_SOURCES model/chat/ChatModel.cpp model/chat/message/ChatMessageModel.cpp + model/chat/message/content/ChatMessageContentModel.cpp model/conference/ConferenceInfoModel.cpp model/conference/ConferenceModel.cpp @@ -42,6 +43,7 @@ list(APPEND _LINPHONEAPP_SOURCES model/tool/ToolModel.cpp + model/tool/VfsUtils.cpp model/videoSource/VideoSourceDescriptorModel.cpp diff --git a/Linphone/model/chat/message/ChatMessageModel.hpp b/Linphone/model/chat/message/ChatMessageModel.hpp index f6f0b6325..a0f83d9b4 100644 --- a/Linphone/model/chat/message/ChatMessageModel.hpp +++ b/Linphone/model/chat/message/ChatMessageModel.hpp @@ -130,4 +130,4 @@ private: void onEphemeralMessageDeleted(const std::shared_ptr &message) override; }; -#endif +#endif \ No newline at end of file diff --git a/Linphone/model/chat/message/content/ChatMessageContentModel.cpp b/Linphone/model/chat/message/content/ChatMessageContentModel.cpp new file mode 100644 index 000000000..a373b38f0 --- /dev/null +++ b/Linphone/model/chat/message/content/ChatMessageContentModel.cpp @@ -0,0 +1,159 @@ +/* + * 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 "ChatMessageContentModel.hpp" + +#include +#include +#include +#include + +#include "core/App.hpp" +#include "tool/providers/ExternalImageProvider.hpp" +#include "tool/providers/ThumbnailProvider.hpp" + +#include "model/chat/message/ChatMessageModel.hpp" +#include "model/conference/ConferenceInfoModel.hpp" +#include "model/core/CoreModel.hpp" +#include "model/setting/SettingsModel.hpp" + +#include "tool/Utils.hpp" + +// ============================================================================= + +DEFINE_ABSTRACT_OBJECT(ChatMessageContentModel) + +ChatMessageContentModel::ChatMessageContentModel(std::shared_ptr content, + std::shared_ptr chatMessageModel) { + mChatMessageModel = chatMessageModel; + mContent = content; + assert(content); + if (content->isFile() || content->isFileEncrypted() || content->isFileTransfer()) { + createThumbnail(); + } + if (mChatMessageModel) + connect(mChatMessageModel.get(), &ChatMessageModel::msgStateChanged, this, + [this] { emit messageStateChanged(mChatMessageModel->getState()); }); +} + +ChatMessageContentModel::~ChatMessageContentModel() { + mustBeInLinphoneThread("~" + getClassName()); +} + +// Create a thumbnail from the first content that have a file +void ChatMessageContentModel::createThumbnail() { + auto path = Utils::coreStringToAppString(mContent->getFilePath()); + if (!path.isEmpty()) { + auto isVideo = Utils::isVideo(path); + emit wasDownloadedChanged(mContent, QFileInfo(path).isFile()); + emit thumbnailChanged(QStringLiteral("image://%1/%2").arg(ThumbnailProvider::ProviderId).arg(path)); + emit filePathChanged(mContent, path); + } +} + +void ChatMessageContentModel::removeDownloadedFile(QString filePath) { + if (!filePath.isEmpty()) { + QFile(filePath).remove(); + } +} + +void ChatMessageContentModel::downloadFile(const QString &name) { + if (!mChatMessageModel) return; + switch (mChatMessageModel->getState()) { + case linphone::ChatMessage::State::Delivered: + case linphone::ChatMessage::State::DeliveredToUser: + case linphone::ChatMessage::State::Displayed: + case linphone::ChatMessage::State::FileTransferDone: + break; + case linphone::ChatMessage::State::FileTransferInProgress: + return; + default: + qWarning() << QStringLiteral("Wrong message state when requesting downloading, state=.") + << LinphoneEnums::fromLinphone(mChatMessageModel->getState()); + } + bool soFarSoGood; + const QString safeFilePath = Utils::getSafeFilePath( + QStringLiteral("%1%2").arg(App::getInstance()->getSettings()->getDownloadFolder()).arg(name), &soFarSoGood); + + if (!soFarSoGood) { + qWarning() << QStringLiteral("Unable to create safe file path for: %1.").arg(name); + return; + } + mContent->setFilePath(Utils::appStringToCoreString(safeFilePath)); + + if (!mContent->isFileTransfer()) { + Utils::showInformationPopup( + //: Error + tr("popup_error_title"), + //: This file was already downloaded and is no more on the server. Your peer have to resend it if you want + //: to get it + tr("popup_download_error_message"), false); + } else { + if (!mChatMessageModel->getMonitor()->downloadContent(mContent)) + qWarning() << QStringLiteral("Unable to download file of entry %1.").arg(name); + } +} + +void ChatMessageContentModel::cancelDownloadFile() { + if (mChatMessageModel && mChatMessageModel->getMonitor()) { + mChatMessageModel->getMonitor()->cancelFileTransfer(); + } +} + +void ChatMessageContentModel::openFile(const QString &name, bool wasDownloaded, bool showDirectory) { + if (mChatMessageModel && + ((!wasDownloaded && !mChatMessageModel->getMonitor()->isOutgoing()) || mContent->getFilePath() == "")) { + downloadFile(name); + } else { + QFileInfo info(Utils::coreStringToAppString(mContent->getFilePath())); + showDirectory = showDirectory || !info.exists(); + if (!QDesktopServices::openUrl(QUrl( + QStringLiteral("file:///%1").arg(showDirectory ? info.absolutePath() : info.absoluteFilePath()))) && + !showDirectory) { + QDesktopServices::openUrl(QUrl(QStringLiteral("file:///%1").arg(info.absolutePath()))); + } + } +} + +bool ChatMessageContentModel::saveAs(const QString &path) { + QString cachePath = Utils::coreStringToAppString(mContent->exportPlainFile()); + bool toDelete = true; + bool result = false; + if (cachePath.isEmpty()) { + cachePath = Utils::coreStringToAppString(mContent->getFilePath()); + toDelete = false; + } + if (!cachePath.isEmpty()) { + QString decodedPath = QUrl::fromPercentEncoding(path.toUtf8()); + QFile file(cachePath); + QFile newFile(decodedPath); + if (newFile.exists()) newFile.remove(); + result = file.copy(decodedPath); + if (toDelete) file.remove(); + if (result) QDesktopServices::openUrl(QUrl(QStringLiteral("file:///%1").arg(decodedPath))); + } + + emit fileSavedChanged(result); + return result; +} + +const std::shared_ptr &ChatMessageContentModel::getContent() const { + return mContent; +} diff --git a/Linphone/model/chat/message/content/ChatMessageContentModel.hpp b/Linphone/model/chat/message/content/ChatMessageContentModel.hpp new file mode 100644 index 000000000..cb94cfb65 --- /dev/null +++ b/Linphone/model/chat/message/content/ChatMessageContentModel.hpp @@ -0,0 +1,73 @@ +/* + * 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 CHAT_MESSAGE_CONTENT_MODEL_H_ +#define CHAT_MESSAGE_CONTENT_MODEL_H_ + +#include "tool/AbstractObject.hpp" +#include + +// ============================================================================= +#include +#include +#include +#include + +class ChatMessageModel; +class ConferenceInfoModel; + +class ChatMessageContentModel : public QObject, public AbstractObject { + Q_OBJECT +public: + ChatMessageContentModel(std::shared_ptr content, + std::shared_ptr chatMessageModel); + ~ChatMessageContentModel(); + + QString getThumbnail() const; + + void setThumbnail(const QString &data); + void setWasDownloaded(bool wasDownloaded); + + void createThumbnail(); + void removeDownloadedFile(QString filePath); + + void downloadFile(const QString &name); + void cancelDownloadFile(); + void openFile(const QString &name, bool wasDownloaded, bool showDirectory = false); + bool saveAs(const QString &path); + + const std::shared_ptr &getContent() const; + +signals: + void thumbnailChanged(QString thumbnail); + void fileOffsetChanged(); + void wasDownloadedChanged(const std::shared_ptr &content, bool downloaded); + void fileSavedChanged(bool success); + void filePathChanged(const std::shared_ptr &content, QString filePath); + void messageStateChanged(linphone::ChatMessage::State state); + +private: + DECLARE_ABSTRACT_OBJECT + std::shared_ptr mContent; + std::shared_ptr mChatMessageModel; + QSharedPointer mConferenceInfoModel; +}; + +#endif \ No newline at end of file diff --git a/Linphone/model/search/MagicSearchModel.cpp b/Linphone/model/search/MagicSearchModel.cpp index 5d8b868a4..0c3cf4416 100644 --- a/Linphone/model/search/MagicSearchModel.cpp +++ b/Linphone/model/search/MagicSearchModel.cpp @@ -87,30 +87,30 @@ void MagicSearchModel::onSearchResultsReceived(const std::shared_ptrgetFriend(); auto friendsManager = FriendsManager::getInstance(); if (f) { - qDebug() << "friend exists, append to unknown map"; auto friendAddress = f->getAddress() ? f->getAddress()->clone() : nullptr; if (friendAddress) { friendAddress->clean(); + qDebug() << "friend exists, append to unknown map"; friendsManager->appendUnknownFriend(friendAddress, f); if (friendsManager->isInOtherAddresses( Utils::coreStringToAppString(friendAddress->asStringUriOnly()))) { friendsManager->removeOtherAddress(Utils::coreStringToAppString(friendAddress->asStringUriOnly())); } } - } - auto fList = f ? f->getFriendList() : nullptr; + auto fList = f->getFriendList(); - // qDebug() << log().arg("") << (f ? f->getName().c_str() : "NoFriend") << ", " - // << (result->getAddress() ? result->getAddress()->asString().c_str() : "NoAddr") << " / " - // << (fList ? fList->getDisplayName().c_str() : "NoList") << result->getSourceFlags() << " / - //" - // << (f ? f.get() : nullptr); - bool isLdap = (result->getSourceFlags() & (int)linphone::MagicSearch::Source::LdapServers) != 0; - // Do not add it into ldap_friends if it already exists in app_friends. - if (isLdap && f && - (!fList || fList->getDisplayName() != "app_friends")) { // Double check because of SDK merging that lead to - // use a ldap result as of app_friends/ldap_friends. - updateFriendListWithFriend(f, ldapFriends); + // qDebug() << log().arg("") << (f ? f->getName().c_str() : "NoFriend") << ", " + // << (result->getAddress() ? result->getAddress()->asString().c_str() : "NoAddr") << " / " + // << (fList ? fList->getDisplayName().c_str() : "NoList") << result->getSourceFlags() << " / + //" + // << (f ? f.get() : nullptr); + bool isLdap = (result->getSourceFlags() & (int)linphone::MagicSearch::Source::LdapServers) != 0; + // Do not add it into ldap_friends if it already exists in app_friends. + if (isLdap && (!fList || fList->getDisplayName() != + "app_friends")) { // Double check because of SDK merging that lead to + // use a ldap result as of app_friends/ldap_friends. + updateFriendListWithFriend(f, ldapFriends); + } } } } diff --git a/Linphone/model/setting/SettingsModel.cpp b/Linphone/model/setting/SettingsModel.cpp index e201772ab..ea3fcbf69 100644 --- a/Linphone/model/setting/SettingsModel.cpp +++ b/Linphone/model/setting/SettingsModel.cpp @@ -19,9 +19,11 @@ */ #include "SettingsModel.hpp" +#include "core/App.hpp" #include "core/path/Paths.hpp" #include "model/core/CoreModel.hpp" #include "model/tool/ToolModel.hpp" +// #include "model/tool/VfsUtils.hpp" #include "tool/Utils.hpp" // ============================================================================= @@ -486,6 +488,26 @@ void SettingsModel::setVfsEnabled(bool enabled) { emit vfsEnabledChanged(enabled); } +bool SettingsModel::getVfsEncrypted() const { + return false; + // mAppSettings.beginGroup("keychain"); + // return mAppSettings.value("enabled", false).toBool(); +} + +// void SettingsModel::setVfsEncrypted(bool encrypted, const bool deleteUserData) { +// #ifdef ENABLE_QT_KEYCHAIN +// if (getVfsEncrypted() != encrypted) { +// if (encrypted) { +// mVfsUtils.newEncryptionKeyAsync(); +// shared_ptr factory = linphone::Factory::get(); +// factory->setDownloadDir(Utils::appStringToCoreString(getDownloadFolder())); +// } else { // Remove key, stop core, delete data and initiate reboot +// mVfsUtils.needToDeleteUserData(deleteUserData); +// mVfsUtils.deleteKey(mVfsUtils.getApplicationVfsEncryptionKey()); +// } +// } +// #endif +// } // ============================================================================= // Logs. // ============================================================================= @@ -740,6 +762,17 @@ int SettingsModel::getEmojiFontSize() const { return mConfig->getInt(UiSection, "emoji_font_size", Constants::DefaultEmojiFontPointSize); } +QFont SettingsModel::getTextMessageFont() const { + QString family = Utils::coreStringToAppString(mConfig->getString( + UiSection, "text_message_font", Utils::appStringToCoreString(App::getInstance()->font().family()))); + int pointSize = getTextMessageFontSize(); + return QFont(family, pointSize); +} + +int SettingsModel::getTextMessageFontSize() const { + return mConfig->getInt(UiSection, "text_message_font_size", Constants::DefaultFontPointSize); +} + // clang-format off void SettingsModel::notifyConfigReady(){ DEFINE_NOTIFY_CONFIG_READY(disableChatFeature, DisableChatFeature) diff --git a/Linphone/model/setting/SettingsModel.hpp b/Linphone/model/setting/SettingsModel.hpp index 8fb1ab208..947d853bd 100644 --- a/Linphone/model/setting/SettingsModel.hpp +++ b/Linphone/model/setting/SettingsModel.hpp @@ -49,6 +49,7 @@ public: std::shared_ptr mConfig; bool getVfsEnabled() const; + bool getVfsEncrypted() const; void setVfsEnabled(const bool enabled); bool getVideoEnabled() const; @@ -159,6 +160,8 @@ public: QFont getEmojiFont() const; int getEmojiFontSize() const; + QFont getTextMessageFont() const; + int getTextMessageFontSize() const; // UI DECLARE_GETSET(bool, disableChatFeature, DisableChatFeature) @@ -240,6 +243,10 @@ private: MediastreamerUtils::SimpleCaptureGraph *mSimpleCaptureGraph = nullptr; int mCaptureGraphListenerCount = 0; +#ifdef ENABLE_QT_KEYCHAIN + VfsUtils mVfsUtils; +#endif + void enableCallForward(QString destination); void disableCallForward(); diff --git a/Linphone/model/tool/VfsUtils.cpp b/Linphone/model/tool/VfsUtils.cpp new file mode 100644 index 000000000..fec7369f1 --- /dev/null +++ b/Linphone/model/tool/VfsUtils.cpp @@ -0,0 +1,186 @@ +// /* +// * Copyright (c) 2010-2022 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 "VfsUtils.hpp" + +// #include +// #include +// #include + +// #include +// #include +// #include +// #include + +// #include +// #include + +// // ============================================================================= + +// VfsUtils::VfsUtils(QObject *parent) +// : QObject(parent), mReadCredentialJob(QLatin1String(APPLICATION_ID)), +// mWriteCredentialJob(QLatin1String(APPLICATION_ID)), mDeleteCredentialJob(QLatin1String(APPLICATION_ID)) { +// mReadCredentialJob.setAutoDelete(false); +// mWriteCredentialJob.setAutoDelete(false); +// mDeleteCredentialJob.setAutoDelete(false); +// } + +// void VfsUtils::deleteKey(const QString &key) { +// mDeleteCredentialJob.setKey(key); + +// QObject::connect(&mDeleteCredentialJob, &QKeychain::DeletePasswordJob::finished, [=]() { +// if (mDeleteCredentialJob.error()) { +// emit error(tr("Delete key failed: %1").arg(qPrintable(mDeleteCredentialJob.errorString()))); +// return; +// } +// emit keyDeleted(key); +// }); + +// mDeleteCredentialJob.start(); +// } + +// void VfsUtils::readKey(const QString &key) { +// mReadCredentialJob.setKey(key); +// QObject::connect(&mReadCredentialJob, &QKeychain::ReadPasswordJob::finished, [=]() { +// if (mReadCredentialJob.error()) { +// emit error(tr("Read key failed: %1").arg(qPrintable(mReadCredentialJob.errorString()))); +// return; +// } +// emit keyRead(key, mReadCredentialJob.textData()); +// }); + +// mReadCredentialJob.start(); +// } + +// void VfsUtils::writeKey(const QString &key, const QString &value) { +// mWriteCredentialJob.setKey(key); + +// QObject::connect(&mWriteCredentialJob, &QKeychain::WritePasswordJob::finished, [=]() { +// if (mWriteCredentialJob.error()) { +// emit error(tr("Write key failed: %1").arg(qPrintable(mWriteCredentialJob.errorString()))); +// return; +// } +// if (key == getApplicationVfsEncryptionKey()) updateSDKWithKey(value); +// emit keyWritten(key); +// }); + +// mWriteCredentialJob.setTextData(value); +// mWriteCredentialJob.start(); +// } + +// bool VfsUtils::needToDeleteUserData() const { +// return mNeedToDeleteUserData; +// } + +// void VfsUtils::needToDeleteUserData(const bool &need) { +// mNeedToDeleteUserData = need; +// } + +// //----------------------------------------------------------------------------------------------- + +// void VfsUtils::newEncryptionKeyAsync() { +// QString value; +// bctoolbox::RNG rng; +// auto key = rng.randomize(32); +// size_t keySize = key.size(); +// uint8_t *shaKey = new uint8_t[keySize]; +// bctbx_sha256(&key[0], key.size(), keySize, shaKey); +// for (int i = 0; i < keySize; ++i) +// value += QString::number(shaKey[i], 16); +// writeKey(getApplicationVfsEncryptionKey(), value); +// } + +// bool VfsUtils::newEncryptionKey() { +// int argc = 1; +// const char *argv = "dummy"; +// QCoreApplication vfsSetter(argc, (char **)&argv); +// VfsUtils vfs; +// QObject::connect( +// &vfs, &VfsUtils::keyWritten, &vfsSetter, [&vfsSetter, &vfs](const QString &key) { vfsSetter.quit(); }, +// Qt::QueuedConnection); +// QObject::connect( +// &vfs, &VfsUtils::error, &vfsSetter, +// [&vfsSetter](const QString &errorText) { +// qCritical() << "[VFS] " << errorText; +// vfsSetter.exit(-1); +// }, +// Qt::QueuedConnection); +// vfs.newEncryptionKeyAsync(); +// return vfsSetter.exec() != -1; +// } +// bool VfsUtils::updateSDKWithKey(int argc, char *argv[]) { +// QCoreApplication core(argc, argv); +// AppController::initQtAppDetails(); // Set settings context. +// QSettings settings; +// return updateSDKWithKey(&settings); +// } +// bool VfsUtils::updateSDKWithKey() { +// QSettings settings; +// return updateSDKWithKey(&settings); +// } + +// bool VfsUtils::updateSDKWithKey(QSettings *settings) { // Update SDK if key exists. Return true if encrypted. +// bool isEnabled = false; +// // Check in factory if it is mandatory. +// auto config = linphone::Factory::get()->createConfigWithFactory("", Paths::getFactoryConfigFilePath()); +// if (config->getBool(SettingsModel::UiSection, "vfs_encryption_enabled", false)) { +// isEnabled = true; +// } + +// settings->beginGroup("keychain"); +// bool settingsValue = settings->value("enabled", false).toBool(); +// if (isEnabled && !settingsValue) settings->setValue("enabled", isEnabled); +// else if (!isEnabled) isEnabled = settingsValue; +// if (isEnabled) { +// int argc = 1; +// const char *argv = "dummy"; +// QCoreApplication vfsSetter(argc, (char **)&argv); +// VfsUtils vfs; +// QObject::connect( +// &vfs, &VfsUtils::keyRead, &vfsSetter, +// [&vfsSetter, &vfs](const QString &key, const QString &value) { +// VfsUtils::updateSDKWithKey(value); +// vfs.mVfsEncrypted = true; +// vfsSetter.quit(); +// }, +// Qt::QueuedConnection); +// QObject::connect( +// &vfs, &VfsUtils::error, &vfsSetter, [&vfsSetter](const QString &errorText) { vfsSetter.quit(); }, +// Qt::QueuedConnection); +// vfs.readKey(vfs.getApplicationVfsEncryptionKey()); +// vfsSetter.exec(); + +// if (!vfs.mVfsEncrypted) { // Doesn't have key. +// return VfsUtils::newEncryptionKey(); // Return false on error. +// } + +// return vfs.mVfsEncrypted; +// } else return false; +// } + +// void VfsUtils::updateSDKWithKey(const QString &key) { +// std::string value = Utils::appStringToCoreString(key); +// linphone::Factory::get()->setVfsEncryption(LINPHONE_VFS_ENCRYPTION_AES256GCM128_SHA256, +// (const uint8_t *)value.c_str(), std::min(32, (int)value.length())); +// } + +// QString VfsUtils::getApplicationVfsEncryptionKey() const { +// return QString(APPLICATION_ID) + "VfsEncryption"; +// } \ No newline at end of file diff --git a/Linphone/model/tool/VfsUtils.hpp b/Linphone/model/tool/VfsUtils.hpp new file mode 100644 index 000000000..8d73aa156 --- /dev/null +++ b/Linphone/model/tool/VfsUtils.hpp @@ -0,0 +1,78 @@ +// /* +// * Copyright (c) 2010-2022 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 VFS_UTILS_H_ +// #define VFS_UTILS_H_ + +// #include "config.h" +// #include +// // #ifdef QTKEYCHAIN_USE_BUILD_INTERFACE +// // #include +// // #elif defined(QTKEYCHAIN_TARGET_NAME) +// // #define KEYCHAIN_HEADER +// // #include KEYCHAIN_HEADER +// // #else +// // #include +// // #endif +// #include +// // #include + +// // ============================================================================= + +// class VfsUtils : public QObject { +// Q_OBJECT + +// public: +// VfsUtils(QObject *parent = Q_NULLPTR); + +// Q_INVOKABLE void deleteKey(const QString &key); // Delete a key and send error() or keyDeleted() +// Q_INVOKABLE void readKey(const QString &key); // Read a key, send error() or keyStored() +// Q_INVOKABLE void writeKey(const QString &key, const QString &value); // Write a key and send error() or keyWritten() + +// void newEncryptionKeyAsync(); // Generate a key, store it and update SDK. Wait for keyWritten() or error(). + +// static bool newEncryptionKey(); // Generate a key, store it and update SDK. +// static bool updateSDKWithKey(int argc, char *argv[]); // Can be calle outside application. +// static bool updateSDKWithKey(QSettings *settings); // Update SDK if key exists. Return true if encrypted. +// static bool updateSDKWithKey(); // Need it to pass QSettings +// static void updateSDKWithKey(const QString &key); // SDK->setVfsEncryption(key) + +// QString getApplicationVfsEncryptionKey() const; // Get the key in store keys for VFS encryyption + +// bool needToDeleteUserData() const; +// void needToDeleteUserData(const bool &need); + +// signals: +// void keyDeleted(const QString &key); +// void keyRead(const QString &key, const QString &value); +// void keyWritten(const QString &key); + +// void error(const QString &errorText); + +// private: +// // QKeychain::ReadPasswordJob mReadCredentialJob; +// // QKeychain::WritePasswordJob mWriteCredentialJob; +// // QKeychain::DeletePasswordJob mDeleteCredentialJob; + +// bool mNeedToDeleteUserData = false; +// bool mVfsEncrypted = false; +// }; + +// #endif diff --git a/Linphone/tool/CMakeLists.txt b/Linphone/tool/CMakeLists.txt index 1d8c9d250..221983684 100644 --- a/Linphone/tool/CMakeLists.txt +++ b/Linphone/tool/CMakeLists.txt @@ -3,6 +3,7 @@ list(APPEND _LINPHONEAPP_SOURCES tool/EnumsToString.cpp tool/Utils.cpp tool/UriTools.cpp + tool/QExifImageHeader.cpp tool/LinphoneEnums.cpp tool/thread/SafeSharedPointer.hpp @@ -10,8 +11,11 @@ list(APPEND _LINPHONEAPP_SOURCES tool/thread/Thread.cpp tool/providers/AvatarProvider.cpp tool/providers/EmojiProvider.cpp + tool/providers/ExternalImageProvider.cpp tool/providers/ImageProvider.cpp tool/providers/ScreenProvider.cpp + tool/providers/ThumbnailProvider.cpp + tool/providers/VideoFrameGrabber.cpp tool/native/DesktopTools.hpp @@ -21,6 +25,8 @@ list(APPEND _LINPHONEAPP_SOURCES tool/file/FileDownloader.cpp tool/file/FileExtractor.cpp + tool/ui/DashRectangle.cpp + ) if (APPLE) diff --git a/Linphone/tool/Constants.hpp b/Linphone/tool/Constants.hpp index c15dd1580..c88536abf 100644 --- a/Linphone/tool/Constants.hpp +++ b/Linphone/tool/Constants.hpp @@ -133,6 +133,7 @@ public: static constexpr char PathAssistantConfig[] = "/" EXECUTABLE_NAME "/assistant/"; static constexpr char PathAvatars[] = "/avatars/"; + static constexpr char PathFiles[] = "/files/"; static constexpr char PathCaptures[] = "/" EXECUTABLE_NAME "/captures/"; static constexpr char PathCodecs[] = "/codecs/"; static constexpr char PathData[] = "/" EXECUTABLE_NAME; diff --git a/Linphone/tool/QExifImageHeader.cpp b/Linphone/tool/QExifImageHeader.cpp new file mode 100644 index 000000000..5d96ff0d4 --- /dev/null +++ b/Linphone/tool/QExifImageHeader.cpp @@ -0,0 +1,1775 @@ +/**************************************************************************** +** +** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the Qt scene graph research project. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +// This file was copied from Qt Extended 4.5 + +#include +#include +#include +#include +#include +#include +#include + +#include "Utils.hpp" + +#include "QExifImageHeader.hpp" + +/*! + \typedef QExifSRational + + A synonym for \c QPair representing a signed rational number as stored in EXIF + headers. The first integer in the pair is the numerator and the second the denominator. + */ + +/*! + \typedef QExifURational + + A synonym for \c QPair representing an unsigned rational number as stored in + EXIF headers. The first integer in the pair is the numerator and the second the denominator. + */ + +struct ExifIfdHeader { + quint16 tag; + quint16 type; + quint32 count; + union { + quint32 offset; + quint8 offsetBytes[4]; + char offsetAscii[4]; + quint16 offsetShorts[2]; + }; +}; + +QDataStream &operator>>(QDataStream &stream, ExifIfdHeader &header) { + stream >> header.tag; + stream >> header.type; + stream >> header.count; + + if (header.type == QExifValue::Byte && header.count <= 4) { + stream.readRawData(header.offsetAscii, 4); + } else if (header.type == QExifValue::Ascii && header.count <= 4) { + stream.readRawData(header.offsetAscii, 4); + } else if (header.type == QExifValue::Short && header.count <= 2) { + stream >> header.offsetShorts[0]; + stream >> header.offsetShorts[1]; + } else { + stream >> header.offset; + } + + return stream; +} + +class QExifValuePrivate : public QSharedData { +public: + QExifValuePrivate(quint16 t, int c) : type(t), count(c) { + } + + virtual ~QExifValuePrivate() { + } + + quint16 type; + int count; +}; + +class QExifByteValuePrivate : public QExifValuePrivate { +public: + QExifByteValuePrivate() : QExifValuePrivate(QExifValue::Byte, 0) { + ref.ref(); + } + + QExifByteValuePrivate(const QVector &v) : QExifValuePrivate(QExifValue::Byte, v.size()), value(v) { + } + ~QExifByteValuePrivate() { + } + QVector value; +}; + +class QExifUndefinedValuePrivate : public QExifValuePrivate { +public: + QExifUndefinedValuePrivate(const QByteArray &v) : QExifValuePrivate(QExifValue::Undefined, v.size()), value(v) { + } + + QByteArray value; +}; + +class QExifAsciiValuePrivate : public QExifValuePrivate { +public: + QExifAsciiValuePrivate(const QString &v) : QExifValuePrivate(QExifValue::Ascii, v.size() + 1), value(v) { + } + + QString value; +}; + +class QExifShortValuePrivate : public QExifValuePrivate { +public: + QExifShortValuePrivate(const QVector &v) : QExifValuePrivate(QExifValue::Short, v.size()), value(v) { + } + + QVector value; +}; + +class QExifLongValuePrivate : public QExifValuePrivate { +public: + QExifLongValuePrivate(const QVector &v) : QExifValuePrivate(QExifValue::Long, v.size()), value(v) { + } + + QVector value; +}; + +class QExifSignedLongValuePrivate : public QExifValuePrivate { +public: + QExifSignedLongValuePrivate(const QVector &v) + : QExifValuePrivate(QExifValue::SignedLong, v.size()), value(v) { + } + + QVector value; +}; + +class QExifRationalValuePrivate : public QExifValuePrivate { +public: + QExifRationalValuePrivate(const QVector &v) + : QExifValuePrivate(QExifValue::Rational, v.size()), value(v) { + } + + QVector value; +}; + +class QExifSignedRationalValuePrivate : public QExifValuePrivate { +public: + QExifSignedRationalValuePrivate(const QVector &v) + : QExifValuePrivate(QExifValue::SignedRational, v.size()), value(v) { + } + + QVector value; +}; + +Q_GLOBAL_STATIC(QExifByteValuePrivate, qExifValuePrivateSharedNull) + +/*! + \class QExifValue + \inpublicgroup QtBaseModule + \brief The QExifValue class represents data types found in EXIF image headers. + + Tag values in EXIF headers are stored as arrays of a limited number of data types. QExifValue + encapsulates a union of these types and provides conversions to and from appropriate Qt types. + + \section1 String encoding + + Most tags with string values in EXIF headers are ASCII encoded and have the Ascii value type, + but some tags allow other encodings. In this case the value type is Undefined and the encoding + of the text is given by the encoding function(). + + \section1 Date-time values + + Date-time values in EXIF headers are stored in ASCII encoded strings of the form + \c {yyyy:MM:dd HH:mm:ss}. Constructing a QExifValue from a QDateTime will perform this + conversion and likewise an appropriately formed QExifValue can be converted to a QDateTime + using the toDateTime() function. + + \sa QExifImageHeader + + \preliminary + */ + +/*! + \enum QExifValue::Type + + Enumerates the possible types of EXIF values. + + \value Byte An unsigned 8 bit integer. + \value Ascii A null terminated ascii string. + \value Short An unsigned 16 bit integer. + \value Long An unsigned 32 bit integer. + \value Rational Two unsigned 32 bit integers, representing a the numerator and denominator of an unsigned rational + number. \value Undefined An array of 8 bit integers. \value SignedLong A signed 32 bit integer. \value SignedRational + Two signed 32 bit integers representing the numerator and denominator of a signed rational number. + */ + +/*! + \enum QExifValue::TextEncoding + + Enumerates the encodings of text strings in EXIF values of Undefined type. + + \value NoEncoding An ASCII string of Ascii type. + \value AsciiEncoding An ASCII string of Undefined type. + \value JisEncoding A JIS X208-1990 string of Undefined type. + \value UnicodeEncoding A Unicode string of Undefined type. + \value UndefinedEncoding An unspecified string encoding of Undefined type. Assumed to be the local 8-bit + encoding. + */ + +/*! + Constructs a null QExifValue. + */ +QExifValue::QExifValue() : d(qExifValuePrivateSharedNull()) { +} + +/*! + Constructs a QExifValue with a \a value of type Byte. + */ +QExifValue::QExifValue(quint8 value) : d(new QExifByteValuePrivate(QVector(1, value))) { +} + +/*! + Constructs a QExifValue with an array of \a values of type Byte. + */ +QExifValue::QExifValue(const QVector &values) : d(new QExifByteValuePrivate(values)) { +} + +/*! + Constructs a QExifValue with a \a value of type Ascii or Undefined. + + If the \a encoding is NoEncoding the value will be of type Ascii, otherwise it will be Undefined and the string + encoded using the given \a encoding. + */ +QExifValue::QExifValue(const QString &value, TextEncoding encoding) : d(qExifValuePrivateSharedNull()) { + switch (encoding) { + case AsciiEncoding: + d = new QExifUndefinedValuePrivate(QByteArray::fromRawData("ASCII\0\0\0", 8) + value.toUtf8()); + break; + case JisEncoding: { + auto encoding = QStringConverter::encodingForName("JIS X 0208"); + if (encoding.has_value()) { + QStringEncoder encoder(encoding.value()); + auto encoded = encoder.encode(value); + d = new QExifUndefinedValuePrivate(QByteArray::fromRawData("JIS\0\0\0\0\0", 8) + encoded); + } + } break; + case UnicodeEncoding: { + auto encoding = QStringConverter::encodingForName("UTF-16"); + if (encoding.has_value()) { + QStringEncoder encoder(encoding.value()); + auto encoded = encoder.encode(value); + d = new QExifUndefinedValuePrivate(QByteArray::fromRawData("UNICODE\0", 8) + encoded); + } + } break; + case UndefinedEncoding: + d = new QExifUndefinedValuePrivate(QByteArray::fromRawData("\0\0\0\0\0\0\0\\0", 8) + value.toLocal8Bit()); + break; + default: + d = new QExifAsciiValuePrivate(value); + } +} + +/*! + Constructs a QExifValue with a \a value of type Short. + */ +QExifValue::QExifValue(quint16 value) : d(new QExifShortValuePrivate(QVector(1, value))) { +} + +/*! + Constructs a QExifValue with an array of \a values of type Short. + */ +QExifValue::QExifValue(const QVector &values) : d(new QExifShortValuePrivate(values)) { +} + +/*! + Constructs a QExifValue with a \a value of type Long. + */ +QExifValue::QExifValue(quint32 value) : d(new QExifLongValuePrivate(QVector(1, value))) { +} + +/*! + Constructs a QExifValue with an array of \a values of type Long. + */ +QExifValue::QExifValue(const QVector &values) : d(new QExifLongValuePrivate(values)) { +} + +/*! + Constructs a QExifValue with a \a value of type Rational. + */ +QExifValue::QExifValue(const QExifURational &value) + : d(new QExifRationalValuePrivate(QVector(1, value))) { +} + +/*! + Constructs a QExifValue with an array of \a values of type Rational. + */ +QExifValue::QExifValue(const QVector &values) : d(new QExifRationalValuePrivate(values)) { +} + +/*! + Constructs a QExifValue with a \a value of type Undefined. + */ +QExifValue::QExifValue(const QByteArray &value) : d(new QExifUndefinedValuePrivate(value)) { +} + +/*! + Constructs a QExifValue with a \a value of type SignedLong. + */ +QExifValue::QExifValue(qint32 value) : d(new QExifSignedLongValuePrivate(QVector(1, value))) { +} + +/*! + Constructs a QExifValue with an array of \a values of type SignedLong. + */ +QExifValue::QExifValue(const QVector &values) : d(new QExifSignedLongValuePrivate(values)) { +} + +/*! + Constructs a QExifValue with a \a value of type SignedRational. + */ +QExifValue::QExifValue(const QExifSRational &value) + : d(new QExifSignedRationalValuePrivate(QVector(1, value))) { +} + +/*! + Constructs a QExifValue with an array of \a values of type SignedRational. + */ +QExifValue::QExifValue(const QVector &values) : d(new QExifSignedRationalValuePrivate(values)) { +} + +/*! + Constructs a QExifValue of type Ascii with an ascii string formatted from a date-time \a value. + + Date-times are stored as strings in the format \c {yyyy:MM:dd HH:mm:ss}. + */ +QExifValue::QExifValue(const QDateTime &value) + : d(new QExifAsciiValuePrivate(value.toString(QLatin1String("yyyy:MM:dd HH:mm:ss")))) { +} + +/*! + Constructs a copy of the QExifValue \a other. + */ +QExifValue::QExifValue(const QExifValue &other) : d(other.d) { +} + +/*! + Assigns the value of \a other to a QExifValue. + */ +QExifValue &QExifValue::operator=(const QExifValue &other) { + d = other.d; + + return *this; +} + +/*! + Destroys a QExifValue. + */ +QExifValue::~QExifValue() { +} + +/*! + Compares a QExifValue to \a other. Returns true if they are the same value and false otherwise. + */ +bool QExifValue::operator==(const QExifValue &other) const { + return d == other.d; +} + +/*! + Returns true if a QExifValue has a null value and false otherwise. + */ +bool QExifValue::isNull() const { + return d == qExifValuePrivateSharedNull(); +} + +/*! + Returns the type of a QExifValue. + */ +int QExifValue::type() const { + return d->type; +} + +/*! + Returns the number of elements in a QExifValue. For ascii strings this is the length of the string + including the terminating null. + */ +int QExifValue::count() const { + return d->count; +} + +/*! + Returns the encoding of strings stored in Undefined values. + */ +QExifValue::TextEncoding QExifValue::encoding() const { + if (d->type == Undefined && d->count > 8) { + QByteArray value = static_cast(d.constData())->value; + + if (value.startsWith(QByteArray::fromRawData("ASCII\0\0\0", 8))) return AsciiEncoding; + else if (value.startsWith(QByteArray::fromRawData("JIS\0\0\0\0\0", 8))) return JisEncoding; + else if (value.startsWith(QByteArray::fromRawData("UNICODE\0", 8))) return UnicodeEncoding; + else if (value.startsWith(QByteArray::fromRawData("\0\0\0\0\0\0\0\0", 8))) return UndefinedEncoding; + } + return NoEncoding; +} + +/*! + Returns the value of a single element QExifValue of type Byte. + */ +quint8 QExifValue::toByte() const { + return d->type == Byte && d->count == 1 ? static_cast(d.constData())->value.at(0) + : 0; +} + +/*! + Returns the value of a multiple element QExifValue of type Byte. + */ +QVector QExifValue::toByteVector() const { + return d->type == Byte ? static_cast(d.constData())->value : QVector(); +} + +/*! + Returns the value of a QExifValue of type Ascii. + */ +QString QExifValue::toString() const { + switch (d->type) { + case Ascii: + return static_cast(d.constData())->value; + case Undefined: { + QByteArray string = static_cast(d.constData())->value.mid(8); + + switch (encoding()) { + case AsciiEncoding: + return QString::fromUtf8(string.constData(), string.length()); + case JisEncoding: { + auto encoding = QStringConverter::encodingForName("JIS X 0208"); + if (encoding.has_value()) { + QStringDecoder decoder(encoding.value()); + return decoder.decode(string); + } + } break; + case UnicodeEncoding: { + auto encoding = QStringConverter::encodingForName("UTF-16"); + if (encoding.has_value()) { + QStringDecoder decoder(encoding.value()); + return decoder.decode(string); + } + return QString::fromLocal8Bit(string.constData(), string.length()); + } + case UndefinedEncoding: + return QString::fromLocal8Bit(string.constData(), string.length()); + default: + break; + } + return QString(); + } + default: + return QString(); + } +} + +/*! + Returns the value of a single element QExifValue of type Byte or Short. + */ +quint16 QExifValue::toShort() const { + if (d->count == 1) { + switch (d->type) { + case Byte: + return static_cast(d.constData())->value.at(0); + case Short: + return static_cast(d.constData())->value.at(0); + } + } + return 0; +} + +/*! + Returns the value of a single element QExifValue of type Short. + */ +QVector QExifValue::toShortVector() const { + return d->type == Short ? static_cast(d.constData())->value : QVector(); +} + +/*! + Returns the value of a single element QExifValue of type Byte, Short, Long, or SignedLong. + */ +quint32 QExifValue::toLong() const { + if (d->count == 1) { + switch (d->type) { + case Byte: + return static_cast(d.constData())->value.at(0); + case Short: + return static_cast(d.constData())->value.at(0); + case Long: + return static_cast(d.constData())->value.at(0); + case SignedLong: + return quint32(static_cast(d.constData())->value.at(0)); + } + } + return 0; +} + +/*! + Returns the value of a multiple element QExifValue of type Long. + */ +QVector QExifValue::toLongVector() const { + return d->type == Long ? static_cast(d.constData())->value : QVector(); +} + +/*! + Returns the value of a multiple element QExifValue of type Rational. + */ +QExifURational QExifValue::toRational() const { + return d->type == Rational && d->count == 1 + ? static_cast(d.constData())->value.at(0) + : QExifURational(); +} + +/*! + Returns the value of a multiple element QExifValue of type Rational. + */ +QVector QExifValue::toRationalVector() const { + return d->type == Rational ? static_cast(d.constData())->value + : QVector(); +} + +/*! + Returns the value of a QExifValue of type Undefined. + */ +QByteArray QExifValue::toByteArray() const { + switch (d->type) { + case Ascii: + return static_cast(d.constData())->value.toUtf8(); + case Undefined: + return static_cast(d.constData())->value; + default: + return QByteArray(); + } +} + +/*! + Returns the value of a single element QExifValue of type Byte, Short, Long, or SignedLong. + */ +qint32 QExifValue::toSignedLong() const { + if (d->count == 1) { + switch (d->type) { + case Byte: + return static_cast(d.constData())->value.at(0); + case Short: + return static_cast(d.constData())->value.at(0); + case Long: + return qint32(static_cast(d.constData())->value.at(0)); + case SignedLong: + return static_cast(d.constData())->value.at(0); + } + } + return 0; +} + +/*! + Returns the value of a multiple element QExifValue of type SignedLong. + */ +QVector QExifValue::toSignedLongVector() const { + return d->type == SignedLong ? static_cast(d.constData())->value + : QVector(); +} + +/*! + Returns the value of a single element QExifValue of type SignedRational. + */ +QExifSRational QExifValue::toSignedRational() const { + return d->type == SignedRational && d->count == 1 + ? static_cast(d.constData())->value.at(0) + : QExifSRational(); +} + +/*! + Returns the value of a multiple element QExifValue of type SignedRational. + */ +QVector QExifValue::toSignedRationalVector() const { + return d->type == SignedRational ? static_cast(d.constData())->value + : QVector(); +} + +/*! + Returns the value of QExifValue storing a date-time. + + Date-times are stored as ascii strings in the format \c {yyyy:MM:dd HH:mm:ss}. + */ +QDateTime QExifValue::toDateTime() const { + return d->type == Ascii && d->count == 20 + ? QDateTime::fromString(static_cast(d.constData())->value, + QLatin1String("yyyy:MM:dd HH:mm:ss")) + : QDateTime(); +} + +class QExifImageHeaderPrivate { +public: + QSysInfo::Endian byteOrder; + mutable qint64 size; + QMap imageIfdValues; + QMap exifIfdValues; + QMap gpsIfdValues; + + QSize thumbnailSize; + QByteArray thumbnailData; + QExifValue thumbnailXResolution; + QExifValue thumbnailYResolution; + QExifValue thumbnailResolutionUnit; + QExifValue thumbnailOrientation; +}; + +/*! + \class QExifImageHeader + \inpublicgroup QtBaseModule + \brief The QExifImageHeader class provides functionality for reading and writing EXIF image headers. + + EXIF headers are a collection of properties that describe the image they're embedded in. + Each property is identified by a tag of which there are three kinds. \l {ImageTag}{Image tags} + which mostly describe the format (dimensions, resolution, orientation) but also include some + descriptive information (description, camera make and model, artist). \l {ExifExtendedTag} + {EXIF extended tags} which elaborate on some of the image tags and record the camera settings at + time of capture among other things. Finally there are \l {GpsTag}{GPS tags} which record the + location the image was captured. + + EXIF tags are typically found in JPEG images but may be found in other image formats. To read + headers from a JPEG image QExifImageHeader provides the loadFromJpeg() function, and the + complementary saveToJpeg() function for writing. To allow reading and writing arbitrary + formats QExifImageHeader provides the read() and write() functions which work with just the + EXIF header data itself. + + \preliminary + */ + +/*! + \enum QExifImageHeader::ImageTag + Enumerates the TIFF image tag IDs defined in the EXIF specification. + + \value ImageWidth + \value ImageLength + \value BitsPerSample + \value Compression + \value PhotometricInterpretation + \value Orientation + \value SamplesPerPixel + \value PlanarConfiguration + \value YCbCrSubSampling + \value XResolution + \value YResolution + \value ResolutionUnit + \value StripOffsets + \value RowsPerStrip + \value StripByteCounts + \value TransferFunction + \value WhitePoint + \value PrimaryChromaciticies + \value YCbCrCoefficients + \value ReferenceBlackWhite + \value DateTime + \value ImageDescription + \value Make + \value Model + \value Software + \value Artist + \value Copyright + */ + +/*! + \enum QExifImageHeader::ExifExtendedTag + Enumerates the extended EXIF tag IDs defined in the EXIF specification. + + \value ExifVersion + \value FlashPixVersion + \value ColorSpace + \value ComponentsConfiguration + \value CompressedBitsPerPixel + \value PixelXDimension + \value PixelYDimension + \value MakerNote + \value UserComment + \value RelatedSoundFile + \value DateTimeOriginal + \value DateTimeDigitized + \value SubSecTime + \value SubSecTimeOriginal + \value SubSecTimeDigitized + \value ImageUniqueId + \value ExposureTime + \value FNumber + \value ExposureProgram + \value SpectralSensitivity + \value ISOSpeedRatings + \value Oecf + \value ShutterSpeedValue + \value ApertureValue + \value BrightnessValue + \value ExposureBiasValue + \value MaxApertureValue + \value SubjectDistance + \value MeteringMode + \value LightSource + \value Flash + \value FocalLength + \value SubjectArea + \value FlashEnergy + \value SpatialFrequencyResponse + \value FocalPlaneXResolution + \value FocalPlaneYResolution + \value FocalPlaneResolutionUnit + \value SubjectLocation + \value ExposureIndex + \value SensingMethod + \value FileSource + \value SceneType + \value CfaPattern + \value CustomRendered + \value ExposureMode + \value WhiteBalance + \value DigitalZoomRatio + \value FocalLengthIn35mmFilm + \value SceneCaptureType + \value GainControl + \value Contrast + \value Saturation + \value Sharpness + \value DeviceSettingDescription + \value SubjectDistanceRange + */ + +/*! + \enum QExifImageHeader::GpsTag + Enumerates the GPS tag IDs from the EXIF specification. + + \value GpsVersionId + \value GpsLatitudeRef + \value GpsLatitude + \value GpsLongitudeRef + \value GpsLongitude + \value GpsAltitudeRef + \value GpsAltitude + \value GpsTimeStamp + \value GpsSatellites + \value GpsStatus + \value GpsMeasureMode + \value GpsDop + \value GpsSpeedRef + \value GpsSpeed + \value GpsTrackRef + \value GpsTrack + \value GpsImageDirectionRef + \value GpsImageDirection + \value GpsMapDatum + \value GpsDestLatitudeRef + \value GpsDestLatitude + \value GpsDestLongitudeRef + \value GpsDestLongitude + \value GpsDestBearingRef + \value GpsDestBearing + \value GpsDestDistanceRef + \value GpsDestDistance + \value GpsProcessingMethod + \value GpsAreaInformation + \value GpsDateStamp + \value GpsDifferential + */ + +/*! + Constructs a new EXIF image data editor. + */ +QExifImageHeader::QExifImageHeader() : d(new QExifImageHeaderPrivate) { + d->byteOrder = QSysInfo::ByteOrder; + d->size = -1; +} + +/*! + Constructs a new EXIF image data editor and reads the meta-data from a JPEG image with the given \a fileName. + */ +QExifImageHeader::QExifImageHeader(const QString &fileName) : d(new QExifImageHeaderPrivate) { + d->byteOrder = QSysInfo::ByteOrder; + d->size = -1; + + loadFromJpeg(fileName); +} + +/*! + Destroys an EXIF image data editor. + */ +QExifImageHeader::~QExifImageHeader() { + clear(); + + delete d; +} + +/*! + Reads meta-data from a JPEG image with the given \a fileName. + + Returns true if the data was successfully parsed and false otherwise. + */ +bool QExifImageHeader::loadFromJpeg(const QString &fileName) { + QFile file(fileName); + + if (file.open(QIODevice::ReadOnly)) return loadFromJpeg(&file); + else return false; +} + +/*! + Reads meta-data from an I/O \a device containing a JPEG image. + + Returns true if the data was successfully parsed and false otherwise. + */ +bool QExifImageHeader::loadFromJpeg(QIODevice *device) { + clear(); + + QByteArray exifData = extractExif(device); + + if (!exifData.isEmpty()) { + QBuffer buffer(&exifData); + + return buffer.open(QIODevice::ReadOnly) && read(&buffer); + } + + return false; +} + +/*! + Saves meta-data to a JPEG image with the given \a fileName. + + Returns true if the data was successfully written. + */ +bool QExifImageHeader::saveToJpeg(const QString &fileName) const { + QFile file(fileName); + + if (file.open(QIODevice::ReadWrite)) return saveToJpeg(&file); + else return false; +} + +/*! + Save meta-data to the given I/O \a device. + + The device must be non-sequential and already contain a valid JPEG image. + + Returns true if the data was successfully written. + */ +bool QExifImageHeader::saveToJpeg(QIODevice *device) const { + if (device->isSequential()) return false; + + QByteArray exif; + + { + QBuffer buffer(&exif); + + if (!buffer.open(QIODevice::WriteOnly)) return false; + + write(&buffer); + + buffer.close(); + + exif = QByteArray::fromRawData("Exif\0\0", 6) + exif; + } + + QDataStream stream(device); + + stream.setByteOrder(QDataStream::BigEndian); + + if (device->read(2) != "\xFF\xD8") // Not a valid JPEG image. + return false; + + quint16 segmentId; + quint16 segmentLength; + + stream >> segmentId; + stream >> segmentLength; + + if (segmentId == 0xFFE0) { + QByteArray jfif = device->read(segmentLength - 2); + + if (!jfif.startsWith("JFIF")) return false; + + stream >> segmentId; + stream >> segmentLength; + + if (segmentId == 0xFFE1) { + QByteArray oldExif = device->read(segmentLength - 2); + + if (!oldExif.startsWith("Exif")) return false; + + int dSize = oldExif.size() - exif.size(); + + if (dSize > 0) exif += QByteArray(dSize, '\0'); + + QByteArray remainder = device->readAll(); + + device->seek(0); + + stream << quint16(0xFFD8); // SOI + stream << quint16(0xFFE0); // APP0 + stream << quint16(jfif.size() + 2); + device->write(jfif); + stream << quint16(0xFFE1); // APP1 + stream << quint16(exif.size() + 2); + device->write(exif); + device->write(remainder); + } else { + QByteArray remainder = device->readAll(); + + device->seek(0); + + stream << quint16(0xFFD8); // SOI + stream << quint16(0xFFE0); // APP0 + stream << quint16(jfif.size() + 2); + device->write(jfif); + stream << quint16(0xFFE1); // APP1 + stream << quint16(exif.size() + 2); + device->write(exif); + stream << quint16(0xFFE0); // APP0 + stream << segmentId; + stream << segmentLength; + device->write(remainder); + } + } else if (segmentId == 0xFFE1) { + QByteArray oldExif = device->read(segmentLength - 2); + + if (!oldExif.startsWith("Exif")) return false; + + int dSize = oldExif.size() - exif.size(); + + if (dSize > 0) exif += QByteArray(dSize, '\0'); + + QByteArray remainder = device->readAll(); + + device->seek(0); + + stream << quint16(0xFFD8); // SOI + stream << quint16(0xFFE1); // APP1 + stream << quint16(exif.size() + 2); + device->write(exif); + device->write(remainder); + } else { + QByteArray remainder = device->readAll(); + + device->seek(0); + + stream << quint16(0xFFD8); // SOI + stream << quint16(0xFFE1); // APP1 + stream << quint16(exif.size() + 2); + device->write(exif); + stream << segmentId; + stream << segmentLength; + device->write(remainder); + } + + return true; +} + +/*! + Returns the byte order of EXIF file. + */ +QSysInfo::Endian QExifImageHeader::byteOrder() const { + return d->byteOrder; +} + +quint32 QExifImageHeader::sizeOf(const QExifValue &value) const { + switch (value.type()) { + case QExifValue::Byte: + case QExifValue::Undefined: + return quint32(value.count() > 4 ? 12 + value.count() : 12); + case QExifValue::Ascii: + return quint32(value.count() > 4 ? 12 + value.count() : 12); + case QExifValue::Short: + return value.count() > 2 ? quint32(12 + quint32(value.count()) * sizeof(quint16)) : 12; + case QExifValue::Long: + case QExifValue::SignedLong: + return value.count() > 1 ? quint32(12 + quint32(value.count()) * sizeof(quint32)) : 12; + case QExifValue::Rational: + case QExifValue::SignedRational: + return value.count() > 0 ? quint32(12 + quint32(value.count()) * sizeof(quint32) * 2) : 12; + default: + return 0; + } +} + +template +quint32 QExifImageHeader::calculateSize(const QMap &values) const { + quint32 size = sizeof(quint16); + + foreach (const QExifValue &value, values) + size += sizeOf(value); + + return size; +} + +/*! + Returns the size of EXIF data in bytes. + */ +qint64 QExifImageHeader::size() const { + if (d->size == -1) { + d->size = 2 + // Byte Order + 2 + // Marker + 4 + // Image Ifd offset + 12 + // ExifIfdPointer Ifd + 4 + // Thumbnail Ifd offset + calculateSize(d->imageIfdValues) + // Image headers and values. + calculateSize(d->exifIfdValues); // Exif headers and values. + + if (!d->gpsIfdValues.isEmpty()) { + d->size += 12 + // GpsInfoIfdPointer Ifd + calculateSize(d->gpsIfdValues); // Gps headers and values. + } + + if (!d->thumbnailData.isEmpty()) { + d->size += 2 + // Thumbnail Ifd count + 12 + // Compression Ifd + 20 + // XResolution Ifd + 20 + // YResolution Ifd + 12 + // ResolutionUnit Ifd + 12 + // JpegInterchangeFormat Ifd + 12 + // JpegInterchangeFormatLength Ifd + d->thumbnailData.size(); // Thumbnail data size. + } + } + + return d->size; +} + +/*! + Clears all image meta-data. + */ +void QExifImageHeader::clear() { + d->imageIfdValues.clear(); + d->exifIfdValues.clear(); + d->gpsIfdValues.clear(); + d->thumbnailData.clear(); + + d->size = -1; +} + +/*! + Returns a list of all image tags in an EXIF header. + */ +QList QExifImageHeader::imageTags() const { + return d->imageIfdValues.keys(); +} + +/*! + Returns a list of all extended EXIF tags in a header. + */ +QList QExifImageHeader::extendedTags() const { + return d->exifIfdValues.keys(); +} + +/*! + Returns a list of all GPS tags in an EXIF header. + */ +QList QExifImageHeader::gpsTags() const { + return d->gpsIfdValues.keys(); +} + +/*! + Returns true if an EXIf header contains a value for an image \a tag and false otherwise. + */ +bool QExifImageHeader::contains(ImageTag tag) const { + return d->imageIfdValues.contains(tag); +} + +/*! + Returns true if a header contains a a value for an extended EXIF \a tag and false otherwise. + */ +bool QExifImageHeader::contains(ExifExtendedTag tag) const { + return d->exifIfdValues.contains(tag); +} + +/*! + Returns true if an EXIf header contains a value for a GPS \a tag and false otherwise. + */ +bool QExifImageHeader::contains(GpsTag tag) const { + return d->gpsIfdValues.contains(tag); +} + +/*! + Removes the value for an image \a tag. + */ +void QExifImageHeader::remove(ImageTag tag) { + d->imageIfdValues.remove(tag); + + d->size = -1; +} + +/*! + Removes the value for an extended EXIF \a tag. + */ +void QExifImageHeader::remove(ExifExtendedTag tag) { + d->exifIfdValues.remove(tag); + + d->size = -1; +} + +/*! + Removes the value for a GPS \a tag. + */ +void QExifImageHeader::remove(GpsTag tag) { + d->gpsIfdValues.remove(tag); + + d->size = -1; +} + +/*! + Returns the value for an image \a tag. + */ +QExifValue QExifImageHeader::value(ImageTag tag) const { + return d->imageIfdValues.value(tag); +} + +/*! + Returns the value for an extended EXIF \a tag. + */ +QExifValue QExifImageHeader::value(ExifExtendedTag tag) const { + return d->exifIfdValues.value(tag); +} + +/*! + Returns the value for a GPS tag. + */ +QExifValue QExifImageHeader::value(GpsTag tag) const { + return d->gpsIfdValues.value(tag); +} + +/*! + Sets the \a value for an image \a tag. + */ +void QExifImageHeader::setValue(ImageTag tag, const QExifValue &value) { + d->imageIfdValues[tag] = value; + + d->size = -1; +} + +/*! + Sets the \a value for an extended EXIF \a tag. + */ +void QExifImageHeader::setValue(ExifExtendedTag tag, const QExifValue &value) { + d->exifIfdValues[tag] = value; + + d->size = -1; +} + +/*! + Sets the \a value for an GPS \a tag. + */ +void QExifImageHeader::setValue(GpsTag tag, const QExifValue &value) { + d->gpsIfdValues[tag] = value; + + d->size = -1; +} + +/*! + Returns the image thumbnail. + */ +QImage QExifImageHeader::thumbnail() const { + QImage image; + + image.loadFromData(d->thumbnailData, "JPG"); + + if (!d->thumbnailOrientation.isNull()) { + switch (d->thumbnailOrientation.toShort()) { + case 1: + return image; + case 2: + return image.transformed(QTransform().rotate(180, Qt::YAxis)); + case 3: + return image.transformed(QTransform().rotate(180, Qt::ZAxis)); + case 4: + return image.transformed(QTransform().rotate(180, Qt::XAxis)); + case 5: + return image.transformed(QTransform().rotate(180, Qt::YAxis).rotate(90, Qt::ZAxis)); + case 6: + return image.transformed(QTransform().rotate(90, Qt::ZAxis)); + case 7: + return image.transformed(QTransform().rotate(180, Qt::XAxis).rotate(90, Qt::ZAxis)); + case 8: + return image.transformed(QTransform().rotate(270, Qt::ZAxis)); + } + } + + return image; +} + +/*! + Sets the image \a thumbnail. + */ +void QExifImageHeader::setThumbnail(const QImage &thumbnail) { + if (!thumbnail.isNull()) { + QBuffer buffer; + + if (buffer.open(QIODevice::WriteOnly) && thumbnail.save(&buffer, "JPG")) { + buffer.close(); + + d->thumbnailSize = thumbnail.size(); + d->thumbnailData = buffer.data(); + d->thumbnailOrientation = QExifValue(); + } + } else { + d->thumbnailSize = QSize(); + d->thumbnailData = QByteArray(); + } + + d->size = -1; +} + +QByteArray QExifImageHeader::extractExif(QIODevice *device) const { + QDataStream stream(device); + + stream.setByteOrder(QDataStream::BigEndian); + + if (device->read(2) != "\xFF\xD8") return QByteArray(); + + while (device->read(2) != "\xFF\xE1") { + if (device->atEnd()) return QByteArray(); + + quint16 length; + + stream >> length; + + device->seek(device->pos() + length - 2); + } + + quint16 length; + + stream >> length; + + if (device->read(4) != "Exif") return QByteArray(); + + device->read(2); + + return device->read(length - 8); +} + +QList QExifImageHeader::readIfdHeaders(QDataStream &stream) const { + QList headers; + + quint16 count; + + stream >> count; + + for (quint16 i = 0; i < count; i++) { + ExifIfdHeader header; + + stream >> header; + + headers.append(header); + } + + return headers; +} + +QExifValue QExifImageHeader::readIfdValue(QDataStream &stream, int startPos, const ExifIfdHeader &header) const { + switch (header.type) { + case QExifValue::Byte: { + QVector value(int(header.count)); + + if (header.count > 4) { + stream.device()->seek(startPos + qint64(header.offset)); + + for (quint32 i = 0; i < header.count; i++) + stream >> value[int(i)]; + } else { + for (quint32 i = 0; i < header.count; i++) + value[int(i)] = header.offsetBytes[i]; + } + return QExifValue(value); + } + case QExifValue::Undefined: + if (header.count > 4) { + stream.device()->seek(startPos + qint64(header.offset)); + + return QExifValue(stream.device()->read(header.count)); + } else { + return QExifValue(QByteArray::fromRawData(header.offsetAscii, int(header.count))); + } + case QExifValue::Ascii: + if (header.count > 4) { + stream.device()->seek(startPos + qint64(header.offset)); + + QByteArray ascii = stream.device()->read(header.count); + + return QExifValue(QString::fromUtf8(ascii.constData(), ascii.size() - 1)); + } else { + return QExifValue(QString::fromUtf8(header.offsetAscii, int(header.count) - 1)); + } + case QExifValue::Short: { + QVector value(int(header.count)); + + if (header.count > 2) { + stream.device()->seek(startPos + qint64(header.offset)); + + for (quint32 i = 0; i < header.count; i++) + stream >> value[int(i)]; + } else { + for (quint32 i = 0; i < header.count; i++) + value[int(i)] = header.offsetShorts[i]; + } + return QExifValue(value); + } + case QExifValue::Long: { + QVector value(int(header.count)); + + if (header.count > 1) { + stream.device()->seek(startPos + qint64(header.offset)); + + for (quint32 i = 0; i < header.count; i++) + stream >> value[int(i)]; + } else if (header.count == 1) { + value[0] = header.offset; + } + return QExifValue(value); + } + case QExifValue::SignedLong: { + QVector value(int(header.count)); + + if (header.count > 1) { + stream.device()->seek(startPos + qint64(header.offset)); + + for (quint32 i = 0; i < header.count; i++) + stream >> value[int(i)]; + } else if (header.count == 1) { + value[0] = int(header.offset); + } + return QExifValue(value); + } + case QExifValue::Rational: { + QVector value(int(header.count)); + + stream.device()->seek(startPos + qint64(header.offset)); + + for (quint32 i = 0; i < header.count; i++) + stream >> value[int(i)]; + + return QExifValue(value); + } + case QExifValue::SignedRational: { + QVector value(int(header.count)); + + stream.device()->seek(startPos + qint64(header.offset)); + + for (quint32 i = 0; i < header.count; i++) + stream >> value[int(i)]; + + return QExifValue(value); + } + default: + qWarning() << "Invalid Ifd Type" << header.type; + + return QExifValue(); + } +} + +template +QMap +QExifImageHeader::readIfdValues(QDataStream &stream, int startPos, const QList &headers) const { + QMap values; + + // This needs to be non-const so it works with gcc3 + QList headers_ = headers; + foreach (const ExifIfdHeader &header, headers_) + values[T(header.tag)] = readIfdValue(stream, startPos, header); + + return values; +} + +template +QMap +QExifImageHeader::readIfdValues(QDataStream &stream, int startPos, const QExifValue &pointer) const { + if (pointer.type() == QExifValue::Long && pointer.count() == 1) { + stream.device()->seek(qint64(startPos) + pointer.toLong()); + + QList headers = readIfdHeaders(stream); + + return readIfdValues(stream, startPos, headers); + } else { + return QMap(); + } +} + +/*! + Reads the contents of an EXIF header from an I/O \a device. + + Returns true if the header was read and false otherwise. + + \sa loadFromJpeg(), write() + */ +bool QExifImageHeader::read(QIODevice *device) { + clear(); + + int startPos = int(device->pos()); + + QDataStream stream(device); + + QByteArray byteOrder = device->read(2); + + if (byteOrder == "II") { + d->byteOrder = QSysInfo::LittleEndian; + + stream.setByteOrder(QDataStream::LittleEndian); + } else if (byteOrder == "MM") { + d->byteOrder = QSysInfo::BigEndian; + + stream.setByteOrder(QDataStream::BigEndian); + } else { + return false; + } + + quint16 id; + quint32 offset; + + stream >> id; + stream >> offset; + + if (id != 0x002A) return false; + + device->seek(startPos + qint64(offset)); + + QList headers = readIfdHeaders(stream); + + stream >> offset; + + d->imageIfdValues = readIfdValues(stream, startPos, headers); + + QExifValue exifIfdPointer = d->imageIfdValues.take(ImageTag(ExifIfdPointer)); + QExifValue gpsIfdPointer = d->imageIfdValues.take(ImageTag(GpsInfoIfdPointer)); + + d->exifIfdValues = readIfdValues(stream, startPos, exifIfdPointer); + d->gpsIfdValues = readIfdValues(stream, startPos, gpsIfdPointer); + + d->exifIfdValues.remove(ExifExtendedTag(InteroperabilityIfdPointer)); + + if (offset) { + device->seek(startPos + qint64(offset)); + + QMap thumbnailIfdValues = readIfdValues(stream, startPos, readIfdHeaders(stream)); + + QExifValue jpegOffset = thumbnailIfdValues.value(JpegInterchangeFormat); + QExifValue jpegLength = thumbnailIfdValues.value(JpegInterchangeFormatLength); + + if (jpegOffset.type() == QExifValue::Long && jpegOffset.count() == 1 && jpegLength.type() == QExifValue::Long && + jpegLength.count() == 1) { + device->seek(startPos + qint64(jpegOffset.toLong())); + + d->thumbnailData = device->read(jpegLength.toLong()); + + d->thumbnailXResolution = thumbnailIfdValues.value(XResolution); + d->thumbnailYResolution = thumbnailIfdValues.value(YResolution); + d->thumbnailResolutionUnit = thumbnailIfdValues.value(ResolutionUnit); + d->thumbnailOrientation = thumbnailIfdValues.value(Orientation); + } + } + return true; +} + +quint32 +QExifImageHeader::writeExifHeader(QDataStream &stream, quint16 tag, const QExifValue &value, quint32 offset) const { + stream << tag; + stream << quint16(value.type()); + stream << quint32(value.count()); + + switch (value.type()) { + case QExifValue::Byte: + if (value.count() <= 4) { + foreach (quint8 byte, value.toByteVector()) + stream << byte; + for (int j = value.count(); j < 4; j++) + stream << quint8(0); + } else { + stream << offset; + + offset += quint32(value.count()); + } + break; + case QExifValue::Undefined: + if (value.count() <= 4) { + stream.device()->write(value.toByteArray()); + + if (value.count() < 4) stream.writeRawData("\0\0\0\0", 4 - value.count()); + } else { + stream << offset; + + offset += quint32(value.count()); + } + break; + case QExifValue::Ascii: + if (value.count() <= 4) { + QByteArray bytes = value.toByteArray(); + + stream.writeRawData(bytes.constData(), value.count()); + if (value.count() < 4) stream.writeRawData("\0\0\0\0", 4 - value.count()); + } else { + stream << offset; + + offset += quint32(value.count()); + } + break; + case QExifValue::Short: + if (value.count() <= 2) { + foreach (quint16 shrt, value.toShortVector()) + stream << shrt; + for (int j = value.count(); j < 2; j++) + stream << quint16(0); + } else { + stream << offset; + + offset += quint32(value.count()) * quint32(sizeof(quint16)); + } + break; + case QExifValue::Long: + if (value.count() == 0) { + stream << quint32(0); + } else if (value.count() == 1) { + stream << value.toLong(); + } else { + stream << offset; + + offset += quint32(value.count()) * quint32(sizeof(quint32)); + } + break; + case QExifValue::SignedLong: + if (value.count() == 0) { + stream << quint32(0); + } else if (value.count() == 1) { + stream << value.toSignedLong(); + } else { + stream << offset; + + offset += quint32(value.count()) * quint32(sizeof(quint32)); + } + break; + case QExifValue::Rational: + if (value.count() == 0) { + stream << quint32(0); + } else { + stream << offset; + + offset += quint32(value.count()) * quint32(sizeof(quint32)) * 2; + } + break; + case QExifValue::SignedRational: + if (value.count() == 0) { + stream << quint32(0); + } else { + stream << offset; + + offset += quint32(value.count()) * quint32(sizeof(quint32)) * 2; + } + break; + default: + qWarning() << "Invalid Ifd Type" << value.type(); + stream << quint32(0); + } + + return offset; +} + +void QExifImageHeader::writeExifValue(QDataStream &stream, const QExifValue &value) const { + switch (value.type()) { + case QExifValue::Byte: + if (value.count() > 4) + foreach (quint8 byte, value.toByteVector()) + stream << byte; + break; + case QExifValue::Undefined: + if (value.count() > 4) stream.device()->write(value.toByteArray()); + break; + case QExifValue::Ascii: + if (value.count() > 4) { + QByteArray bytes = value.toByteArray(); + + stream.writeRawData(bytes.constData(), bytes.size() + 1); + } + break; + case QExifValue::Short: + if (value.count() > 2) + foreach (quint16 shrt, value.toShortVector()) + stream << shrt; + break; + case QExifValue::Long: + if (value.count() > 1) + foreach (quint32 lng, value.toLongVector()) + stream << lng; + break; + case QExifValue::SignedLong: + if (value.count() > 1) + foreach (qint32 lng, value.toSignedLongVector()) + stream << lng; + break; + case QExifValue::Rational: + if (value.count() > 0) + foreach (QExifURational rational, value.toRationalVector()) + stream << rational; + break; + case QExifValue::SignedRational: + if (value.count() > 0) + foreach (QExifSRational rational, value.toSignedRationalVector()) + stream << rational; + break; + default: + qWarning() << "Invalid Ifd Type" << value.type(); + break; + } +} + +template +quint32 +QExifImageHeader::writeExifHeaders(QDataStream &stream, const QMap &values, quint32 offset) const { + offset += quint32(values.count() * 12); + + for (typename QMap::const_iterator i = values.constBegin(); i != values.constEnd(); i++) + offset = writeExifHeader(stream, i.key(), i.value(), offset); + + return offset; +} + +template +void QExifImageHeader::writeExifValues(QDataStream &stream, const QMap &values) const { + for (typename QMap::const_iterator i = values.constBegin(); i != values.constEnd(); i++) + writeExifValue(stream, i.value()); +} + +/*! + Writes an EXIF header to an I/O \a device. + + Returns the total number of bytes written. + */ +qint64 QExifImageHeader::write(QIODevice *device) const { + // #ifndef QT_NO_DEBUG + qint64 startPos = device->pos(); + // #endif + + QDataStream stream(device); + + if (d->byteOrder == QSysInfo::LittleEndian) { + stream.setByteOrder(QDataStream::LittleEndian); + + device->write("II", 2); + device->write("\x2A\x00", 2); + device->write("\x08\x00\x00\x00", 4); + } else if (d->byteOrder == QSysInfo::BigEndian) { + stream.setByteOrder(QDataStream::BigEndian); + + device->write("MM", 2); + device->write("\x00\x2A", 2); + device->write("\x00\x00\x00\x08", 4); + } + + quint16 count = quint16(d->imageIfdValues.count() + 1); + quint32 offset = 26; + + if (!d->gpsIfdValues.isEmpty()) { + count++; + offset += 12; + } + + stream << count; + + offset = writeExifHeaders(stream, d->imageIfdValues, offset); + + quint32 exifIfdOffset = offset; + + stream << quint16(ExifIfdPointer); + stream << quint16(QExifValue::Long); + stream << quint32(1); + stream << exifIfdOffset; + offset += calculateSize(d->exifIfdValues); + + quint32 gpsIfdOffset = offset; + + if (!d->gpsIfdValues.isEmpty()) { + stream << quint16(GpsInfoIfdPointer); + stream << quint16(QExifValue::Long); + stream << quint32(1); + stream << gpsIfdOffset; + + d->imageIfdValues.insert(ImageTag(GpsInfoIfdPointer), QExifValue(offset)); + + offset += calculateSize(d->gpsIfdValues); + } + + if (!d->thumbnailData.isEmpty()) stream << offset; // Write offset to thumbnail Ifd. + else stream << quint32(0); + + writeExifValues(stream, d->imageIfdValues); + + Q_ASSERT(startPos + exifIfdOffset == device->pos()); + + stream << quint16(d->exifIfdValues.count()); + + writeExifHeaders(stream, d->exifIfdValues, exifIfdOffset); + writeExifValues(stream, d->exifIfdValues); + + Q_ASSERT(startPos + gpsIfdOffset == device->pos()); + + if (!d->gpsIfdValues.isEmpty()) { + stream << quint16(d->gpsIfdValues.count()); + + writeExifHeaders(stream, d->gpsIfdValues, gpsIfdOffset); + writeExifValues(stream, d->gpsIfdValues); + } + + Q_ASSERT(startPos + offset == device->pos()); + + if (!d->thumbnailData.isEmpty()) { + offset += 86; + + stream << quint16(7); + + QExifValue xResolution = + d->thumbnailXResolution.isNull() ? QExifValue(QExifURational(72, 1)) : d->thumbnailXResolution; + + QExifValue yResolution = + d->thumbnailYResolution.isNull() ? QExifValue(QExifURational(72, 1)) : d->thumbnailYResolution; + + QExifValue resolutionUnit = + d->thumbnailResolutionUnit.isNull() ? QExifValue(quint16(2)) : d->thumbnailResolutionUnit; + + QExifValue orientation = d->thumbnailOrientation.isNull() ? QExifValue(quint16(0)) : d->thumbnailOrientation; + + writeExifHeader(stream, Compression, QExifValue(quint16(6)), offset); + + offset = writeExifHeader(stream, XResolution, xResolution, offset); + offset = writeExifHeader(stream, YResolution, yResolution, offset); + + writeExifHeader(stream, ResolutionUnit, resolutionUnit, offset); + writeExifHeader(stream, Orientation, orientation, offset); + writeExifHeader(stream, JpegInterchangeFormat, QExifValue(offset), offset); + writeExifHeader(stream, JpegInterchangeFormatLength, QExifValue(quint32(d->thumbnailData.size())), offset); + + writeExifValue(stream, xResolution); + writeExifValue(stream, yResolution); + + Q_ASSERT(startPos + offset == device->pos()); + + device->write(d->thumbnailData); + + offset += quint32(d->thumbnailData.size()); + } + + Q_ASSERT(startPos + offset == device->pos()); + + d->size = offset; + + return offset; +} diff --git a/Linphone/tool/QExifImageHeader.hpp b/Linphone/tool/QExifImageHeader.hpp new file mode 100644 index 000000000..edd729ee7 --- /dev/null +++ b/Linphone/tool/QExifImageHeader.hpp @@ -0,0 +1,336 @@ +/**************************************************************************** +** +** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the Qt scene graph research project. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +// This file was copied from Qt Extended 4.5 + +#ifndef QEXIFIMAGEHEADER_H_ +#define QEXIFIMAGEHEADER_H_ + +#include +#include +#include +#include +#include +#include + +typedef QPair QExifURational; +typedef QPair QExifSRational; + +class QExifValuePrivate; + +class QExifValue { +public: + enum Type { + Byte = 1, + Ascii = 2, + Short = 3, + Long = 4, + Rational = 5, + Undefined = 7, + SignedLong = 9, + SignedRational = 10 + }; + + enum TextEncoding { + NoEncoding, + AsciiEncoding, + JisEncoding, + UnicodeEncoding, + UndefinedEncoding + }; + + QExifValue (); + QExifValue (quint8 value); + QExifValue (const QVector &value); + QExifValue (const QString &value, TextEncoding encoding = NoEncoding); + QExifValue (quint16 value); + QExifValue (const QVector &value); + QExifValue (quint32 value); + QExifValue (const QVector &value); + QExifValue (const QExifURational &value); + QExifValue (const QVector &value); + QExifValue (const QByteArray &value); + QExifValue (qint32 value); + QExifValue (const QVector &value); + QExifValue (const QExifSRational &value); + QExifValue (const QVector &value); + QExifValue (const QDateTime &value); + QExifValue (const QExifValue &other); + QExifValue &operator= (const QExifValue &other); + ~QExifValue (); + + bool operator== (const QExifValue &other) const; + + bool isNull () const; + + int type () const; + int count () const; + + TextEncoding encoding () const; + + quint8 toByte () const; + QVector toByteVector () const; + QString toString () const; + quint16 toShort () const; + QVector toShortVector () const; + quint32 toLong () const; + QVector toLongVector () const; + QExifURational toRational () const; + QVector toRationalVector () const; + QByteArray toByteArray () const; + qint32 toSignedLong () const; + QVector toSignedLongVector () const; + QExifSRational toSignedRational () const; + QVector toSignedRationalVector () const; + QDateTime toDateTime () const; + +private: + QExplicitlySharedDataPointer d; +}; + +struct ExifIfdHeader; + +class QExifImageHeaderPrivate; + +class QExifImageHeader { + Q_DISABLE_COPY(QExifImageHeader) + +public: + enum ImageTag { + ImageWidth = 0x0100, + ImageLength = 0x0101, + BitsPerSample = 0x0102, + Compression = 0x0103, + PhotometricInterpretation = 0x0106, + Orientation = 0x0112, + SamplesPerPixel = 0x0115, + PlanarConfiguration = 0x011C, + YCbCrSubSampling = 0x0212, + XResolution = 0x011A, + YResolution = 0x011B, + ResolutionUnit = 0x0128, + StripOffsets = 0x0111, + RowsPerStrip = 0x0116, + StripByteCounts = 0x0117, + TransferFunction = 0x012D, + WhitePoint = 0x013E, + PrimaryChromaciticies = 0x013F, + YCbCrCoefficients = 0x0211, + ReferenceBlackWhite = 0x0214, + DateTime = 0x0132, + ImageDescription = 0x010E, + Make = 0x010F, + Model = 0x0110, + Software = 0x0131, + Artist = 0x013B, + Copyright = 0x8298 + }; + + enum ExifExtendedTag { + ExifVersion = 0x9000, + FlashPixVersion = 0xA000, + ColorSpace = 0xA001, + ComponentsConfiguration = 0x9101, + CompressedBitsPerPixel = 0x9102, + PixelXDimension = 0xA002, + PixelYDimension = 0xA003, + MakerNote = 0x927C, + UserComment = 0x9286, + RelatedSoundFile = 0xA004, + DateTimeOriginal = 0x9003, + DateTimeDigitized = 0x9004, + SubSecTime = 0x9290, + SubSecTimeOriginal = 0x9291, + SubSecTimeDigitized = 0x9292, + ImageUniqueId = 0xA420, + ExposureTime = 0x829A, + FNumber = 0x829D, + ExposureProgram = 0x8822, + SpectralSensitivity = 0x8824, + ISOSpeedRatings = 0x8827, + Oecf = 0x8828, + ShutterSpeedValue = 0x9201, + ApertureValue = 0x9202, + BrightnessValue = 0x9203, + ExposureBiasValue = 0x9204, + MaxApertureValue = 0x9205, + SubjectDistance = 0x9206, + MeteringMode = 0x9207, + LightSource = 0x9208, + Flash = 0x9209, + FocalLength = 0x920A, + SubjectArea = 0x9214, + FlashEnergy = 0xA20B, + SpatialFrequencyResponse = 0xA20C, + FocalPlaneXResolution = 0xA20E, + FocalPlaneYResolution = 0xA20F, + FocalPlaneResolutionUnit = 0xA210, + SubjectLocation = 0xA214, + ExposureIndex = 0xA215, + SensingMethod = 0xA217, + FileSource = 0xA300, + SceneType = 0xA301, + CfaPattern = 0xA302, + CustomRendered = 0xA401, + ExposureMode = 0xA402, + WhiteBalance = 0xA403, + DigitalZoomRatio = 0xA404, + FocalLengthIn35mmFilm = 0xA405, + SceneCaptureType = 0xA406, + GainControl = 0xA407, + Contrast = 0xA408, + Saturation = 0xA409, + Sharpness = 0xA40A, + DeviceSettingDescription = 0xA40B, + SubjectDistanceRange = 0x40C + }; + + enum GpsTag { + GpsVersionId = 0x0000, + GpsLatitudeRef = 0x0001, + GpsLatitude = 0x0002, + GpsLongitudeRef = 0x0003, + GpsLongitude = 0x0004, + GpsAltitudeRef = 0x0005, + GpsAltitude = 0x0006, + GpsTimeStamp = 0x0007, + GpsSatellites = 0x0008, + GpsStatus = 0x0009, + GpsMeasureMode = 0x000A, + GpsDop = 0x000B, + GpsSpeedRef = 0x000C, + GpsSpeed = 0x000D, + GpsTrackRef = 0x000E, + GpsTrack = 0x000F, + GpsImageDirectionRef = 0x0010, + GpsImageDirection = 0x0011, + GpsMapDatum = 0x0012, + GpsDestLatitudeRef = 0x0013, + GpsDestLatitude = 0x0014, + GpsDestLongitudeRef = 0x0015, + GpsDestLongitude = 0x0016, + GpsDestBearingRef = 0x0017, + GpsDestBearing = 0x0018, + GpsDestDistanceRef = 0x0019, + GpsDestDistance = 0x001A, + GpsProcessingMethod = 0x001B, + GpsAreaInformation = 0x001C, + GpsDateStamp = 0x001D, + GpsDifferential = 0x001E + }; + + QExifImageHeader (); + explicit QExifImageHeader (const QString &fileName); + ~QExifImageHeader (); + + bool loadFromJpeg (const QString &fileName); + bool loadFromJpeg (QIODevice *device); + bool saveToJpeg (const QString &fileName) const; + bool saveToJpeg (QIODevice *device) const; + + bool read (QIODevice *device); + qint64 write (QIODevice *device) const; + + qint64 size () const; + + QSysInfo::Endian byteOrder () const; + + void clear (); + + QList imageTags () const; + QList extendedTags () const; + QList gpsTags () const; + + bool contains (ImageTag tag) const; + bool contains (ExifExtendedTag tag) const; + bool contains (GpsTag tag) const; + + void remove (ImageTag tag); + void remove (ExifExtendedTag tag); + void remove (GpsTag tag); + + QExifValue value (ImageTag tag) const; + QExifValue value (ExifExtendedTag tag) const; + QExifValue value (GpsTag tag) const; + + void setValue (ImageTag tag, const QExifValue &value); + void setValue (ExifExtendedTag tag, const QExifValue &value); + void setValue (GpsTag tag, const QExifValue &value); + + QImage thumbnail () const; + void setThumbnail (const QImage &thumbnail); + +private: + enum PrivateTag { + ExifIfdPointer = 0x8769, + GpsInfoIfdPointer = 0x8825, + InteroperabilityIfdPointer = 0xA005, + JpegInterchangeFormat = 0x0201, + JpegInterchangeFormatLength = 0x0202 + }; + + QByteArray extractExif (QIODevice *device) const; + + QList readIfdHeaders (QDataStream &stream) const; + + QExifValue readIfdValue (QDataStream &stream, int startPos, const ExifIfdHeader &header) const; + template + QMap readIfdValues (QDataStream &stream, int startPos, const QList &headers) const; + template + QMap readIfdValues (QDataStream &stream, int startPos, const QExifValue &pointer) const; + + quint32 writeExifHeader (QDataStream &stream, quint16 tag, const QExifValue &value, quint32 offset) const; + void writeExifValue (QDataStream &stream, const QExifValue &value) const; + + template + quint32 writeExifHeaders (QDataStream &stream, const QMap &values, quint32 offset) const; + template + void writeExifValues (QDataStream &target, const QMap &values) const; + + quint32 sizeOf (const QExifValue &value) const; + + template + quint32 calculateSize (const QMap &values) const; + + QExifImageHeaderPrivate *d; +}; + +#endif // ifndef QEXIFIMAGEHEADER_H_ diff --git a/Linphone/tool/Utils.cpp b/Linphone/tool/Utils.cpp index 9d318ce65..381851ed1 100644 --- a/Linphone/tool/Utils.cpp +++ b/Linphone/tool/Utils.cpp @@ -43,6 +43,7 @@ #include #include #include +#include #include #include #include @@ -59,6 +60,10 @@ DEFINE_ABSTRACT_OBJECT(Utils) +namespace { +constexpr int SafeFilePathLimit = 100; +} + // ============================================================================= char *Utils::rstrstr(const char *a, const char *b) { @@ -347,6 +352,12 @@ QString Utils::formatTime(const QDateTime &date) { return date.time().toString("hh:mm"); } +QString Utils::formatDuration(int durationMs) { + QTime duration(0, 0); + duration = duration.addMSecs(durationMs); + return duration.hour() > 0 ? duration.toString("hh:mm:ss") : duration.toString("mm:ss"); +} + QString Utils::formatDateElapsedTime(const QDateTime &date) { // auto y = floor(seconds / 31104000); // if (y > 0) return QString::number(y) + " years"; @@ -1870,6 +1881,18 @@ QString Utils::encodeEmojiToQmlRichFormat(const QString &body) { return fmtBody; } +static bool codepointIsVisible(uint code) { + return code > 0x00020; +} + +bool Utils::isOnlyEmojis(const QString &text) { + if (text.isEmpty()) return false; + QVector utf32_string = text.toUcs4(); + for (auto &code : utf32_string) + if (codepointIsVisible(code) && !Utils::codepointIsEmoji(code)) return false; + return true; +} + QString Utils::getFilename(QUrl url) { return url.fileName(); } @@ -1906,3 +1929,79 @@ QString Utils::toTimeString(QDateTime date, const QString &format) { // Issue : date.toString() will not print the good time in timezones. Get it from date and add ourself the offset. return getOffsettedUTC(date).toString(format); } +QString Utils::getSafeFilePath(const QString &filePath, bool *soFarSoGood) { + if (soFarSoGood) *soFarSoGood = true; + + QFileInfo info(filePath); + if (!info.exists()) return filePath; + + const QString prefix = QStringLiteral("%1/%2").arg(info.absolutePath()).arg(info.baseName()); + const QString ext = info.completeSuffix(); + + for (int i = 1; i < SafeFilePathLimit; ++i) { + QString safePath = QStringLiteral("%1 (%3).%4").arg(prefix).arg(i).arg(ext); + if (!QFileInfo::exists(safePath)) return safePath; + } + + if (soFarSoGood) *soFarSoGood = false; + + return QString(""); +} + +bool Utils::isVideo(const QString &path) { + if (path.isEmpty()) return false; + return QMimeDatabase().mimeTypeForFile(path).name().contains("video/"); +} + +bool Utils::isPdf(const QString &path) { + if (path.isEmpty()) return false; + return QMimeDatabase().mimeTypeForFile(path).name().contains("application/pdf"); +} + +bool Utils::isText(const QString &path) { + if (path.isEmpty()) return false; + return QMimeDatabase().mimeTypeForFile(path).name().contains("text"); +} + +bool Utils::isImage(const QString &path) { + if (path.isEmpty()) return false; + QFileInfo info(path); + if (!info.exists() || SettingsModel::getInstance()->getVfsEncrypted()) { + return QMimeDatabase().mimeTypeForFile(path).name().contains("image/"); + } else { + if (!QMimeDatabase().mimeTypeForFile(info).name().contains("image/")) return false; + QImageReader reader(path); + return reader.canRead() && reader.imageCount() == 1; + } +} + +bool Utils::isAnimatedImage(const QString &path) { + if (path.isEmpty()) return false; + QFileInfo info(path); + if (!info.exists() || !QMimeDatabase().mimeTypeForFile(info).name().contains("image/")) return false; + QImageReader reader(path); + return reader.canRead() && reader.supportsAnimation() && reader.imageCount() > 1; +} + +bool Utils::canHaveThumbnail(const QString &path) { + if (path.isEmpty()) return false; + return isImage(path) || isAnimatedImage(path) /*|| isPdf(path)*/ || isVideo(path); +} + +QImage Utils::getImage(const QString &pUri) { + QImage image(pUri); + QImageReader reader(pUri); + reader.setAutoTransform(true); + if (image.isNull()) { // Try to determine format from headers instead of using suffix + reader.setDecideFormatFromContent(true); + } + return reader.read(); +} + +void Utils::setGlobalCursor(Qt::CursorShape cursor) { + App::getInstance()->setOverrideCursor(QCursor(cursor)); +} + +void Utils::restoreGlobalCursor() { + App::getInstance()->restoreOverrideCursor(); +} \ No newline at end of file diff --git a/Linphone/tool/Utils.hpp b/Linphone/tool/Utils.hpp index e6bb51ba1..7c4b276a3 100644 --- a/Linphone/tool/Utils.hpp +++ b/Linphone/tool/Utils.hpp @@ -95,6 +95,7 @@ public: QString format = ""); // Return the date formated Q_INVOKABLE static QString formatDateElapsedTime(const QDateTime &date); Q_INVOKABLE static QString formatTime(const QDateTime &date); // Return the time formated + Q_INVOKABLE static QString formatDuration(int durationMs); // Return the duration formated Q_INVOKABLE static QStringList generateSecurityLettersArray(int arraySize, int correctIndex, QString correctCode); Q_INVOKABLE static int getRandomIndex(int size); Q_INVOKABLE static bool copyToClipboard(const QString &text); @@ -154,9 +155,20 @@ public: Q_INVOKABLE static QString encodeTextToQmlRichFormat(const QString &text, const QVariantMap &options = QVariantMap()); Q_INVOKABLE static QString encodeEmojiToQmlRichFormat(const QString &body); + Q_INVOKABLE static bool isOnlyEmojis(const QString &text); Q_INVOKABLE static QString getFilename(QUrl url); static bool codepointIsEmoji(uint code); + Q_INVOKABLE static bool isVideo(const QString &path); + static QString getSafeFilePath(const QString &filePath, bool *soFarSoGood); + Q_INVOKABLE static bool isAnimatedImage(const QString &path); + Q_INVOKABLE static bool canHaveThumbnail(const QString &path); + Q_INVOKABLE static bool isImage(const QString &path); + Q_INVOKABLE static bool isPdf(const QString &path); + Q_INVOKABLE static bool isText(const QString &path); + Q_INVOKABLE static QImage getImage(const QString &pUri); + Q_INVOKABLE static void setGlobalCursor(Qt::CursorShape cursor); + Q_INVOKABLE static void restoreGlobalCursor(); Q_INVOKABLE static QString toDateTimeString(QDateTime date, const QString &format = "yyyy/MM/dd hh:mm:ss"); static QDateTime getOffsettedUTC(const QDateTime &date); diff --git a/Linphone/tool/providers/ExternalImageProvider.cpp b/Linphone/tool/providers/ExternalImageProvider.cpp new file mode 100644 index 000000000..635ece438 --- /dev/null +++ b/Linphone/tool/providers/ExternalImageProvider.cpp @@ -0,0 +1,48 @@ +/* + * 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 "core/path/Paths.hpp" +#include "tool/Utils.hpp" + +#include "ExternalImageProvider.hpp" + +#include + +// ============================================================================= + +const QString ExternalImageProvider::ProviderId = "external"; + +ExternalImageProvider::ExternalImageProvider() + : QQuickImageProvider(QQmlImageProviderBase::Image, QQmlImageProviderBase::ForceAsynchronousImageLoading) { +} + +QImage ExternalImageProvider::requestImage(const QString &id, QSize *size, const QSize &requestedSize) { + QImage image(Utils::getImage(QUrl::fromPercentEncoding(id.toUtf8()))); + double requestedFactor = 1.0; + double factor = image.width() / (double)image.height(); + if (requestedSize.isValid()) requestedFactor = requestedSize.width() / (double)requestedSize.height(); + if (factor < 0.2) { // too height + image = image.copy(0, 0, image.width(), image.width() / requestedFactor); + } else if (factor > 5) { // too large + image = image.copy(0, 0, image.height() * requestedFactor, image.height()); + } + *size = image.size(); + return image; +} diff --git a/Linphone/tool/providers/ExternalImageProvider.hpp b/Linphone/tool/providers/ExternalImageProvider.hpp new file mode 100644 index 000000000..8266175bb --- /dev/null +++ b/Linphone/tool/providers/ExternalImageProvider.hpp @@ -0,0 +1,37 @@ +/* + * 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 EXTERNAL_IMAGE_PROVIDER_H_ +#define EXTERNAL_IMAGE_PROVIDER_H_ + +#include + +// ============================================================================= + +class ExternalImageProvider : public QQuickImageProvider { +public: + ExternalImageProvider(); + + QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize) override; + + static const QString ProviderId; +}; + +#endif // EXTERNAL_IMAGE_PROVIDER_H_ diff --git a/Linphone/tool/providers/ThumbnailProvider.cpp b/Linphone/tool/providers/ThumbnailProvider.cpp new file mode 100644 index 000000000..ce695bb5a --- /dev/null +++ b/Linphone/tool/providers/ThumbnailProvider.cpp @@ -0,0 +1,130 @@ +/* + * 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 "ThumbnailProvider.hpp" +#include "model/setting/SettingsModel.hpp" +#include "tool/QExifImageHeader.hpp" +#include "tool/Utils.hpp" + +#include +#include +#include +#include + +DEFINE_ABSTRACT_OBJECT(ThumbnailAsyncImageResponse) + +// ============================================================================= + +const QString ThumbnailProvider::ProviderId = "thumbnail"; + +ThumbnailAsyncImageResponse::ThumbnailAsyncImageResponse(const QString &id, const QSize &requestedSize) { + mPath = id; + connect(&mListener, &VideoFrameGrabberListener::imageGrabbed, this, &ThumbnailAsyncImageResponse::imageGrabbed); + + if (QFileInfo(mPath).isFile()) { + bool removeExportedFile = SettingsModel::getInstance()->getVfsEncrypted(); + if (removeExportedFile) { + std::shared_ptr content = + linphone::Factory::get()->createContentFromFile(Utils::appStringToCoreString(mPath)); + mPath = Utils::coreStringToAppString(content->exportPlainFile()); + } + QImage originalImage(mPath); + if (originalImage.isNull()) { // Try to determine format from headers + QImageReader reader(mPath); + reader.setDecideFormatFromContent(true); + QByteArray format = reader.format(); + if (!format.isEmpty()) { + originalImage = QImage(mPath, format); + } else if (Utils::isVideo(mPath)) { + VideoFrameGrabber *grabber = new VideoFrameGrabber(removeExportedFile); + removeExportedFile = false; + connect(grabber, &VideoFrameGrabber::grabFinished, &mListener, + &VideoFrameGrabberListener::imageGrabbed); + grabber->requestFrame(mPath); + } + } + if (removeExportedFile) QFile(mPath).remove(); + if (!originalImage.isNull()) { + emit imageGrabbed(originalImage); + } + } +} + +QImage ThumbnailAsyncImageResponse::createThumbnail(const QString &path, QImage originalImage) { + QImage thumbnail; + if (!originalImage.isNull()) { + int rotation = 0; + QExifImageHeader exifImageHeader; + if (exifImageHeader.loadFromJpeg(path)) + rotation = int(exifImageHeader.value(QExifImageHeader::ImageTag::Orientation).toShort()); + double factor = originalImage.width() / (double)originalImage.height(); + Qt::AspectRatioMode aspectRatio = Qt::KeepAspectRatio; + if (factor < 0.2 || factor > 5) aspectRatio = Qt::KeepAspectRatioByExpanding; + QImageReader reader(path); + if (reader.format() == "svg") { + QSvgRenderer svgRenderer(path); + if (svgRenderer.isValid()) { + thumbnail = QImage(Constants::ThumbnailImageFileWidth, Constants::ThumbnailImageFileHeight, + originalImage.format()); + thumbnail.fill(QColor(Qt::transparent)); + QPainter painter(&thumbnail); + svgRenderer.setAspectRatioMode(aspectRatio); + svgRenderer.render(&painter); + } + } + if (thumbnail.isNull()) { + QImage image(originalImage.size(), originalImage.format()); + // Fill with color to replace transparency with white color instead of black (default). + image.fill(QColor(Qt::white).rgb()); + QPainter painter(&image); + painter.drawImage(0, 0, originalImage); + //-------------------- + thumbnail = image.scaled(Constants::ThumbnailImageFileWidth, Constants::ThumbnailImageFileHeight, + aspectRatio, Qt::SmoothTransformation); + if (aspectRatio == Qt::KeepAspectRatioByExpanding) // Cut + thumbnail = + thumbnail.copy(0, 0, Constants::ThumbnailImageFileWidth, Constants::ThumbnailImageFileHeight); + } + + if (rotation != 0) { + QTransform transform; + if (rotation == 3 || rotation == 4) transform.rotate(180); + else if (rotation == 5 || rotation == 6) transform.rotate(90); + else if (rotation == 7 || rotation == 8) transform.rotate(-90); + thumbnail = thumbnail.transformed(transform); + if (rotation == 2 || rotation == 4 || rotation == 5 || rotation == 7) + thumbnail = thumbnail.flipped(Qt::Horizontal); + } + } + return thumbnail; +} + +void ThumbnailAsyncImageResponse::imageGrabbed(QImage image) { + mImage = createThumbnail(mPath, image); + emit finished(); +} + +QQuickTextureFactory *ThumbnailAsyncImageResponse::textureFactory() const { + return QQuickTextureFactory::textureFactoryForImage(mImage); +} +QQuickImageResponse *ThumbnailProvider::requestImageResponse(const QString &id, const QSize &requestedSize) { + ThumbnailAsyncImageResponse *response = new ThumbnailAsyncImageResponse(id, requestedSize); + return response; +} \ No newline at end of file diff --git a/Linphone/tool/providers/ThumbnailProvider.hpp b/Linphone/tool/providers/ThumbnailProvider.hpp new file mode 100644 index 000000000..fbf97afb0 --- /dev/null +++ b/Linphone/tool/providers/ThumbnailProvider.hpp @@ -0,0 +1,59 @@ +/* + * 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 THUMBNAIL_PROVIDER_H_ +#define THUMBNAIL_PROVIDER_H_ + +#include + +#include "VideoFrameGrabber.hpp" +#include "tool/AbstractObject.hpp" + +// Thumbnails are created asynchronously with QQuickAsyncImageProvider and not QQuickImageProvider. +// This ensure to have async objects like QMediaPlayer and QAbstractVideoSurface while keeping them in the main thread +// (mandatory for VideoSurface). If not, there seems to have some deadlocks in Qt library when GUI objects are deleted +// while still playing media. +// ============================================================================= +class ThumbnailAsyncImageResponse : public QQuickImageResponse, public AbstractObject { +public: + ThumbnailAsyncImageResponse(const QString &id, const QSize &requestedSize); + + QQuickTextureFactory *textureFactory() const override; // Convert QImage into texture. If Image is null, then + // sourceSize will be egal to 0. So there will be no errors. + + void imageGrabbed(QImage image); + QImage createThumbnail(const QString &path, QImage originalImage); + + QImage mImage; + QString mPath; + VideoFrameGrabberListener mListener; + +private: + DECLARE_ABSTRACT_OBJECT +}; + +class ThumbnailProvider : public QQuickAsyncImageProvider { +public: + virtual QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) override; + + static const QString ProviderId; +}; + +#endif // THUMBNAIL_PROVIDER_H_ diff --git a/Linphone/tool/providers/VideoFrameGrabber.cpp b/Linphone/tool/providers/VideoFrameGrabber.cpp new file mode 100644 index 000000000..16818e51d --- /dev/null +++ b/Linphone/tool/providers/VideoFrameGrabber.cpp @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2023 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 "VideoFrameGrabber.hpp" + +#include +#include + +VideoFrameGrabberListener::VideoFrameGrabberListener() { +} + +VideoFrameGrabber::VideoFrameGrabber(bool deleteFile, QObject *parent) : QVideoSink(parent) { + mDeleteFile = deleteFile; + mPlayer = new QMediaPlayer(); + mVideoSink = new QVideoSink(); + connect( + mPlayer, &QMediaPlayer::errorOccurred, this, + [this](QMediaPlayer::Error error, const QString &errorString) { end(); }, Qt::DirectConnection); + QObject::connect( + mPlayer, &QMediaPlayer::mediaStatusChanged, this, + [this](QMediaPlayer::MediaStatus status) mutable { + switch (status) { + case QMediaPlayer::LoadedMedia: + if (!mLoadedMedia) { + mLoadedMedia = true; + if (mPlayer->hasVideo()) { + mPlayer->setPosition(mPlayer->duration() / 2); + mPlayer->play(); + } else { + end(); + } + } + break; + case QMediaPlayer::InvalidMedia: + case QMediaPlayer::EndOfMedia: + case QMediaPlayer::NoMedia: + end(); + break; + default: { + } + } + }, + Qt::DirectConnection); + + connect(mVideoSink, &QVideoSink::videoFrameChanged, this, [this](const QVideoFrame &frame) { + if (isFormatSupported(frame)) mResult = frame.toImage().copy(); + }); + + mPlayer->setVideoSink(mVideoSink); + mPlayer->setVideoOutput(this); +} + +VideoFrameGrabber::~VideoFrameGrabber() { + if (mDeleteFile) QFile(mPath).remove(); +} + +void VideoFrameGrabber::requestFrame(const QString &path) { + mLoadedMedia = false; + mPath = path; + // mPlayer->set(QUrl::fromLocalFile(mPath)); +} + +void VideoFrameGrabber::end() { + if (mPlayer->mediaStatus() != QMediaPlayer::NoMedia) { + // mPlayer->setMedia(QUrl()); + } else if (!mResultSent) { // Avoid sending multiple times before destroying the object + mResultSent = true; + emit grabFinished(mResult); + deleteLater(); + } +} + +QList VideoFrameGrabber::supportedPixelFormats() const { + return QList() + << QVideoFrameFormat::Format_YUV420P << QVideoFrameFormat::Format_YV12 << QVideoFrameFormat::Format_UYVY + << QVideoFrameFormat::Format_YUYV << QVideoFrameFormat::Format_NV12 << QVideoFrameFormat::Format_NV21 + << QVideoFrameFormat::Format_IMC1 << QVideoFrameFormat::Format_IMC2 << QVideoFrameFormat::Format_IMC3 + << QVideoFrameFormat::Format_IMC4 << QVideoFrameFormat::Format_Y8 << QVideoFrameFormat::Format_Y16 + << QVideoFrameFormat::Format_Jpeg << QVideoFrameFormat::Format_ABGR8888 << QVideoFrameFormat::Format_ARGB8888 + << QVideoFrameFormat::Format_ARGB8888_Premultiplied << QVideoFrameFormat::Format_AYUV + << QVideoFrameFormat::Format_AYUV_Premultiplied << QVideoFrameFormat::Format_BGRA8888 + << QVideoFrameFormat::Format_BGRA8888_Premultiplied << QVideoFrameFormat::Format_BGRA8888_Premultiplied + << QVideoFrameFormat::Format_BGRX8888; +} + +bool VideoFrameGrabber::isFormatSupported(const QVideoFrame &frame) const { + const QImage::Format imageFormat = QVideoFrameFormat::imageFormatFromPixelFormat(frame.pixelFormat()); + const QSize size = frame.size(); + + return imageFormat != QImage::Format_Invalid && !size.isEmpty() && + frame.handleType() == QVideoFrame::HandleType::NoHandle; +} + +bool VideoFrameGrabber::start(const QVideoFrameFormat::PixelFormat &format) { + return true; + // return QVideoSink::start(format); +} + +void VideoFrameGrabber::stop() { + // QVideoSink::stop(); +} \ No newline at end of file diff --git a/Linphone/tool/providers/VideoFrameGrabber.hpp b/Linphone/tool/providers/VideoFrameGrabber.hpp new file mode 100644 index 000000000..de98aab6a --- /dev/null +++ b/Linphone/tool/providers/VideoFrameGrabber.hpp @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2023 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 VIDEO_FRAME_GRABBER_H +#define VIDEO_FRAME_GRABBER_H + +#include +#include +#include + +// Call VideoFrameGrabber::requestFrame() and wait for imageGrabbed() to get the image. +// You will need to link your listener with connect(grabber, &VideoFrameGrabber::grabFinished, listener, +// &VideoFrameGrabberListener::imageGrabbed); +class VideoFrameGrabberListener : public QObject { + Q_OBJECT +public: + VideoFrameGrabberListener(); +signals: + void imageGrabbed(QImage image); +}; + +class VideoFrameGrabber : public QVideoSink { + Q_OBJECT +public: + VideoFrameGrabber(bool deleteFile = false, QObject *parent = 0); + ~VideoFrameGrabber(); + + void requestFrame(const QString &path); // Function to call. + + void end(); + + QList supportedPixelFormats() const; + bool isFormatSupported(const QVideoFrame &frame) const; + + bool start(const QVideoFrameFormat::PixelFormat &format); + void stop(); + + QMediaPlayer *mPlayer = nullptr; + QVideoSink *mVideoSink = nullptr; + bool mLoadedMedia = false; + bool mResultSent = false; + bool mDeleteFile = false; + QString mPath; + QImage mResult; + +signals: + void frameAvailable(QImage frame); + void grabFinished(QImage frame); +}; + +#endif diff --git a/Linphone/tool/ui/DashRectangle.cpp b/Linphone/tool/ui/DashRectangle.cpp new file mode 100644 index 000000000..21b2d388f --- /dev/null +++ b/Linphone/tool/ui/DashRectangle.cpp @@ -0,0 +1,57 @@ +/* + * 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 "DashRectangle.hpp" +#include "core/App.hpp" + +DashRectangle::DashRectangle(QQuickItem *parent) : QQuickPaintedItem(parent) { + connect(this, &DashRectangle::radiusChanged, this, [this] { update(); }); + connect(this, &DashRectangle::colorChanged, this, [this] { update(); }); +} + +void DashRectangle::paint(QPainter *painter) { + QPen pen(Qt::DotLine); + pen.setColor(mColor); + pen.setWidthF(4 * App::getInstance()->getScreenRatio()); + painter->setPen(pen); + painter->drawRoundedRect(x(), y(), width(), height(), mRadius, mRadius); +} + +float DashRectangle::getRadius() const { + return mRadius; +} + +void DashRectangle::setRadius(float radius) { + if (mRadius != radius) { + mRadius = radius; + emit radiusChanged(); + } +} + +QColor DashRectangle::getColor() const { + return mColor; +} + +void DashRectangle::setColor(QColor Color) { + if (mColor != Color) { + mColor = Color; + emit colorChanged(); + } +} \ No newline at end of file diff --git a/Linphone/tool/ui/DashRectangle.hpp b/Linphone/tool/ui/DashRectangle.hpp new file mode 100644 index 000000000..9dc9fe3de --- /dev/null +++ b/Linphone/tool/ui/DashRectangle.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 DASHRECTANGLE_H +#define DASHRECTANGLE_H + +#include +#include +#include + +class DashRectangle : public QQuickPaintedItem { + Q_OBJECT + Q_PROPERTY(float radius READ getRadius WRITE setRadius NOTIFY radiusChanged) + Q_PROPERTY(QColor color READ getColor WRITE setColor NOTIFY colorChanged) +public: + explicit DashRectangle(QQuickItem *parent = nullptr); + + virtual void paint(QPainter *painter); + + float getRadius() const; + void setRadius(float radius); + + QColor getColor() const; + void setColor(QColor color); + +signals: + void radiusChanged(); + void colorChanged(); + +private: + float mRadius = 0; + QColor mColor; +}; + +#endif // DASHRECTANGLE_H diff --git a/Linphone/view/CMakeLists.txt b/Linphone/view/CMakeLists.txt index 7466d7c5b..ea74a250d 100644 --- a/Linphone/view/CMakeLists.txt +++ b/Linphone/view/CMakeLists.txt @@ -35,6 +35,7 @@ list(APPEND _LINPHONEAPP_QML_FILES view/Control/Container/Call/CallLayout.qml view/Control/Container/Call/CallGridLayout.qml view/Control/Container/Call/Mosaic.qml + view/Control/Container/Chat/ChatFilesGridLayout.qml view/Control/Container/Contact/ContactLayout.qml view/Control/Container/Contact/PresenceNoteLayout.qml view/Control/Container/Main/MainRightPanel.qml @@ -46,6 +47,7 @@ list(APPEND _LINPHONEAPP_QML_FILES view/Control/Display/TemporaryText.qml view/Control/Display/ProgressBar.qml view/Control/Display/RoundedPane.qml + view/Control/Display/RoundProgressBar.qml view/Control/Display/Sticker.qml view/Control/Display/Text.qml view/Control/Display/ToolTip.qml @@ -53,11 +55,15 @@ list(APPEND _LINPHONEAPP_QML_FILES view/Control/Display/Call/CallHistoryListView.qml view/Control/Display/Call/CallStatistics.qml view/Control/Display/Chat/Emoji/EmojiPicker.qml + view/Control/Display/Chat/ChatMessageContent.qml + view/Control/Display/Chat/ChatAudioContent.qml + view/Control/Display/Chat/ChatTextContent.qml view/Control/Display/Chat/ChatListView.qml view/Control/Display/Chat/ChatMessage.qml view/Control/Display/Chat/ChatMessageInvitationBubble.qml view/Control/Display/Chat/ChatMessagesListView.qml view/Control/Display/Chat/Event.qml + view/Control/Display/Chat/FileView.qml view/Control/Display/Contact/Avatar.qml view/Control/Display/Contact/Contact.qml view/Control/Display/Contact/Presence.qml @@ -78,6 +84,7 @@ list(APPEND _LINPHONEAPP_QML_FILES view/Control/Form/Settings/MultimediaSettings.qml view/Control/Form/Settings/ScreencastSettings.qml + view/Control/Input/Chat/ChatDroppableTextArea.qml view/Control/Input/Calendar.qml view/Control/Input/DecoratedTextField.qml view/Control/Input/DigitInput.qml @@ -167,6 +174,7 @@ list(APPEND _LINPHONEAPP_QML_SINGLETONS view/Style/AppIcons.qml view/Style/buttonStyle.js view/Style/DefaultStyle.qml + view/Style/FileViewStyle.qml view/Style/Typography.qml ) diff --git a/Linphone/view/Control/Button/RoundButton.qml b/Linphone/view/Control/Button/RoundButton.qml index 7218a7d03..b5f22a0c8 100644 --- a/Linphone/view/Control/Button/RoundButton.qml +++ b/Linphone/view/Control/Button/RoundButton.qml @@ -8,13 +8,13 @@ Button { id: mainItem textSize: Typography.p1s.pixelSize textWeight: Typography.p1s.weight - topPadding: Math.round(16 * DefaultStyle.dp) - bottomPadding: Math.round(16 * DefaultStyle.dp) - leftPadding: Math.round(16 * DefaultStyle.dp) - rightPadding: Math.round(16 * DefaultStyle.dp) - icon.width: Math.round(24 * DefaultStyle.dp) - icon.height: Math.round(24 * DefaultStyle.dp) - radius: Math.round(40 * DefaultStyle.dp) - width: Math.round(24 * DefaultStyle.dp) - height: Math.round(24 * DefaultStyle.dp) + padding: Math.round(16 * DefaultStyle.dp) + // bottomPadding: Math.round(16 * DefaultStyle.dp) + // leftPadding: Math.round(16 * DefaultStyle.dp) + // rightPadding: Math.round(16 * DefaultStyle.dp) + icon.width: width + icon.height: width + radius: width * 2 + // width: Math.round(24 * DefaultStyle.dp) + height: width } diff --git a/Linphone/view/Control/Container/Call/CallGridLayout.qml b/Linphone/view/Control/Container/Call/CallGridLayout.qml index 370be140a..a760201dd 100644 --- a/Linphone/view/Control/Container/Call/CallGridLayout.qml +++ b/Linphone/view/Control/Container/Call/CallGridLayout.qml @@ -24,10 +24,6 @@ Mosaic { qmlName: "G" Component.onCompleted: console.log("Loaded : " +allDevices + " = " +allDevices.count) } - property AccountProxy accounts: AccountProxy { - id: accountProxy - sourceModel: AppCpp.accounts - } model: grid.call && grid.call.core.isConference ? participantDevices: [0,1] delegate: Item{ id: avatarCell diff --git a/Linphone/view/Control/Container/Call/Mosaic.qml b/Linphone/view/Control/Container/Call/Mosaic.qml index 756907fdb..33cea2476 100644 --- a/Linphone/view/Control/Container/Call/Mosaic.qml +++ b/Linphone/view/Control/Container/Call/Mosaic.qml @@ -3,9 +3,8 @@ import QtQuick.Controls.Basic import QtQuick.Layouts import QtQml.Models - // ============================================================================= -ColumnLayout{ +ColumnLayout { id: mainLayout property alias delegateModel: grid.model property alias cellHeight: grid.cellHeight diff --git a/Linphone/view/Control/Container/Chat/ChatFilesGridLayout.qml b/Linphone/view/Control/Container/Chat/ChatFilesGridLayout.qml new file mode 100644 index 000000000..e3917f3aa --- /dev/null +++ b/Linphone/view/Control/Container/Chat/ChatFilesGridLayout.qml @@ -0,0 +1,61 @@ +import QtQuick +import QtQuick.Layouts +import QtQml.Models + +import Linphone +import UtilsCpp + +// ============================================================================= +GridLayout { + id: mainItem + property ChatMessageGui chatMessageGui: null + property bool isHoveringFile: false + property int itemCount: delModel.count + property int itemWidth: Math.round(95 * DefaultStyle.dp) + // cellWidth: + // cellHeight: Math.round(105 * DefaultStyle.dp) + property real maxWidth: 3 * 105 * DefaultStyle.dp + columns: optimalColumns + + + property int optimalColumns: { + let maxCols = Math.floor(maxWidth / itemWidth); + let bestCols = 1; + let minRows = Number.MAX_VALUE; + let minEmptySlots = Number.MAX_VALUE; + + for (let cols = maxCols; cols >= 1; cols--) { + let rows = Math.ceil(itemCount / cols); + let emptySlots = cols * rows - itemCount; + + if ( + rows < minRows || + (rows === minRows && emptySlots < minEmptySlots) + ) { + bestCols = cols; + minRows = rows; + minEmptySlots = emptySlots; + } + } + + return bestCols; + } + + Repeater { + id: delModel + model: ChatMessageContentProxy { + id: contentProxy + filterType: ChatMessageContentProxy.FilterContentType.File + chatMessageGui: mainItem.chatMessageGui + } + + delegate: FileView { + id: avatarCell + contentGui: modelData + visible: modelData + height: mainItem.itemWidth + width: mainItem.itemWidth + onIsHoveringChanged: mainItem.isHoveringFile = isHovering + } + } +} \ No newline at end of file diff --git a/Linphone/view/Control/Display/Chat/ChatAudioContent.qml b/Linphone/view/Control/Display/Chat/ChatAudioContent.qml new file mode 100644 index 000000000..8726e5045 --- /dev/null +++ b/Linphone/view/Control/Display/Chat/ChatAudioContent.qml @@ -0,0 +1,127 @@ +import QtQuick +import QtQuick.Layouts + +import Linphone + +import UtilsCpp + +// ============================================================================= + +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 + + active: chatMessageContentGui && chatMessageContentGui.core.isVoiceRecording() + + onChatMessageContentGuiChanged: if(chatMessageContentGui){ + tempFile.createFileFromContentModel(chatMessageContentGui, false); + } + + TemporaryFile { + id: tempFile + } + + sourceComponent: Item{ + id: loadedItem + property bool isPlaying : vocalPlayer.item && vocalPlayer.item.playbackState === SoundPlayer.PlayingState + onIsPlayingChanged: isPlaying ? mediaProgressBar.resume() : mediaProgressBar.stop() + + width: availableWidth < 0 || availableWidth > mainItem.width ? mainItem.width : availableWidth + height: mainItem.fitHeight + + clip: false + Loader { + id: vocalPlayer + + active: false + 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() + } + } + } + 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() + } + } + onStatusChanged: if (loader.status == Loader.Ready) play() + } + RowLayout{ + id: lineLayout + 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() + } + } + 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) + } + } + } + + } + } +} diff --git a/Linphone/view/Control/Display/Chat/ChatListView.qml b/Linphone/view/Control/Display/Chat/ChatListView.qml index f5a2b4bca..7280731f8 100644 --- a/Linphone/view/Control/Display/Chat/ChatListView.qml +++ b/Linphone/view/Control/Display/Chat/ChatListView.qml @@ -311,8 +311,8 @@ ListView { unread: modelData.core.unreadMessagesCount } EffectImage { - visible: modelData != undefined && lastMessageText.visible && modelData?.core.lastMessage && modelData?.core.lastMessageState !== LinphoneEnums.ChatMessageState.StateIdle - && !modelData?.core.lastMessage.core.isRemoteMessage + visible: modelData?.core.lastMessage && modelData?.core.lastMessageState !== LinphoneEnums.ChatMessageState.StateIdle + && !modelData.core.lastMessage.core.isRemoteMessage Layout.preferredWidth: visible ? 14 * DefaultStyle.dp : 0 Layout.preferredHeight: 14 * DefaultStyle.dp colorizationColor: DefaultStyle.main1_500_main diff --git a/Linphone/view/Control/Display/Chat/ChatMessage.qml b/Linphone/view/Control/Display/Chat/ChatMessage.qml index 20969177f..d1bd65d1c 100644 --- a/Linphone/view/Control/Display/Chat/ChatMessage.qml +++ b/Linphone/view/Control/Display/Chat/ChatMessage.qml @@ -14,37 +14,23 @@ Control.Control { property color backgroundColor property bool isFirstMessage - property string imgUrl - property ChatMessageGui chatMessage property string ownReaction: chatMessage? chatMessage.core.ownReaction : "" property string fromAddress: chatMessage? chatMessage.core.fromAddress : "" property bool isRemoteMessage: chatMessage? chatMessage.core.isRemoteMessage : false property bool isFromChatGroup: chatMessage? chatMessage.core.isFromChatGroup : false property var msgState: chatMessage ? chatMessage.core.messageState : LinphoneEnums.ChatMessageState.StateIdle - property string richFormatText: chatMessage.core.hasTextContent ? UtilsCpp.encodeTextToQmlRichFormat(chatMessage.core.utf8Text) : "" hoverEnabled: true property bool linkHovered: false property real maxWidth: parent?.width || Math.round(300 * DefaultStyle.dp) + leftPadding: isRemoteMessage ? Math.round(5 * DefaultStyle.dp) : 0 + signal messageDeletionRequested() + signal isFileHoveringChanged(bool isFileHovering) background: Item { anchors.fill: parent - Text { - id: fromNameText - visible: mainItem.isFromChatGroup && mainItem.isRemoteMessage && mainItem.isFirstMessage - anchors.top: parent.top - maximumLineCount: 1 - width: implicitWidth - x: chatBubble.x - text: mainItem.chatMessage.core.fromName - color: DefaultStyle.main2_500main - font { - pixelSize: Typography.p4.pixelSize - weight: Typography.p4.weight - } - } } function handleDefaultMouseEvent(event) { @@ -66,179 +52,162 @@ Control.Control { Layout.topMargin: isFirstMessage ? 16 * DefaultStyle.dp : 0 _address: chatMessage ? chatMessage.core.fromAddress : "" } - Item { - Layout.topMargin: isFirstMessage ? 16 * DefaultStyle.dp : 0 - Layout.leftMargin: mainItem.isFromChatGroup ? Math.round(9 * DefaultStyle.dp) : 0 - Layout.preferredHeight: childrenRect.height - Layout.preferredWidth: childrenRect.width - Control.Control { - id: chatBubble - spacing: Math.round(2 * DefaultStyle.dp) - topPadding: Math.round(12 * DefaultStyle.dp) - bottomPadding: Math.round(6 * DefaultStyle.dp) - leftPadding: Math.round(12 * DefaultStyle.dp) - rightPadding: Math.round(12 * DefaultStyle.dp) - width: Math.min(implicitWidth, mainItem.maxWidth - avatar.implicitWidth) - - MouseArea { // Default mouse area. Each sub bubble can control the mouse and pass on to the main mouse handler. Child bubble mouse area must cover the entire bubble. - id: defaultMouseArea - visible: invitationLoader.status !== Loader.Ready // Add other bubbles here that could control the mouse themselves, then add in bubble a signal onMouseEvent - anchors.fill: parent - acceptedButtons: Qt.RightButton - onClicked: (mouse) => mainItem.handleDefaultMouseEvent(mouse) - cursorShape: mainItem.linkHovered ? Qt.PointingHandCursor : Qt.ArrowCursor - } - - background: Item { - anchors.fill: parent - Rectangle { - anchors.fill: parent - color: mainItem.backgroundColor - radius: Math.round(16 * DefaultStyle.dp) - } - Rectangle { - visible: mainItem.isFirstMessage && mainItem.isRemoteMessage - anchors.top: parent.top - anchors.left: parent.left - width: Math.round(parent.width / 4) - height: Math.round(parent.height / 4) - color: mainItem.backgroundColor - } - Rectangle { - visible: mainItem.isFirstMessage && !mainItem.isRemoteMessage - anchors.bottom: parent.bottom - anchors.right: parent.right - width: Math.round(parent.width / 4) - height: Math.round(parent.height / 4) - color: mainItem.backgroundColor - } - } - contentItem: ColumnLayout { - id: contentLayout - Image { - visible: mainItem.imgUrl != undefined - id: contentimage - } - Text { // Uses default mouse area for link hovering. - id: textElement - visible: mainItem.richFormatText !== "" - text: mainItem.richFormatText - wrapMode: Text.Wrap - Layout.fillWidth: true - Layout.fillHeight: true - horizontalAlignment: Text.AlignLeft - color: DefaultStyle.main2_700 - textFormat: Text.RichText - font { - pixelSize: Typography.p1.pixelSize - weight: Typography.p1.weight - } - onLinkActivated: { - if (link.startsWith('sip')) - UtilsCpp.createCall(link) - else - Qt.openUrlExternally(link) - } - onHoveredLinkChanged: { - mainItem.linkHovered = hoveredLink !== "" - } - } - - // Meeting invitation bubble - ///////////////////////////// - Loader { - id: invitationLoader - active: mainItem.chatMessage.core.conferenceInfo !== null - sourceComponent: invitationComponent - } - Component { - id: invitationComponent - ChatMessageInvitationBubble { - conferenceInfoGui: mainItem.chatMessage.core.conferenceInfo - Layout.fillWidth: true - Layout.fillHeight: true - onMouseEvent: mainItem.handleDefaultMouseEvent(event) - } - } - ///////////////////////////// - - RowLayout { - Layout.alignment: mainItem.isRemoteMessage ? Qt.AlignLeft : Qt.AlignRight - Text { - text: UtilsCpp.formatDate(mainItem.chatMessage.core.timestamp, true, false) - color: DefaultStyle.main2_500main - font { - pixelSize: Typography.p3.pixelSize - weight: Typography.p3.weight - } - } - EffectImage { - visible: !mainItem.isRemoteMessage - Layout.preferredWidth: visible ? 14 * DefaultStyle.dp : 0 - Layout.preferredHeight: 14 * DefaultStyle.dp - colorizationColor: DefaultStyle.main1_500_main - imageSource: mainItem.msgState === LinphoneEnums.ChatMessageState.StateDelivered - ? AppIcons.envelope - : mainItem.msgState === LinphoneEnums.ChatMessageState.StateDeliveredToUser - ? AppIcons.check - : mainItem.msgState === LinphoneEnums.ChatMessageState.StateNotDelivered - ? AppIcons.warningCircle - : mainItem.msgState === LinphoneEnums.ChatMessageState.StateDisplayed - ? AppIcons.checks - : "" - } - } + ColumnLayout { + Layout.alignment: Qt.AlignTop + spacing: 0 + Text { + id: fromNameText + Layout.alignment: Qt.AlignTop + visible: mainItem.isFromChatGroup && mainItem.isRemoteMessage && mainItem.isFirstMessage + // anchors.top: parent.top + // anchors.left: parent.left + // anchors.leftMargin: avatar.width// mainItem.isFromChatGroup ? Math.round(9 * DefaultStyle.dp) : 0 + maximumLineCount: 1 + Layout.leftMargin: mainItem.isFromChatGroup ? Math.round(9 * DefaultStyle.dp) : 0 + width: implicitWidth + x: mapToItem(this, chatBubble.x, chatBubble.y).x + text: mainItem.chatMessage.core.fromName + color: DefaultStyle.main2_500main + font { + pixelSize: Typography.p4.pixelSize + weight: Typography.p4.weight } } - Button { - id: reactionsButton - visible: reactionList.count > 0 - anchors.top: chatBubble.bottom - Binding { - target: reactionsButton - when: !mainItem.isRemoteMessage - property: "anchors.left" - value: chatBubble.left - } - Binding { - target: reactionsButton - when: mainItem.isRemoteMessage - property: "anchors.right" - value: chatBubble.right - } - anchors.topMargin: Math.round(-6 * DefaultStyle.dp) - topPadding: Math.round(8 * DefaultStyle.dp) - bottomPadding: Math.round(8 * DefaultStyle.dp) - leftPadding: Math.round(8 * DefaultStyle.dp) - rightPadding: Math.round(8 * DefaultStyle.dp) - background: Rectangle { - color: DefaultStyle.grey_100 - border.color: DefaultStyle.grey_0 - border.width: Math.round(2 * DefaultStyle.dp) - radius: Math.round(20 * DefaultStyle.dp) - } - contentItem: RowLayout { - spacing: Math.round(6 * DefaultStyle.dp) - Repeater { - id: reactionList - model: mainItem.chatMessage ? mainItem.chatMessage.core.reactionsSingleton : [] - delegate: RowLayout { - spacing: Math.round(3 * DefaultStyle.dp) + Item { + // Layout.topMargin: isFirstMessage ? 16 * DefaultStyle.dp : 0 + Layout.leftMargin: mainItem.isFromChatGroup ? Math.round(9 * DefaultStyle.dp) : 0 + Layout.preferredHeight: childrenRect.height + Layout.preferredWidth: childrenRect.width + Control.Control { + id: chatBubble + spacing: Math.round(2 * DefaultStyle.dp) + topPadding: Math.round(12 * DefaultStyle.dp) + bottomPadding: Math.round(6 * DefaultStyle.dp) + leftPadding: Math.round(12 * DefaultStyle.dp) + rightPadding: Math.round(12 * DefaultStyle.dp) + width: Math.min(implicitWidth, mainItem.maxWidth - avatar.implicitWidth) + + MouseArea { // Default mouse area. Each sub bubble can control the mouse and pass on to the main mouse handler. Child bubble mouse area must cover the entire bubble. + id: defaultMouseArea + // visible: invitationLoader.status !== Loader.Ready // Add other bubbles here that could control the mouse themselves, then add in bubble a signal onMouseEvent + anchors.fill: parent + acceptedButtons: Qt.RightButton + onClicked: (mouse) => mainItem.handleDefaultMouseEvent(mouse) + } + + background: Item { + anchors.fill: parent + Rectangle { + anchors.fill: parent + color: mainItem.backgroundColor + radius: Math.round(16 * DefaultStyle.dp) + } + Rectangle { + visible: mainItem.isFirstMessage && mainItem.isRemoteMessage + anchors.top: parent.top + anchors.left: parent.left + width: Math.round(parent.width / 4) + height: Math.round(parent.height / 4) + color: mainItem.backgroundColor + } + Rectangle { + visible: mainItem.isFirstMessage && !mainItem.isRemoteMessage + anchors.bottom: parent.bottom + anchors.right: parent.right + width: Math.round(parent.width / 4) + height: Math.round(parent.height / 4) + color: mainItem.backgroundColor + } + } + contentItem: ColumnLayout { + spacing: Math.round(5 * DefaultStyle.dp) + ChatMessageContent { + id: chatBubbleContent + Layout.fillWidth: true + Layout.fillHeight: true + chatMessageGui: mainItem.chatMessage + onMouseEvent: (event) => { + mainItem.handleDefaultMouseEvent(event) + } + } + RowLayout { + Layout.alignment: mainItem.isRemoteMessage ? Qt.AlignLeft : Qt.AlignRight Text { - text: UtilsCpp.encodeEmojiToQmlRichFormat(modelData.body) - textFormat: Text.RichText + text: UtilsCpp.formatDate(mainItem.chatMessage.core.timestamp, true, false, "dd/MM") + color: DefaultStyle.main2_500main font { - pixelSize: Math.round(15 * DefaultStyle.dp) - weight: Math.round(400 * DefaultStyle.dp) + pixelSize: Typography.p3.pixelSize + weight: Typography.p3.weight } } - Text { - visible: modelData.count > 1 - text: modelData.count - verticalAlignment: Text.AlignBottom - font { - pixelSize: Typography.p4.pixelSize - weight: Typography.p4.weight + EffectImage { + visible: !mainItem.isRemoteMessage + Layout.preferredWidth: visible ? 14 * DefaultStyle.dp : 0 + Layout.preferredHeight: 14 * DefaultStyle.dp + colorizationColor: DefaultStyle.main1_500_main + imageSource: mainItem.msgState === LinphoneEnums.ChatMessageState.StateDelivered + ? AppIcons.envelope + : mainItem.msgState === LinphoneEnums.ChatMessageState.StateDeliveredToUser + ? AppIcons.check + : mainItem.msgState === LinphoneEnums.ChatMessageState.StateNotDelivered + ? AppIcons.warningCircle + : mainItem.msgState === LinphoneEnums.ChatMessageState.StateDisplayed + ? AppIcons.checks + : "" + } + } + } + } + Button { + id: reactionsButton + visible: reactionList.count > 0 + anchors.top: chatBubble.bottom + Binding { + target: reactionsButton + when: !mainItem.isRemoteMessage + property: "anchors.left" + value: chatBubble.left + } + Binding { + target: reactionsButton + when: mainItem.isRemoteMessage + property: "anchors.right" + value: chatBubble.right + } + anchors.topMargin: Math.round(-6 * DefaultStyle.dp) + topPadding: Math.round(8 * DefaultStyle.dp) + bottomPadding: Math.round(8 * DefaultStyle.dp) + leftPadding: Math.round(8 * DefaultStyle.dp) + rightPadding: Math.round(8 * DefaultStyle.dp) + background: Rectangle { + color: DefaultStyle.grey_100 + border.color: DefaultStyle.grey_0 + border.width: Math.round(2 * DefaultStyle.dp) + radius: Math.round(20 * DefaultStyle.dp) + } + contentItem: RowLayout { + spacing: Math.round(6 * DefaultStyle.dp) + Repeater { + id: reactionList + model: mainItem.chatMessage ? mainItem.chatMessage.core.reactionsSingleton : [] + delegate: RowLayout { + spacing: Math.round(3 * DefaultStyle.dp) + Text { + text: UtilsCpp.encodeEmojiToQmlRichFormat(modelData.body) + textFormat: Text.RichText + font { + pixelSize: Math.round(15 * DefaultStyle.dp) + weight: Math.round(400 * DefaultStyle.dp) + } + } + Text { + visible: modelData.count > 1 + text: modelData.count + verticalAlignment: Text.AlignBottom + font { + pixelSize: Typography.p4.pixelSize + weight: Typography.p4.weight + } } } } @@ -262,14 +231,17 @@ Control.Control { spacing: 0 IconLabelButton { inverseLayout: true - //: "Copy" - text: qsTr("chat_message_copy") + text: chatBubbleContent.selectedText != "" + //: "Copy selection" + ? qsTr("chat_message_copy_selection") + //: "Copy" + : qsTr("chat_message_copy") icon.source: AppIcons.copy // spacing: Math.round(10 * DefaultStyle.dp) Layout.fillWidth: true Layout.preferredHeight: 45 * DefaultStyle.dp onClicked: { - var success = UtilsCpp.copyToClipboard(mainItem.chatMessage.core.text) + var success = UtilsCpp.copyToClipboard(chatBubbleContent.selectedText != "" ? chatBubbleContent.selectedText : mainItem.chatMessage.core.text) //: Copied if (success) UtilsCpp.showInformationPopup(qsTr("chat_message_copied_to_clipboard_title"), //: "to clipboard" diff --git a/Linphone/view/Control/Display/Chat/ChatMessageContent.qml b/Linphone/view/Control/Display/Chat/ChatMessageContent.qml new file mode 100644 index 000000000..ca0c80039 --- /dev/null +++ b/Linphone/view/Control/Display/Chat/ChatMessageContent.qml @@ -0,0 +1,105 @@ +import QtQuick +import QtQuick.Effects +import QtQuick.Layouts +import QtQuick.Controls.Basic as Control +import Linphone + +// ============================================================================= +// Simple content display without reply and forward. These modules need to be splitted because of cyclic dependencies. +// See ChatFullContent + +ColumnLayout { + id: mainItem + property ChatMessageGui chatMessageGui: null + + signal isFileHoveringChanged(bool isFileHovering) + signal lastSelectedTextChanged(string selectedText) + // signal conferenceIcsCopied() + signal mouseEvent(MouseEvent event) + property string selectedText + + property color textColor + + property int fileBorderWidth : 0 + + spacing: Math.round(5 * DefaultStyle.dp) + 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 + // } + // CONFERENCE + Repeater { + id: conferenceList + visible: count > 0 + model: ChatMessageContentProxy{ + filterType: ChatMessageContentProxy.FilterContentType.Conference + chatMessageGui: mainItem.chatMessageGui + } + delegate: ChatMessageInvitationBubble { + Layout.fillWidth: true + conferenceInfoGui: modelData.core.conferenceInfo + // width: conferenceList.width + onMouseEvent: (event) => mainItem.mouseEvent(event) + } + } + // FILES + ChatFilesGridLayout { + id: messageFilesList + visible: itemCount > 0 + Layout.fillWidth: true + maxWidth: Math.round(115*3 * DefaultStyle.dp) + Layout.fillHeight: true + // Layout.preferredHeight: contentHeight + chatMessageGui: mainItem.chatMessageGui + // onIsHoveringFileChanged: mainItem.isHoveringFile = isHoveringFile + onIsHoveringFileChanged: mainItem.isFileHoveringChanged(isHoveringFile) + // borderWidth: mainItem.fileBorderWidth + // property int availableSection: mainItem.availableWidth / mainItem.filesBestWidth + // property int bestFitSection: mainItem.bestWidth / mainItem.filesBestWidth + // columns: Math.max(1, Math.min(availableSection , bestFitSection)) + // columnSpacing: 0 + // rowSpacing: ChatStyle.entry.message.file.spacing + } + // TEXTS + Repeater { + id: messagesTextsList + visible: count > 0 + model: ChatMessageContentProxy { + filterType: ChatMessageContentProxy.FilterContentType.Text + chatMessageGui: mainItem.chatMessageGui + } + delegate: ChatTextContent { + Layout.fillWidth: true + // height: implicitHeight + contentGui: modelData + onLastTextSelectedChanged: mainItem.selectedText = selectedText + // onRightClicked: mainItem.rightClicked() + } + } +} diff --git a/Linphone/view/Control/Display/Chat/ChatMessageInvitationBubble.qml b/Linphone/view/Control/Display/Chat/ChatMessageInvitationBubble.qml index 4e5f3997d..bce3875d0 100644 --- a/Linphone/view/Control/Display/Chat/ChatMessageInvitationBubble.qml +++ b/Linphone/view/Control/Display/Chat/ChatMessageInvitationBubble.qml @@ -16,7 +16,7 @@ Rectangle { clip: true antialiasing: true - property var conferenceInfoGui: ConferenceInfoGui + property ConferenceInfoGui conferenceInfoGui property var conferenceInfo: conferenceInfoGui?.core property string timeRangeText: "" property bool linkHovered: false diff --git a/Linphone/view/Control/Display/Chat/ChatTextContent.qml b/Linphone/view/Control/Display/Chat/ChatTextContent.qml new file mode 100644 index 000000000..19afaff66 --- /dev/null +++ b/Linphone/view/Control/Display/Chat/ChatTextContent.qml @@ -0,0 +1,74 @@ +import QtQuick +import QtQuick.Layouts + +import Linphone + +import UtilsCpp + + +// TODO : into Loader +// ============================================================================= +TextEdit { + id: message + property ChatMessageContentGui contentGui + property string lastTextSelected : '' + color: DefaultStyle.main2_700 + font { + pixelSize: (contentGui && UtilsCpp.isOnlyEmojis(contentGui.core.text)) ? Typography.h1.pixelSize : Typography.p1.pixelSize + weight: Typography.p1.weight + } + // property int removeWarningFromBindingLoop : implicitWidth // Just a dummy variable to remove meaningless binding loop on implicitWidth + + visible: contentGui && contentGui.core.isText + textMargin: 0 + readOnly: true + selectByMouse: true + + text: visible ? UtilsCpp.encodeTextToQmlRichFormat(contentGui.core.utf8Text) + : '' + + textFormat: Text.RichText // To supports links and imgs. + wrapMode: TextEdit.Wrap + + onLinkActivated: (link) => { + if (link.startsWith('sip')) + UtilsCpp.createCall(link) + else + Qt.openUrlExternally(link) + } + onSelectedTextChanged:{ + if(selectedText != '') lastTextSelected = selectedText + // else { + // if(mouseArea.keepLastSelection) { + // mouseArea.keepLastSelection = false + // } + // } + } + onActiveFocusChanged: { + if(activeFocus) { + lastTextSelected = '' + } + else mouseArea.keepLastSelection = false + // deselect() + } + MouseArea { + id: mouseArea + property bool keepLastSelection: false + property int lastStartSelection:0 + property int lastEndSelection:0 + anchors.fill: parent + propagateComposedEvents: true + hoverEnabled: true + scrollGestureEnabled: false + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor + acceptedButtons: Qt.LeftButton + onPressed: (mouse) => { + // if(!keepLastSelection) { + // lastStartSelection = parent.selectionStart + // lastEndSelection = parent.selectionEnd + // } + keepLastSelection = true + mouse.accepted = false + } + } +} diff --git a/Linphone/view/Control/Display/Chat/FileView.qml b/Linphone/view/Control/Display/Chat/FileView.qml new file mode 100644 index 000000000..e6f26fa2e --- /dev/null +++ b/Linphone/view/Control/Display/Chat/FileView.qml @@ -0,0 +1,294 @@ +import QtQuick +import QtQuick.Controls as Control +import QtQuick.Layouts +import QtMultimedia + +import Linphone +import UtilsCpp +import 'qrc:/qt/qml/Linphone/view/Style/buttonStyle.js' as ButtonStyle +import 'qrc:/qt/qml/Linphone/view/Control/Tool/Helper/utils.js' as Utils + +// ============================================================================= + + +Item { + id: mainItem + property ChatMessageContentGui contentGui + property string thumbnail: contentGui && contentGui.core.thumbnail + property string name: contentGui && contentGui.core.name + property string filePath: contentGui && contentGui.core.filePath + property bool active: true + property real animationScale : FileViewStyle.animation.to + property alias imageScale: thumbnailProvider.scale + property bool wasDownloaded: contentGui && contentGui.core.wasDownloaded + property bool isAnimatedImage : contentGui && contentGui.core.wasDownloaded && UtilsCpp.isAnimatedImage(filePath) + property bool haveThumbnail: contentGui && UtilsCpp.canHaveThumbnail(filePath) + property int fileSize: contentGui ? contentGui.core.fileSize : 0 + property bool isTransferring + + Connections { + enabled: contentGui + target: contentGui.core + function onMsgStateChanged(state) { + isTransferring = state === LinphoneEnums.ChatMessageState.StateFileTransferInProgress + || state === LinphoneEnums.ChatMessageState.StateInProgress + } + } + + property bool isHovering: thumbnailProvider.state == 'hovered' + property bool isOutgoing: false + + MouseArea { + hoverEnabled: true + propagateComposedEvents: true + // Changing of cursor seems not to work with the Loader + // Use override cursor for this case + onEntered: { + UtilsCpp.setGlobalCursor(Qt.PointingHandCursor) + thumbnailProvider.state = 'hovered' + } + onExited: { + UtilsCpp.restoreGlobalCursor() + thumbnailProvider.state = '' + } + anchors.fill: parent + onClicked: (mouse) => { + mouse.accepted = false + if(mainItem.isTransferring) { + mainItem.contentGui.core.lCancelDownloadFile() + mouse.accepted = true + } + else if(!mainItem.contentGui.core.wasDownloaded) { + mouse.accepted = true + thumbnailProvider.state = '' + mainItem.contentGui.core.lDownloadFile() + } else if (Utils.pointIsInItem(this, thumbnailProvider, mouse)) { + mouse.accepted = true + // if(SettingsModel.isVfsEncrypted){ + // window.attachVirtualWindow(Utils.buildCommonDialogUri('FileViewDialog'), { + // contentGui: mainItem.contentGui, + // }, function (status) { + // }) + // }else + mainItem.contentGui.core.lOpenFile() + } else if (mainItem.contentGui) { + mouse.accepted = true + thumbnailProvider.state = '' + mainItem.contentGui.core.lOpenFile(true)// Show directory + } + } + } + + // --------------------------------------------------------------------- + // Thumbnail + // --------------------------------------------------------------------- + Component { + id: thumbnailImage + Item { + id: thumbnailSource + property bool isVideo: UtilsCpp.isVideo(mainItem.filePath) + property bool isImage: UtilsCpp.isImage(mainItem.filePath) + property bool isPdf: UtilsCpp.isPdf(mainItem.filePath) + Image { + anchors.fill: parent + visible: thumbnailSource.isPdf + source: AppIcons.filePdf + sourceSize.width: mainItem.width + sourceSize.height: mainItem.height + fillMode: Image.PreserveAspectFit + } + Image { + visible: thumbnailSource.isImage + mipmap: false//SettingsModel.mipmapEnabled + source: mainItem.thumbnail + sourceSize.width: mainItem.width + sourceSize.height: mainItem.height + autoTransform: true + fillMode: Image.PreserveAspectCrop + anchors.fill: parent + Image { + anchors.fill: parent + visible: parent.status !== Image.Ready + source: AppIcons.fileImage + sourceSize.width: mainItem.width + sourceSize.height: mainItem.height + fillMode: Image.PreserveAspectFit + } + } + Rectangle { + visible: thumbnailSource.isVideo + color: DefaultStyle.grey_1000 + anchors.fill: parent + Video { + id: videoThumbnail + anchors.fill: parent + position: 100 + source: "file:///" + mainItem.filePath + fillMode: playbackState === MediaPlayer.PlayingState ? VideoOutput.PreserveAspectFit : VideoOutput.PreserveAspectCrop + MouseArea { + propagateComposedEvents: false + enabled: videoThumbnail.visible + anchors.fill: parent + hoverEnabled: false + acceptedButtons: Qt.LeftButton + onClicked: (mouse) => { + mouse.accepted = true + videoThumbnail.playbackState === MediaPlayer.PlayingState ? videoThumbnail.pause() : videoThumbnail.play() + } + } + EffectImage { + anchors.centerIn: parent + visible: videoThumbnail.playbackState !== MediaPlayer.PlayingState + width: Math.round(24 * DefaultStyle.dp) + height: Math.round(24 * DefaultStyle.dp) + imageSource: AppIcons.playFill + colorizationColor: DefaultStyle.main2_0 + } + Text { + z: parent.z + 1 + RectangleTest{anchors.fill: parent} + property int timeDisplayed: videoThumbnail.playbackState === MediaPlayer.PlayingState ? videoThumbnail.position : videoThumbnail.duration + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.bottomMargin: Math.round(6 * DefaultStyle.dp) + anchors.leftMargin: Math.round(6 * DefaultStyle.dp) + text: UtilsCpp.formatDuration(timeDisplayed) + color: DefaultStyle.grey_0 + font { + pixelSize: Typography.d1.pixelSize + weight: Typography.d1.weight + } + } + } + } + } + } + Component { + id: animatedImage + AnimatedImage { + id: animatedImageSource + mipmap: false//SettingsModel.mipmapEnabled + source: 'file:/'+ mainItem.filePath + autoTransform: true + fillMode: Image.PreserveAspectFit + } + } + + // --------------------------------------------------------------------- + // Extension + // --------------------------------------------------------------------- + Component { + id: defaultFileView + + Control.Control { + id: defaultView + leftPadding: Math.round(4 * DefaultStyle.dp) + rightPadding: Math.round(4 * DefaultStyle.dp) + topPadding: Math.round(23 * DefaultStyle.dp) + bottomPadding: Math.round(4 * DefaultStyle.dp) + hoverEnabled: false + + background: Rectangle { + anchors.fill: parent + color: FileViewStyle.extension.background.color + radius: FileViewStyle.extension.radius + + Rectangle { + color: DefaultStyle.main2_200 + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + height: Math.round(23 * DefaultStyle.dp) + EffectImage { + anchors.centerIn: parent + imageSource: contentGui + ? UtilsCpp.isImage(mainItem.filePath) + ? AppIcons.fileImage + : UtilsCpp.isPdf(mainItem.filePath) + ? AppIcons.filePdf + : UtilsCpp.isText(mainItem.filePath) + ? AppIcons.fileText + : AppIcons.file + : '' + imageWidth: Math.round(14 * DefaultStyle.dp) + imageHeight: Math.round(14 * DefaultStyle.dp) + colorizationColor: DefaultStyle.main2_600 + } + } + } + + contentItem: Item { + Text { + id: fileName + visible: !progressBar.visible + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + // visible: mainItem.contentGui && !mainItem.isAnimatedImage + font.pixelSize: Typography.f1.pixelSize + font.weight: Typography.f1l.weight + wrapMode: Text.WrapAnywhere + maximumLineCount: 2 + text: mainItem.name + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + } + Text { + id: fileSizeText + visible: !progressBar.visible + anchors.bottom: parent.bottom + anchors.right: parent.right + text: Utils.formatSize(mainItem.fileSize) + font.pixelSize: Typography.f1l.pixelSize + font.weight: Typography.f1l.weight + } + RoundProgressBar { + id: progressBar + anchors.centerIn: parent + to: 100 + value: mainItem.contentGui ? (mainItem.fileSize>0 ? Math.floor(100 * mainItem.contentGui.core.fileOffset / mainItem.fileSize) : 0) : to + visible: mainItem.isTransferring && value != 0 + /* Change format? Current is % + text: if(mainRow.contentGui){ + var mainItem.fileSize = Utils.formatSize(mainRow.contentGui.core.mainItem.fileSize) + return progressBar.visible + ? Utils.formatSize(mainRow.contentGui.core.fileOffset) + '/' + mainItem.fileSize + : mainItem.fileSize + }else + return '' + */ + } + Rectangle { + visible: thumbnailProvider.state === 'hovered' && mainItem.contentGui && (/*!mainItem.isOutgoing &&*/ !mainItem.contentGui.core.wasDownloaded) + color: DefaultStyle.grey_0 + opacity: 0.5 + anchors.fill: parent + } + EffectImage { + visible: thumbnailProvider.state === 'hovered' && mainItem.contentGui && (/*!mainItem.isOutgoing &&*/ !mainItem.contentGui.core.wasDownloaded) + anchors.centerIn: parent + imageSource: AppIcons.download + width: Math.round(24 * DefaultStyle.dp) + height: Math.round(24 * DefaultStyle.dp) + colorizationColor: DefaultStyle.main2_600 + } + } + } + } + + Loader { + id: thumbnailProvider + anchors.fill: parent + sourceComponent: mainItem.contentGui + ? mainItem.isAnimatedImage + ? animatedImage + : mainItem.haveThumbnail + ? thumbnailImage + : defaultFileView + : undefined + + states: State { + name: 'hovered' + } + } +} \ No newline at end of file diff --git a/Linphone/view/Control/Display/RoundProgressBar.qml b/Linphone/view/Control/Display/RoundProgressBar.qml new file mode 100644 index 000000000..60bb3b537 --- /dev/null +++ b/Linphone/view/Control/Display/RoundProgressBar.qml @@ -0,0 +1,78 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Shapes +import Linphone + + +ProgressBar{ + id: mainItem + property string text: value + '%' + implicitHeight: 35 + implicitWidth: 35 + to: 100 + value: 0 + background: Item {} + Timer{ + id: animationTest + repeat: true + onTriggered: value = (value + 1) % to + interval: 5 + } + contentItem: Item{ + Shape { + id: shape + anchors.fill: parent + anchors.margins: Math.round(2 * DefaultStyle.dp) + + property real progressionRadius : Math.min(shape.width / 2, shape.height / 2) - Math.round(3 * DefaultStyle.dp) / 2 + + layer.enabled: true + layer.samples: 8 + layer.smooth: true + vendorExtensionsEnabled: false + + ShapePath { + id: pathDial + strokeColor: DefaultStyle.main1_100 + fillColor: 'transparent' + strokeWidth: Math.round(3 * DefaultStyle.dp) + capStyle: Qt.RoundCap + + PathAngleArc { + radiusX: shape.progressionRadius + radiusY: shape.progressionRadius + centerX: shape.width / 2 + centerY: shape.height / 2 + startAngle: -90 // top start + sweepAngle: 360 + } + } + + ShapePath { + id: pathProgress + strokeColor: DefaultStyle.main1_500_main + fillColor: 'transparent' + strokeWidth: Math.round(3 * DefaultStyle.dp) + capStyle: Qt.RoundCap + + PathAngleArc { + radiusX: shape.progressionRadius + radiusY: shape.progressionRadius + centerX: shape.width / 2 + centerY: shape.height / 2 + startAngle: -90 // top start + sweepAngle: (360/ mainItem.to * mainItem.value) + } + } + } + Text{ + anchors.centerIn: parent + text: mainItem.text + color: DefaultStyle.main1_500_main + font.pixelSize: Typography.p4.pixelSize + font.weight: Typography.p2.weight + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + } +} diff --git a/Linphone/view/Control/Input/Chat/ChatDroppableTextArea.qml b/Linphone/view/Control/Input/Chat/ChatDroppableTextArea.qml new file mode 100644 index 000000000..c8ab575eb --- /dev/null +++ b/Linphone/view/Control/Input/Chat/ChatDroppableTextArea.qml @@ -0,0 +1,223 @@ +import QtQuick +import QtQuick.Controls.Basic as Control +import QtQuick.Layouts +import Linphone +import UtilsCpp + +import 'qrc:/qt/qml/Linphone/view/Style/buttonStyle.js' as ButtonStyle +import 'qrc:/qt/qml/Linphone/view/Control/Tool/Helper/utils.js' as Utils + +Control.Control { + id: mainItem + + property alias placeholderText: sendingTextArea.placeholderText + property alias text: sendingTextArea.text + property alias textArea: sendingTextArea + property alias cursorPosition: sendingTextArea.cursorPosition + property alias emojiPickerButtonChecked: emojiPickerButton.checked + + property bool dropEnabled: true + property string dropDisabledReason + property bool isEphemeral : false + property bool emojiVisible: false + + // --------------------------------------------------------------------------- + + signal dropped (var files) + signal validText (string text) + signal sendText() + signal audioRecordRequest() + signal emojiClicked() + signal composing() + + // --------------------------------------------------------------------------- + + function _emitFiles (files) { + // Filtering files, other urls are forbidden. + files = files.reduce(function (files, file) { + console.log("dropping", file.toString()) + if (file.toString().startsWith("file:")) { + files.push(Utils.getSystemPathFromUri(file)) + } + + return files + }, []) + if (files.length > 0) { + dropped(files) + } + } + + // width: mainItem.implicitWidth + // height: mainItem.height + leftPadding: Math.round(15 * DefaultStyle.dp) + rightPadding: Math.round(15 * DefaultStyle.dp) + topPadding: Math.round(24 * DefaultStyle.dp) + bottomPadding: Math.round(16 * DefaultStyle.dp) + background: Rectangle { + anchors.fill: parent + color: DefaultStyle.grey_100 + MediumButton { + id: expandButton + anchors.top: parent.top + anchors.topMargin: Math.round(4 * DefaultStyle.dp) + anchors.horizontalCenter: parent.horizontalCenter + style: ButtonStyle.noBackgroundOrange + icon.source: checked ? AppIcons.downArrow : AppIcons.upArrow + checkable: true + } + } + contentItem: RowLayout { + spacing: Math.round(20 * DefaultStyle.dp) + RowLayout { + spacing: Math.round(16 * DefaultStyle.dp) + BigButton { + id: emojiPickerButton + style: ButtonStyle.noBackground + checkable: true + icon.source: checked ? AppIcons.closeX : AppIcons.smiley + } + BigButton { + style: ButtonStyle.noBackground + icon.source: AppIcons.paperclip + onClicked: { + console.log("TODO : open explorer to attach file") + } + } + Control.Control { + Layout.fillWidth: true + leftPadding: Math.round(15 * DefaultStyle.dp) + rightPadding: Math.round(15 * DefaultStyle.dp) + topPadding: Math.round(15 * DefaultStyle.dp) + bottomPadding: Math.round(15 * DefaultStyle.dp) + background: Rectangle { + id: inputBackground + anchors.fill: parent + radius: Math.round(35 * DefaultStyle.dp) + color: DefaultStyle.grey_0 + MouseArea { + anchors.fill: parent + onPressed: sendingTextArea.forceActiveFocus() + cursorShape: Qt.IBeamCursor + } + } + contentItem: RowLayout { + 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 + when: expandButton.checked + property: "Layout.preferredHeight" + value: Math.round(250 * DefaultStyle.dp) + restoreMode: Binding.RestoreBindingOrValue + } + Layout.fillHeight: true + contentHeight: sendingTextArea.contentHeight + contentWidth: width + + function ensureVisible(r) { + if (contentX >= r.x) + contentX = r.x; + else if (contentX+width <= r.x+r.width) + contentX = r.x+r.width-width; + if (contentY >= r.y) + contentY = r.y; + else if (contentY+height <= r.y+r.height) + contentY = r.y+r.height-height; + } + + TextArea { + id: sendingTextArea + width: sendingAreaFlickable.width + height: sendingAreaFlickable.height + textFormat: TextEdit.AutoText + //: Say something… : placeholder text for sending message text area + placeholderText: qsTr("chat_view_send_area_placeholder_text") + placeholderTextColor: DefaultStyle.main2_400 + color: DefaultStyle.main2_700 + font { + pixelSize: Typography.p1.pixelSize + weight: Typography.p1.weight + } + onCursorRectangleChanged: sendingAreaFlickable.ensureVisible(cursorRectangle) + wrapMode: TextEdit.WordWrap + Keys.onPressed: (event) => { + if ((event.key == Qt.Key_Enter || event.key == Qt.Key_Return) + && (!(event.modifier & Qt.ShiftModifier))) { + mainItem.sendText() + sendingTextArea.clear() + event.accepted = true + } + } + } + } + StackLayout { + id: stackButton + currentIndex: sendingTextArea.text.length === 0 ? 0 : 1 + BigButton { + style: ButtonStyle.noBackground + icon.source: AppIcons.microphone + onClicked: { + console.log("TODO : go to record message") + } + } + BigButton { + style: ButtonStyle.noBackgroundOrange + icon.source: AppIcons.paperPlaneRight + onClicked: { + mainItem.sendText() + sendingTextArea.clear() + } + } + } + } + } + } + } + + Rectangle { + id: hoverContent + anchors.fill: parent + color: DefaultStyle.main2_0 + visible: false + radius: Math.round(20 * DefaultStyle.dp) + + EffectImage { + anchors.centerIn: parent + imageSource: AppIcons.filePlus + width: Math.round(37 * DefaultStyle.dp) + height: Math.round(37 * DefaultStyle.dp) + colorizationColor: DefaultStyle.main2_500main + } + + DashRectangle { + x: parent.x + y: parent.y + radius: hoverContent.radius + color: DefaultStyle.main2_500main + width: parent.width + height: parent.height + } + } + DropArea { + anchors.fill: parent + keys: [ 'text/uri-list' ] + visible: mainItem.dropEnabled + + onDropped: (drop) => { + state = '' + if (drop.hasUrls) { + _emitFiles(drop.urls) + } + } + onEntered: state = 'hover' + onExited: state = '' + + states: State { + name: 'hover' + PropertyChanges { target: hoverContent; visible: true } + } + } +} \ No newline at end of file diff --git a/Linphone/view/Page/Form/Chat/SelectedChatView.qml b/Linphone/view/Page/Form/Chat/SelectedChatView.qml index b46f56f3f..219e0e6e5 100644 --- a/Linphone/view/Page/Form/Chat/SelectedChatView.qml +++ b/Linphone/view/Page/Form/Chat/SelectedChatView.qml @@ -76,210 +76,164 @@ RowLayout { } ] - content: [ - ChatMessagesListView { - id: chatMessagesListView - clip: true - height: contentHeight - backgroundColor: splitPanel.panelColor - width: parent.width - anchors.leftMargin - anchors.rightMargin - chat: mainItem.chat - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: messageSender.top - anchors.leftMargin: Math.round(18 * DefaultStyle.dp) - anchors.rightMargin: Math.round(18 * DefaultStyle.dp) - Control.ScrollBar.vertical: scrollbar - - Popup { - id: emojiPickerPopup - y: Math.round(chatMessagesListView.y + chatMessagesListView.height - height - 8*DefaultStyle.dp) - x: Math.round(chatMessagesListView.x + 8*DefaultStyle.dp) - width: Math.round(393 * DefaultStyle.dp) - height: Math.round(291 * DefaultStyle.dp) - visible: emojiPickerButton.checked - closePolicy: Popup.CloseOnPressOutside - onClosed: emojiPickerButton.checked = false - padding: 10 * DefaultStyle.dp - background: Item { - anchors.fill: parent - Rectangle { - id: buttonBackground - anchors.fill: parent - color: DefaultStyle.grey_0 - radius: Math.round(20 * DefaultStyle.dp) - } - MultiEffect { - anchors.fill: buttonBackground - source: buttonBackground - shadowEnabled: true - shadowColor: DefaultStyle.grey_1000 - shadowBlur: 0.1 - shadowOpacity: 0.5 - } - } - contentItem: EmojiPicker { - id: emojiPicker - editor: sendingTextArea - } - } - }, - ScrollBar { - id: scrollbar - visible: chatMessagesListView.contentHeight > parent.height - active: visible - anchors.top: chatMessagesListView.top - anchors.bottom: chatMessagesListView.bottom - anchors.right: parent.right - anchors.rightMargin: Math.round(5 * DefaultStyle.dp) - policy: Control.ScrollBar.AsNeeded - }, - Control.Control { - id: messageSender - visible: !mainItem.chat.core.isReadOnly - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.right: parent.right - leftPadding: Math.round(15 * DefaultStyle.dp) - rightPadding: Math.round(15 * DefaultStyle.dp) - topPadding: Math.round(24 * DefaultStyle.dp) - bottomPadding: Math.round(16 * DefaultStyle.dp) - background: Rectangle { + content: ColumnLayout { + spacing: 0 + anchors.fill: parent + Item { + Layout.fillWidth: true + Layout.fillHeight: true + ChatMessagesListView { + id: chatMessagesListView + clip: true + height: contentHeight + backgroundColor: splitPanel.panelColor + width: parent.width - anchors.leftMargin - anchors.rightMargin + chat: mainItem.chat anchors.fill: parent - color: DefaultStyle.grey_100 - MediumButton { - id: expandButton - anchors.top: parent.top - anchors.topMargin: Math.round(4 * DefaultStyle.dp) - anchors.horizontalCenter: parent.horizontalCenter - style: ButtonStyle.noBackgroundOrange - icon.source: checked ? AppIcons.downArrow : AppIcons.upArrow - checkable: true + anchors.leftMargin: Math.round(18 * DefaultStyle.dp) + anchors.rightMargin: Math.round(18 * DefaultStyle.dp) + Control.ScrollBar.vertical: scrollbar + + Popup { + id: emojiPickerPopup + y: Math.round(chatMessagesListView.y + chatMessagesListView.height - height - 8*DefaultStyle.dp) + x: Math.round(chatMessagesListView.x + 8*DefaultStyle.dp) + width: Math.round(393 * DefaultStyle.dp) + height: Math.round(291 * DefaultStyle.dp) + visible: messageSender.emojiPickerButtonChecked + closePolicy: Popup.CloseOnPressOutside + onClosed: messageSender.emojiPickerButtonChecked = false + padding: 10 * DefaultStyle.dp + background: Item { + anchors.fill: parent + Rectangle { + id: buttonBackground + anchors.fill: parent + color: DefaultStyle.grey_0 + radius: Math.round(20 * DefaultStyle.dp) + } + MultiEffect { + anchors.fill: buttonBackground + source: buttonBackground + shadowEnabled: true + shadowColor: DefaultStyle.grey_1000 + shadowBlur: 0.1 + shadowOpacity: 0.5 + } + } + contentItem: EmojiPicker { + id: emojiPicker + editor: messageSender.textArea + } } } - contentItem: RowLayout { - spacing: Math.round(20 * DefaultStyle.dp) - RowLayout { - spacing: Math.round(16 * DefaultStyle.dp) - BigButton { - id: emojiPickerButton - style: ButtonStyle.noBackground - checkable: true - icon.source: checked ? AppIcons.closeX : AppIcons.smiley - } - BigButton { - style: ButtonStyle.noBackground - icon.source: AppIcons.paperclip - onClicked: { - console.log("TODO : open explorer to attach file") - } - } - Control.Control { - Layout.fillWidth: true - leftPadding: Math.round(15 * DefaultStyle.dp) - rightPadding: Math.round(15 * DefaultStyle.dp) - topPadding: Math.round(15 * DefaultStyle.dp) - bottomPadding: Math.round(15 * DefaultStyle.dp) - background: Rectangle { - id: inputBackground - anchors.fill: parent - radius: Math.round(35 * DefaultStyle.dp) - color: DefaultStyle.grey_0 - MouseArea { - anchors.fill: parent - onPressed: sendingTextArea.forceActiveFocus() - cursorShape: Qt.IBeamCursor - } - } - contentItem: RowLayout { - 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 - when: expandButton.checked - property: "Layout.preferredHeight" - value: Math.round(250 * DefaultStyle.dp) - restoreMode: Binding.RestoreBindingOrValue - } - Layout.fillHeight: true - contentHeight: sendingTextArea.contentHeight - contentWidth: width + ScrollBar { + id: scrollbar + visible: chatMessagesListView.contentHeight > parent.height + active: visible + anchors.top: chatMessagesListView.top + anchors.bottom: chatMessagesListView.bottom + anchors.right: parent.right + anchors.rightMargin: Math.round(5 * DefaultStyle.dp) + policy: Control.ScrollBar.AsNeeded + } + } + Control.Control { + id: selectedFilesArea + visible: selectedFiles.count > 0 + Layout.fillWidth: true + Layout.preferredHeight: Math.round(104 * DefaultStyle.dp) + topPadding: Math.round(12 * DefaultStyle.dp) + bottomPadding: Math.round(12 * DefaultStyle.dp) + leftPadding: Math.round(19 * DefaultStyle.dp) + rightPadding: Math.round(19 * DefaultStyle.dp) - function ensureVisible(r) { - if (contentX >= r.x) - contentX = r.x; - else if (contentX+width <= r.x+r.width) - contentX = r.x+r.width-width; - if (contentY >= r.y) - contentY = r.y; - else if (contentY+height <= r.y+r.height) - contentY = r.y+r.height-height; - } - - TextArea { - id: sendingTextArea - width: sendingAreaFlickable.width - height: sendingAreaFlickable.height - textFormat: TextEdit.AutoText - //: Say something… : placeholder text for sending message text area - placeholderText: qsTr("chat_view_send_area_placeholder_text") - placeholderTextColor: DefaultStyle.main2_400 - color: DefaultStyle.main2_700 - font { - pixelSize: Typography.p1.pixelSize - weight: Typography.p1.weight - } - onCursorRectangleChanged: sendingAreaFlickable.ensureVisible(cursorRectangle) - wrapMode: TextEdit.WordWrap - Component.onCompleted: { - if (mainItem.chat) text = mainItem.chat.core.sendingText - } - onTextChanged: { - if (text !== "" && mainItem.chat.core.composingName !== "") { - mainItem.chat.core.lCompose() - } - mainItem.chat.core.sendingText = text - } - Keys.onPressed: (event) => { - event.accepted = false - if (UtilsCpp.isEmptyMessage(sendingTextArea.text)) return - if (!(event.modifiers & Qt.ShiftModifier) && (event.key == Qt.Key_Return || event.key == Qt.Key_Enter)) { - mainItem.chat.core.lSendTextMessage(sendingTextArea.text) - sendingTextArea.clear() - event.accepted = true - } - } - } - } - StackLayout { - id: stackButton - currentIndex: sendingTextArea.text.length === 0 ? 0 : 1 - BigButton { - style: ButtonStyle.noBackground - icon.source: AppIcons.microphone - onClicked: { - console.log("TODO : go to record message") - } - } - BigButton { - style: ButtonStyle.noBackgroundOrange - icon.source: AppIcons.paperPlaneRight - onClicked: { - mainItem.chat.core.lSendTextMessage(sendingTextArea.text) - sendingTextArea.clear() - } - } - } - } + function addFile(path) { + contents.addFile(path) + } + + Button { + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: selectedFilesArea.topPadding + anchors.rightMargin: selectedFilesArea.rightPadding + icon.source: AppIcons.closeX + style: ButtonStyle.noBackground + onClicked: { + contents.clear() + } + } + background: Item{ + anchors.fill: parent + Rectangle { + color: DefaultStyle.grey_0 + border.color: DefaultStyle.main2_100 + border.width: Math.round(2 * DefaultStyle.dp) + radius: Math.round(20 * DefaultStyle.dp) + height: parent.height / 2 + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + } + Rectangle { + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + height: 2 * parent.height / 3 + } + } + contentItem: ListView { + id: selectedFiles + orientation: ListView.Horizontal + spacing: Math.round(16 * DefaultStyle.dp) + model: ChatMessageContentProxy { + id: contents + filterType: ChatMessageContentProxy.FilterContentType.File + } + delegate: Item { + width: Math.round(80 * DefaultStyle.dp) + height: Math.round(80 * DefaultStyle.dp) + FileView { + contentGui: modelData + anchors.left: parent.left + anchors.bottom: parent.bottom + width: Math.round(69 * DefaultStyle.dp) + height: Math.round(69 * DefaultStyle.dp) + } + RoundButton { + icon.source: AppIcons.closeX + icon.width: Math.round(12 * DefaultStyle.dp) + icon.height: Math.round(12 * DefaultStyle.dp) + anchors.top: parent.top + anchors.right: parent.right + style: ButtonStyle.numericPad + shadowEnabled: true + padding: Math.round(3 * DefaultStyle.dp) + onClicked: contents.removeContent(modelData) } } } } - ] + ChatDroppableTextArea { + id: messageSender + visible: !mainItem.chat.core.isReadOnly + Layout.fillWidth: true + Layout.preferredHeight: height + Component.onCompleted: { + if (mainItem.chat) text = mainItem.chat.core.sendingText + } + onTextChanged: { + if (text !== "" && mainItem.chat.core.composingName !== "") { + mainItem.chat.core.lCompose() + } + mainItem.chat.core.sendingText = text + } + onSendText: mainItem.chat.core.lSendTextMessage(text) + onDropped: (files) => { + files.forEach(selectedFilesArea.addFile) + } + } + } } Rectangle { diff --git a/Linphone/view/Style/AppIcons.qml b/Linphone/view/Style/AppIcons.qml index 107e92028..1305cb3bc 100644 --- a/Linphone/view/Style/AppIcons.qml +++ b/Linphone/view/Style/AppIcons.qml @@ -75,6 +75,13 @@ QtObject { property string avatar: "image://internal/randomAvatar.png" property string pause: "image://internal/pause.svg" property string play: "image://internal/play.svg" + property string playFill: "image://internal/play-fill.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" + property string filePlus: "image://internal/file-plus.svg" + property string download: "image://internal/download-simple.svg" + property string file: "image://internal/file.svg" property string paperclip: "image://internal/paperclip.svg" property string paperPlaneRight: "image://internal/paper-plane-right.svg" property string smiley: "image://internal/smiley.svg" diff --git a/Linphone/view/Style/DefaultStyle.qml b/Linphone/view/Style/DefaultStyle.qml index 21ecbbf1f..8c8478111 100644 --- a/Linphone/view/Style/DefaultStyle.qml +++ b/Linphone/view/Style/DefaultStyle.qml @@ -51,6 +51,7 @@ QtObject { onDpChanged: { console.log("Screen ratio changed", dp) + AppCpp.setScreenRatio(dp) } // Warning: Qt 6.8.1 (current version) and previous versions, Qt only support COLRv0 fonts. Don't try to use v1. diff --git a/Linphone/view/Style/FileViewStyle.qml b/Linphone/view/Style/FileViewStyle.qml new file mode 100644 index 000000000..699b3bf5c --- /dev/null +++ b/Linphone/view/Style/FileViewStyle.qml @@ -0,0 +1,73 @@ +pragma Singleton +import QtQml + +import Linphone + +// ============================================================================= + +QtObject { + property string sectionName : 'FileView' + + property int height: 120 + property int heightbetter: 200 + property int iconSize: 18 + property int margins: 8 + property int spacing: 8 + property int width: 100 + + property QtObject name: QtObject{ + property int pointSize: Math.round(DefaultStyle.dp * 7) + } + + property QtObject download: QtObject{ + property string icon: AppIcons.download + property int height: 20 + property int pointSize: Math.round(DefaultStyle.dp * 8) + property int iconSize: 30 + } + property QtObject thumbnailVideoIcon: QtObject { + property int iconSize: 40 + property string name : 'play' + property string icon : AppIcons.playFill + } + property QtObject animation: QtObject { + property int duration: 300 + property real to: 1.7 + property real thumbnailTo: 2 + } + + property QtObject extension: QtObject { + property string icon: AppIcons.file + property string imageIcon: AppIcons.fileImage + property int iconSize: 60 + property int internalSize: 37 + property int radius: Math.round(5 * DefaultStyle.dp) + + property QtObject background: QtObject { + property var color: DefaultStyle.grey_0 + property var borderColor: DefaultStyle.grey_0 + } + + property QtObject text: QtObject { + property var color: DefaultStyle.grey_0 + property int pointSize: Math.round(DefaultStyle.dp * 9) + } + } + + property QtObject status: QtObject { + property int spacing: 4 + + property QtObject bar: QtObject { + property int height: 6 + property int radius: 3 + + property QtObject background: QtObject { + property var color: DefaultStyle.grey_0 + } + + property QtObject contentItem: QtObject { + property var color: DefaultStyle.grey_0 + } + } + } +} diff --git a/Linphone/view/Style/Typography.qml b/Linphone/view/Style/Typography.qml index 884d8fce9..2c979b566 100644 --- a/Linphone/view/Style/Typography.qml +++ b/Linphone/view/Style/Typography.qml @@ -59,7 +59,7 @@ QtObject { weight: Math.min(Math.round(700 * DefaultStyle.dp), 1000) }) - // Text/P2 - Large Bold, reduced paragraph text + // Text/P2l - Large Bold, reduced paragraph text property font p2l: Qt.font( { family: DefaultStyle.defaultFont, pixelSize: Math.round(14 * DefaultStyle.dp), @@ -73,7 +73,7 @@ QtObject { weight: Math.min(Math.round(400 * DefaultStyle.dp), 1000) }) - // Text/P1 - Paragraph text + // Text/P1s - Paragraph text property font p1s: Qt.font( { family: DefaultStyle.defaultFont, pixelSize: Math.round(13 * DefaultStyle.dp), @@ -101,4 +101,24 @@ QtObject { weight: Math.min(Math.round(600 * DefaultStyle.dp), 1000) }) + // FileView/F1 - File View name text + property font f1: Qt.font( { + family: DefaultStyle.defaultFont, + pixelSize: Math.round(11 * DefaultStyle.dp), + weight: Math.min(Math.round(700 * DefaultStyle.dp), 1000) + }) + + // FileView/F1light - File View size text + property font f1l: Qt.font( { + family: DefaultStyle.defaultFont, + pixelSize: Math.round(10 * DefaultStyle.dp), + weight: Math.min(Math.round(500 * DefaultStyle.dp), 1000) + }) + + // FileView/F1light - Duration text + property font d1: Qt.font( { + family: DefaultStyle.defaultFont, + pixelSize: Math.round(8 * DefaultStyle.dp), + weight: Math.min(Math.round(600 * DefaultStyle.dp), 1000) + }) } diff --git a/Linphone/view/Style/buttonStyle.js b/Linphone/view/Style/buttonStyle.js index 622a197c3..ccecee2b0 100644 --- a/Linphone/view/Style/buttonStyle.js +++ b/Linphone/view/Style/buttonStyle.js @@ -127,6 +127,28 @@ } } +// No background light + var noBackgroundLight = { + color: { + normal: "#00000000", + hovered: "#00000000", + pressed: "#00000000", + checked: "#00000000" + }, + text: { + normal: Linphone.DefaultStyle.main2_200, + hovered: Linphone.DefaultStyle.main2_300, + pressed: Linphone.DefaultStyle.main2_400, + checked: Linphone.DefaultStyle.main1_500main + }, + image: { + normal: Linphone.DefaultStyle.main2_200, + hovered: Linphone.DefaultStyle.main2_300, + pressed: Linphone.DefaultStyle.main2_400, + checked: Linphone.DefaultStyle.main1_500main, + } + } + // No background var noBackground = { color: {