diff --git a/kaidan_qml.qrc b/kaidan_qml.qrc index 7a3ca7f..2d48f32 100644 --- a/kaidan_qml.qrc +++ b/kaidan_qml.qrc @@ -1,43 +1,51 @@ src/qml/main.qml src/qml/RosterPage.qml src/qml/LoginPage.qml src/qml/ChatPage.qml src/qml/AboutDialog.qml src/qml/GlobalDrawer.qml src/qml/EmptyChatPage.qml src/qml/QrCodeScannerPage.qml src/qml/UserProfilePage.qml + src/qml/MultimediaSettingsPage.qml src/qml/elements/SubRequestAcceptSheet.qml src/qml/elements/RosterAddContactSheet.qml src/qml/elements/RosterRenameContactSheet.qml src/qml/elements/RosterRemoveContactSheet.qml src/qml/elements/RosterListItem.qml src/qml/elements/MessageCounter.qml src/qml/elements/ChatMessage.qml src/qml/elements/RoundImage.qml src/qml/elements/IconButton.qml src/qml/elements/FileChooser.qml src/qml/elements/FileChooserDesktop.qml src/qml/elements/FileChooserMobile.qml src/qml/elements/SendMediaSheet.qml + src/qml/elements/MediaPreview.qml src/qml/elements/MediaPreviewImage.qml - src/qml/elements/MediaPreviewVideo.qml src/qml/elements/MediaPreviewAudio.qml + src/qml/elements/MediaPreviewVideo.qml src/qml/elements/MediaPreviewOther.qml + src/qml/elements/MediaPreviewLocation.qml src/qml/elements/MediaPreviewLoader.qml + + src/qml/elements/NewMedia.qml + src/qml/elements/NewMediaLocation.qml + src/qml/elements/NewMediaLoader.qml + src/qml/elements/EmojiPicker.qml src/qml/elements/TextAvatar.qml src/qml/elements/Avatar.qml src/qml/settings/SettingsItem.qml src/qml/settings/SettingsPage.qml src/qml/settings/SettingsSheet.qml src/qml/settings/ChangePassword.qml misc/qtquickcontrols2.conf diff --git a/src/AudioDeviceModel.cpp b/src/AudioDeviceModel.cpp new file mode 100644 index 0000000..a5d4dec --- /dev/null +++ b/src/AudioDeviceModel.cpp @@ -0,0 +1,243 @@ +/* + * Kaidan - A user-friendly XMPP client for every device! + * + * Copyright (C) 2016-2019 Kaidan developers and contributors + * (see the LICENSE file for a full list of copyright authors) + * + * Kaidan 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. + * + * In addition, as a special exception, the author of Kaidan gives + * permission to link the code of its release with the OpenSSL + * project's "OpenSSL" library (or with modified versions of it that + * use the same license as the "OpenSSL" library), and distribute the + * linked executables. You must obey the GNU General Public License in + * all respects for all of the code used other than "OpenSSL". If you + * modify this file, you may extend this exception to your version of + * the file, but you are not obligated to do so. If you do not wish to + * do so, delete this exception statement from your version. + * + * Kaidan 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 Kaidan. If not, see . + */ + +#include "AudioDeviceModel.h" + +static AudioDeviceInfo audioDeviceByDeviceName(QAudio::Mode mode, const QString &deviceName) { + const auto audioDevices(QAudioDeviceInfo::availableDevices(mode)); + + for (const auto &audioDevice: audioDevices) { + if (audioDevice.deviceName() == deviceName) { + return AudioDeviceInfo(audioDevice); + } + } + + return AudioDeviceInfo(); +} + +AudioDeviceInfo::AudioDeviceInfo(const QAudioDeviceInfo &other) + : QAudioDeviceInfo(other) +{ +} + +QString AudioDeviceInfo::description() const +{ + return description(deviceName()); +} + +QString AudioDeviceInfo::description(const QString &deviceName) +{ + return QString(deviceName) + .replace(QLatin1Char(':'), QLatin1Char(' ')) + .replace(QLatin1Char('='), QLatin1Char(' ')) + .replace(QLatin1Char(','), QLatin1Char(' ')) + .trimmed(); +} + +AudioDeviceModel::AudioDeviceModel(QObject *parent) + : QAbstractListModel(parent) +{ + refresh(); + + connect(this, &AudioDeviceModel::modeChanged, this, &AudioDeviceModel::refresh); +} + +int AudioDeviceModel::rowCount(const QModelIndex &parent) const +{ + return parent == QModelIndex() ? m_audioDevices.count() : 0; +} + +QVariant AudioDeviceModel::data(const QModelIndex &index, int role) const +{ + if (hasIndex(index.row(), index.column(), index.parent())) { + const auto &audioDeviceInfo(m_audioDevices[index.row()]); + + switch (role) { + case AudioDeviceModel::CustomRoles::IsNullRole: + return audioDeviceInfo.isNull(); + case AudioDeviceModel::CustomRoles::DeviceNameRole: + return audioDeviceInfo.deviceName(); + case AudioDeviceModel::CustomRoles::DescriptionRole: + return AudioDeviceInfo::description(audioDeviceInfo.deviceName()); + case AudioDeviceModel::CustomRoles::SupportedCodecsRole: + return audioDeviceInfo.supportedCodecs(); + case AudioDeviceModel::CustomRoles::SupportedSampleRatesRole: + return QVariant::fromValue(audioDeviceInfo.supportedSampleRates()); + case AudioDeviceModel::CustomRoles::SupportedChannelCountsRole: + return QVariant::fromValue(audioDeviceInfo.supportedChannelCounts()); + case AudioDeviceModel::CustomRoles::SupportedSampleSizesRole: + return QVariant::fromValue(audioDeviceInfo.supportedSampleSizes()); + case AudioDeviceModel::CustomRoles::AudioDeviceInfoRole: + return QVariant::fromValue(AudioDeviceInfo(audioDeviceInfo)); + } + } + + return QVariant(); +} + +QHash AudioDeviceModel::roleNames() const +{ + static const QHash roles { + { IsNullRole, QByteArrayLiteral("isNull") }, + { DeviceNameRole, QByteArrayLiteral("deviceName") }, + { DescriptionRole, QByteArrayLiteral("description") }, + { SupportedCodecsRole, QByteArrayLiteral("supportedCodecs") }, + { SupportedSampleRatesRole, QByteArrayLiteral("supportedSampleRates") }, + { SupportedChannelCountsRole, QByteArrayLiteral("supportedChannelCounts") }, + { SupportedSampleSizesRole, QByteArrayLiteral("supportedSampleSizes") }, + { AudioDeviceInfoRole, QByteArrayLiteral("audioDeviceInfo") } + }; + + return roles; +} + +AudioDeviceModel::Mode AudioDeviceModel::mode() const +{ + return m_mode; +} + +void AudioDeviceModel::setMode(AudioDeviceModel::Mode mode) +{ + if (m_mode == mode) { + return; + } + + m_mode = mode; + emit modeChanged(); +} + +QList AudioDeviceModel::audioDevices() const +{ + return m_audioDevices; +} + +AudioDeviceInfo AudioDeviceModel::defaultAudioDevice() const +{ + switch (m_mode) { + case AudioDeviceModel::Mode::AudioInput: + return defaultAudioInputDevice(); + case AudioDeviceModel::Mode::AudioOutput: + return defaultAudioOutputDevice(); + } + + Q_UNREACHABLE(); + return AudioDeviceInfo(); +} + +int AudioDeviceModel::currentIndex() const +{ + return m_currentIndex; +} + +void AudioDeviceModel::setCurrentIndex(int currentIndex) +{ + if (currentIndex < 0 || currentIndex >= m_audioDevices.count() + || m_currentIndex == currentIndex) { + return; + } + + m_currentIndex = currentIndex; + emit currentIndexChanged(); +} + +AudioDeviceInfo AudioDeviceModel::currentAudioDevice() const +{ + return m_currentIndex >= 0 && m_currentIndex < m_audioDevices.count() + ? AudioDeviceInfo(m_audioDevices[m_currentIndex]) + : AudioDeviceInfo(); +} + +void AudioDeviceModel::setCurrentAudioDevice(const AudioDeviceInfo ¤tAudioDevice) +{ + setCurrentIndex(m_audioDevices.indexOf(currentAudioDevice)); +} + +AudioDeviceInfo AudioDeviceModel::audioDevice(int row) const +{ + return hasIndex(row, 0) + ? AudioDeviceInfo(m_audioDevices[row]) + : AudioDeviceInfo(); +} + +int AudioDeviceModel::indexOf(const QString &deviceName) const +{ + for (int i = 0; i < m_audioDevices.count(); ++i) { + const auto &audioDevice(m_audioDevices[i]); + + if (audioDevice.deviceName() == deviceName) { + return i; + } + } + + return -1; +} + +AudioDeviceInfo AudioDeviceModel::defaultAudioInputDevice() +{ + return AudioDeviceInfo(QAudioDeviceInfo::defaultInputDevice()); +} + +AudioDeviceInfo AudioDeviceModel::audioInputDevice(const QString &deviceName) +{ + return audioDeviceByDeviceName(QAudio::AudioInput, deviceName); +} + +AudioDeviceInfo AudioDeviceModel::defaultAudioOutputDevice() +{ + return AudioDeviceInfo(QAudioDeviceInfo::defaultOutputDevice()); +} + +AudioDeviceInfo AudioDeviceModel::audioOutputDevice(const QString &deviceName) +{ + return audioDeviceByDeviceName(QAudio::AudioOutput, deviceName); +} + +void AudioDeviceModel::refresh() +{ + const auto audioDevices = QAudioDeviceInfo::availableDevices(static_cast(m_mode)); + + if (m_audioDevices == audioDevices) { + return; + } + + beginResetModel(); + const QString currentDeviceName = currentAudioDevice().deviceName(); + const auto it = std::find_if(m_audioDevices.constBegin(), m_audioDevices.constEnd(), + [¤tDeviceName](const QAudioDeviceInfo &deviceInfo) { + return deviceInfo.deviceName() == currentDeviceName; + }); + + m_audioDevices = audioDevices; + m_currentIndex = it == m_audioDevices.constEnd() ? -1 : it - m_audioDevices.constBegin(); + endResetModel(); + + emit audioDevicesChanged(); + emit currentIndexChanged(); +} diff --git a/src/AudioDeviceModel.h b/src/AudioDeviceModel.h new file mode 100644 index 0000000..2c78280 --- /dev/null +++ b/src/AudioDeviceModel.h @@ -0,0 +1,127 @@ +/* + * Kaidan - A user-friendly XMPP client for every device! + * + * Copyright (C) 2016-2019 Kaidan developers and contributors + * (see the LICENSE file for a full list of copyright authors) + * + * Kaidan 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. + * + * In addition, as a special exception, the author of Kaidan gives + * permission to link the code of its release with the OpenSSL + * project's "OpenSSL" library (or with modified versions of it that + * use the same license as the "OpenSSL" library), and distribute the + * linked executables. You must obey the GNU General Public License in + * all respects for all of the code used other than "OpenSSL". If you + * modify this file, you may extend this exception to your version of + * the file, but you are not obligated to do so. If you do not wish to + * do so, delete this exception statement from your version. + * + * Kaidan 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 Kaidan. If not, see . + */ + +#pragma once + +#include +#include + +class AudioDeviceInfo : public QAudioDeviceInfo { + Q_GADGET + + Q_PROPERTY(bool isNull READ isNull CONSTANT) + Q_PROPERTY(QString deviceName READ deviceName CONSTANT) + Q_PROPERTY(QString description READ description CONSTANT) + Q_PROPERTY(QStringList supportedCodecs READ supportedCodecs CONSTANT) + Q_PROPERTY(QList supportedSampleRates READ supportedSampleRates CONSTANT) + Q_PROPERTY(QList supportedChannelCounts READ supportedChannelCounts CONSTANT) + Q_PROPERTY(QList supportedSampleSizes READ supportedSampleSizes CONSTANT) + +public: + using QAudioDeviceInfo::QAudioDeviceInfo; + AudioDeviceInfo(const QAudioDeviceInfo &other); + AudioDeviceInfo() = default; + + QString description() const; + + static QString description(const QString &deviceName); +}; + +class AudioDeviceModel : public QAbstractListModel +{ + Q_OBJECT + + Q_PROPERTY(AudioDeviceModel::Mode mode READ mode WRITE setMode NOTIFY modeChanged) + Q_PROPERTY(int rowCount READ rowCount NOTIFY audioDevicesChanged) + Q_PROPERTY(QList audioDevices READ audioDevices NOTIFY audioDevicesChanged) + Q_PROPERTY(AudioDeviceInfo defaultAudioDevice READ defaultAudioDevice NOTIFY audioDevicesChanged) + Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged) + Q_PROPERTY(AudioDeviceInfo currentAudioDevice READ currentAudioDevice WRITE setCurrentAudioDevice NOTIFY currentIndexChanged) + +public: + enum Mode { + AudioInput = QAudio::Mode::AudioInput, + AudioOutput = QAudio::Mode::AudioOutput + }; + Q_ENUM(Mode) + + enum CustomRoles { + IsNullRole = Qt::UserRole, + DeviceNameRole, + DescriptionRole, + SupportedCodecsRole, + SupportedSampleRatesRole, + SupportedChannelCountsRole, + SupportedSampleSizesRole, + AudioDeviceInfoRole + }; + + explicit AudioDeviceModel(QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash roleNames() const override; + + AudioDeviceModel::Mode mode() const; + void setMode(AudioDeviceModel::Mode mode); + + QList audioDevices() const; + AudioDeviceInfo defaultAudioDevice() const; + + int currentIndex() const; + void setCurrentIndex(int currentIndex); + + AudioDeviceInfo currentAudioDevice() const; + void setCurrentAudioDevice(const AudioDeviceInfo ¤tAudioDevice); + + Q_INVOKABLE AudioDeviceInfo audioDevice(int row) const; + Q_INVOKABLE int indexOf(const QString &deviceName) const; + + static AudioDeviceInfo defaultAudioInputDevice(); + static AudioDeviceInfo audioInputDevice(const QString &deviceName); + + static AudioDeviceInfo defaultAudioOutputDevice(); + static AudioDeviceInfo audioOutputDevice(const QString &deviceName); + +public slots: + void refresh(); + +signals: + void modeChanged(); + void audioDevicesChanged(); + void currentIndexChanged(); + +private: + AudioDeviceModel::Mode m_mode = AudioDeviceModel::Mode::AudioInput; + QList m_audioDevices; + int m_currentIndex = -1; +}; + +Q_DECLARE_METATYPE(AudioDeviceInfo) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 91f76df..b412109 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,56 +1,61 @@ # set Kaidans sources (used in main cmake file) set(KAIDAN_SOURCES src/main.cpp src/Kaidan.cpp src/ClientWorker.cpp src/AvatarFileStorage.cpp src/Database.cpp src/RosterItem.cpp src/RosterModel.cpp src/RosterFilterProxyModel.cpp src/RosterDb.cpp src/RosterManager.cpp src/RegistrationManager.cpp src/Message.cpp src/MessageModel.cpp src/MessageDb.cpp src/MessageHandler.cpp src/Notifications.cpp src/PresenceCache.cpp src/DiscoveryManager.cpp src/VCardManager.cpp src/VCardModel.cpp src/LogHandler.cpp src/StatusBar.cpp src/UploadManager.cpp src/EmojiModel.cpp src/TransferCache.cpp src/DownloadManager.cpp src/QmlUtils.cpp src/Utils.cpp - src/QrCodeDecoder + src/QrCodeDecoder.cpp src/QrCodeScannerFilter.cpp src/QrCodeVideoFrame.cpp + src/CameraModel.cpp + src/AudioDeviceModel.cpp + src/MediaSettings.cpp + src/CameraImageCapture.cpp src/MediaUtils.cpp + src/MediaRecorder.cpp # needed to trigger moc generation / to be displayed in IDEs src/Enums.h src/Globals.h # kaidan QXmpp extensions (need to be merged into QXmpp upstream) src/qxmpp-exts/QXmppHttpUploadIq.cpp src/qxmpp-exts/QXmppUploadRequestManager.cpp src/qxmpp-exts/QXmppUploadManager.cpp src/qxmpp-exts/QXmppColorGenerator.cpp src/qxmpp-exts/QXmppUri.cpp # hsluv-c required for color generation src/hsluv-c/hsluv.c ) if(NOT ANDROID AND NOT IOS) set(KAIDAN_SOURCES ${KAIDAN_SOURCES} src/singleapp/singleapplication.cpp src/singleapp/singleapplication_p.cpp ) endif() diff --git a/src/qml/elements/MediaPreview.qml b/src/CameraImageCapture.cpp similarity index 62% copy from src/qml/elements/MediaPreview.qml copy to src/CameraImageCapture.cpp index 82c926e..1075841 100644 --- a/src/qml/elements/MediaPreview.qml +++ b/src/CameraImageCapture.cpp @@ -1,62 +1,61 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 Kaidan developers and contributors * (see the LICENSE file for a full list of copyright authors) * * Kaidan 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. * * In addition, as a special exception, the author of Kaidan gives * permission to link the code of its release with the OpenSSL * project's "OpenSSL" library (or with modified versions of it that * use the same license as the "OpenSSL" library), and distribute the * linked executables. You must obey the GNU General Public License in * all respects for all of the code used other than "OpenSSL". If you * modify this file, you may extend this exception to your version of * the file, but you are not obligated to do so. If you do not wish to * do so, delete this exception statement from your version. * * Kaidan 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 Kaidan. If not, see . */ -/** - * This element is used in the @see SendMediaSheet to display information about a selected file to - * the user. It shows the file name, file size and a little file icon. - */ - -import QtQuick 2.6 -import QtQuick.Layouts 1.3 -import QtQuick.Controls 2.0 as Controls -import org.kde.kirigami 2.0 as Kirigami +#include "CameraImageCapture.h" -import im.kaidan.kaidan 1.0 +CameraImageCapture::CameraImageCapture(QMediaObject *mediaObject, QObject *parent) + : QCameraImageCapture(mediaObject, parent) +{ + connect(this, &QCameraImageCapture::imageSaved, + this, [this](int id, const QString &filePath) { + Q_UNUSED(id); + m_actualLocation = QUrl::fromLocalFile(filePath); + emit actualLocationChanged(m_actualLocation); + }); +} -Rectangle { - id: root +QUrl CameraImageCapture::actualLocation() const +{ + return m_actualLocation; +} - property url mediaSource - property int mediaSourceType: Enums.MessageType.MessageUnknown - property bool showOpenButton: false - property Item message: null - property int messageSize: Kirigami.Units.gridUnit * 14 +bool CameraImageCapture::setMediaObject(QMediaObject *mediaObject) +{ + const QMultimedia::AvailabilityStatus previousAvailability = availability(); + const bool result = QCameraImageCapture::setMediaObject(mediaObject); - color: message ? 'transparent' : Kirigami.Theme.backgroundColor + if (previousAvailability != availability()) { + QMetaObject::invokeMethod(this, [this]() { + emit availabilityChanged(availability()); + }, Qt::QueuedConnection); + } - Layout.fillHeight: false - Layout.fillWidth: message ? false : true - Layout.alignment: Qt.AlignCenter - Layout.margins: 0 - Layout.leftMargin: undefined - Layout.topMargin: undefined - Layout.rightMargin: undefined - Layout.bottomMargin: undefined + return result; } diff --git a/src/qml/elements/MediaPreview.qml b/src/CameraImageCapture.h similarity index 62% copy from src/qml/elements/MediaPreview.qml copy to src/CameraImageCapture.h index 82c926e..c94631d 100644 --- a/src/qml/elements/MediaPreview.qml +++ b/src/CameraImageCapture.h @@ -1,62 +1,60 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 Kaidan developers and contributors * (see the LICENSE file for a full list of copyright authors) * * Kaidan 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. * * In addition, as a special exception, the author of Kaidan gives * permission to link the code of its release with the OpenSSL * project's "OpenSSL" library (or with modified versions of it that * use the same license as the "OpenSSL" library), and distribute the * linked executables. You must obey the GNU General Public License in * all respects for all of the code used other than "OpenSSL". If you * modify this file, you may extend this exception to your version of * the file, but you are not obligated to do so. If you do not wish to * do so, delete this exception statement from your version. * * Kaidan 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 Kaidan. If not, see . */ -/** - * This element is used in the @see SendMediaSheet to display information about a selected file to - * the user. It shows the file name, file size and a little file icon. - */ +#pragma once + +#include +#include + +// A QCameraImageCapture that mimic api of QMediaRecorder + +class CameraImageCapture : public QCameraImageCapture +{ + Q_OBJECT + + Q_PROPERTY(QUrl actualLocation READ actualLocation NOTIFY actualLocationChanged) + Q_PROPERTY(QMultimedia::AvailabilityStatus availability READ availability NOTIFY availabilityChanged) + Q_PROPERTY(bool isAvailable READ isAvailable NOTIFY availabilityChanged) + +public: + CameraImageCapture(QMediaObject *mediaObject, QObject *parent = nullptr); + + QUrl actualLocation() const; + +signals: + void availabilityChanged(QMultimedia::AvailabilityStatus availability); + void actualLocationChanged(const QUrl &location); + +protected: + bool setMediaObject(QMediaObject *mediaObject) override; -import QtQuick 2.6 -import QtQuick.Layouts 1.3 -import QtQuick.Controls 2.0 as Controls -import org.kde.kirigami 2.0 as Kirigami - -import im.kaidan.kaidan 1.0 - -Rectangle { - id: root - - property url mediaSource - property int mediaSourceType: Enums.MessageType.MessageUnknown - property bool showOpenButton: false - property Item message: null - property int messageSize: Kirigami.Units.gridUnit * 14 - - color: message ? 'transparent' : Kirigami.Theme.backgroundColor - - Layout.fillHeight: false - Layout.fillWidth: message ? false : true - Layout.alignment: Qt.AlignCenter - Layout.margins: 0 - Layout.leftMargin: undefined - Layout.topMargin: undefined - Layout.rightMargin: undefined - Layout.bottomMargin: undefined -} +private: + QUrl m_actualLocation; +}; diff --git a/src/CameraModel.cpp b/src/CameraModel.cpp new file mode 100644 index 0000000..7528477 --- /dev/null +++ b/src/CameraModel.cpp @@ -0,0 +1,182 @@ +/* + * Kaidan - A user-friendly XMPP client for every device! + * + * Copyright (C) 2016-2019 Kaidan developers and contributors + * (see the LICENSE file for a full list of copyright authors) + * + * Kaidan 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. + * + * In addition, as a special exception, the author of Kaidan gives + * permission to link the code of its release with the OpenSSL + * project's "OpenSSL" library (or with modified versions of it that + * use the same license as the "OpenSSL" library), and distribute the + * linked executables. You must obey the GNU General Public License in + * all respects for all of the code used other than "OpenSSL". If you + * modify this file, you may extend this exception to your version of + * the file, but you are not obligated to do so. If you do not wish to + * do so, delete this exception statement from your version. + * + * Kaidan 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 Kaidan. If not, see . + */ + +#include "CameraModel.h" + +CameraInfo::CameraInfo(const QString &deviceName) + : QCameraInfo(deviceName.toLocal8Bit()) +{ +} + +CameraInfo::CameraInfo(const QCameraInfo &other) + : QCameraInfo(other) +{ +} + +CameraModel::CameraModel(QObject *parent) +: QAbstractListModel(parent) +{ + refresh(); +} + +int CameraModel::rowCount(const QModelIndex &parent) const +{ + return parent == QModelIndex() ? m_cameras.count() : 0; +} + +QVariant CameraModel::data(const QModelIndex &index, int role) const +{ + if (hasIndex(index.row(), index.column(), index.parent())) { + const auto &cameraInfo(m_cameras[index.row()]); + + switch (role) { + case CameraModel::CustomRoles::IsNullRole: + return cameraInfo.isNull(); + case CameraModel::CustomRoles::DeviceNameRole: + return cameraInfo.deviceName(); + case CameraModel::CustomRoles::DescriptionRole: + return cameraInfo.description(); + case CameraModel::CustomRoles::PositionRole: + return cameraInfo.position(); + case CameraModel::CustomRoles::OrientationRole: + return cameraInfo.orientation(); + case CameraModel::CustomRoles::CameraInfoRole: + return QVariant::fromValue(CameraInfo(cameraInfo)); + } + } + + return QVariant(); +} + +QHash CameraModel::roleNames() const +{ + static const QHash roles { + { IsNullRole, QByteArrayLiteral("isNull") }, + { DeviceNameRole, QByteArrayLiteral("deviceName") }, + { DescriptionRole, QByteArrayLiteral("description") }, + { PositionRole, QByteArrayLiteral("position") }, + { OrientationRole, QByteArrayLiteral("orientation") }, + { CameraInfoRole, QByteArrayLiteral("cameraInfo") } + }; + + return roles; +} + +QList CameraModel::cameras() const +{ + return m_cameras; +} + +CameraInfo CameraModel::defaultCamera() +{ + return CameraInfo(QCameraInfo::defaultCamera()); +} + +int CameraModel::currentIndex() const +{ + return m_currentIndex; +} + +void CameraModel::setCurrentIndex(int currentIndex) +{ + if (currentIndex < 0 || currentIndex >= m_cameras.count() + || m_currentIndex == currentIndex) { + return; + } + + m_currentIndex = currentIndex; + emit currentIndexChanged(); +} + +CameraInfo CameraModel::currentCamera() const +{ + return m_currentIndex >= 0 && m_currentIndex < m_cameras.count() + ? CameraInfo(m_cameras[m_currentIndex]) + : CameraInfo(); +} + +void CameraModel::setCurrentCamera(const CameraInfo ¤tCamera) +{ + setCurrentIndex(m_cameras.indexOf(currentCamera)); +} + +CameraInfo CameraModel::camera(int row) const +{ + return hasIndex(row, 0) + ? CameraInfo(m_cameras[row]) + : CameraInfo(); +} + +int CameraModel::indexOf(const QString &deviceName) const +{ + for (int i = 0; i < m_cameras.count(); ++i) { + const auto &camera(m_cameras[i]); + + if (camera.deviceName() == deviceName) { + return i; + } + } + + return -1; +} + +CameraInfo CameraModel::camera(const QString &deviceName) +{ + return CameraInfo(deviceName); +} + +CameraInfo CameraModel::camera(QCamera::Position position) +{ + const auto cameras = QCameraInfo::availableCameras(position); + return cameras.isEmpty() ? CameraInfo() : CameraInfo(cameras.first()); +} + +void CameraModel::refresh() +{ + const auto cameras = QCameraInfo::availableCameras(); + + if (m_cameras == cameras) { + return; + } + + beginResetModel(); + const QString currentDeviceName = currentCamera().deviceName(); + const auto it = std::find_if(m_cameras.constBegin(), m_cameras.constEnd(), + [¤tDeviceName](const QCameraInfo &deviceInfo) { + return deviceInfo.deviceName() == currentDeviceName; + }); + + m_cameras = cameras; + m_currentIndex = it == m_cameras.constEnd() ? -1 : it - m_cameras.constBegin(); + endResetModel(); + + emit camerasChanged(); + emit currentIndexChanged(); +} diff --git a/src/CameraModel.h b/src/CameraModel.h new file mode 100644 index 0000000..93307b8 --- /dev/null +++ b/src/CameraModel.h @@ -0,0 +1,105 @@ +/* + * Kaidan - A user-friendly XMPP client for every device! + * + * Copyright (C) 2016-2019 Kaidan developers and contributors + * (see the LICENSE file for a full list of copyright authors) + * + * Kaidan 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. + * + * In addition, as a special exception, the author of Kaidan gives + * permission to link the code of its release with the OpenSSL + * project's "OpenSSL" library (or with modified versions of it that + * use the same license as the "OpenSSL" library), and distribute the + * linked executables. You must obey the GNU General Public License in + * all respects for all of the code used other than "OpenSSL". If you + * modify this file, you may extend this exception to your version of + * the file, but you are not obligated to do so. If you do not wish to + * do so, delete this exception statement from your version. + * + * Kaidan 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 Kaidan. If not, see . + */ + +#pragma once + +#include +#include + +class CameraInfo : public QCameraInfo { + Q_GADGET + + Q_PROPERTY(bool isNull READ isNull CONSTANT) + Q_PROPERTY(QString deviceName READ deviceName CONSTANT) + Q_PROPERTY(QString description READ description CONSTANT) + Q_PROPERTY(QCamera::Position position READ position CONSTANT) + Q_PROPERTY(int orientation READ orientation CONSTANT) + +public: + using QCameraInfo::QCameraInfo; + explicit CameraInfo(const QString &deviceName); + CameraInfo() = default; + CameraInfo(const QCameraInfo &other); +}; + +class CameraModel : public QAbstractListModel +{ + Q_OBJECT + + Q_PROPERTY(int rowCount READ rowCount NOTIFY camerasChanged) + Q_PROPERTY(QList cameras READ cameras NOTIFY camerasChanged) + Q_PROPERTY(CameraInfo defaultCamera READ defaultCamera NOTIFY camerasChanged) + Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged) + Q_PROPERTY(CameraInfo currentCamera READ currentCamera WRITE setCurrentCamera NOTIFY currentIndexChanged) + +public: + enum CustomRoles { + IsNullRole = Qt::UserRole, + DeviceNameRole, + DescriptionRole, + PositionRole, + OrientationRole, + CameraInfoRole + }; + + explicit CameraModel(QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash roleNames() const override; + + QList cameras() const; + static CameraInfo defaultCamera(); + + int currentIndex() const; + void setCurrentIndex(int currentIndex); + + CameraInfo currentCamera() const; + void setCurrentCamera(const CameraInfo ¤tCamera); + + Q_INVOKABLE CameraInfo camera(int row) const; + Q_INVOKABLE int indexOf(const QString &deviceName) const; + + static CameraInfo camera(const QString &deviceName); + static CameraInfo camera(QCamera::Position position); + +public slots: + void refresh(); + +signals: + void camerasChanged(); + void currentIndexChanged(); + +private: + QList m_cameras; + int m_currentIndex = -1; +}; + +Q_DECLARE_METATYPE(CameraInfo) diff --git a/src/Enums.h b/src/Enums.h index 5a6cd5d..deece28 100644 --- a/src/Enums.h +++ b/src/Enums.h @@ -1,110 +1,136 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 Kaidan developers and contributors * (see the LICENSE file for a full list of copyright authors) * * Kaidan 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. * * In addition, as a special exception, the author of Kaidan gives * permission to link the code of its release with the OpenSSL * project's "OpenSSL" library (or with modified versions of it that * use the same license as the "OpenSSL" library), and distribute the * linked executables. You must obey the GNU General Public License in * all respects for all of the code used other than "OpenSSL". If you * modify this file, you may extend this exception to your version of * the file, but you are not obligated to do so. If you do not wish to * do so, delete this exception statement from your version. * * Kaidan 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 Kaidan. If not, see . */ #ifndef ENUMS_H #define ENUMS_H #include #include +#include #include +#define ENABLE_IF(...) typename std::enable_if<__VA_ARGS__>::type* = nullptr + +template struct make_void { typedef void type; }; +template using void_t = typename make_void::type; + +// primary template handles types that have no nested ::enum_type member, like standard enum +template > +struct has_enum_type : std::false_type { }; + +// specialization recognizes types that do have a nested ::enum_type member, like QFlags enum +template +struct has_enum_type> : std::true_type { }; + namespace Enums { Q_NAMESPACE /** * Enumeration of possible connection states. */ enum class ConnectionState { StateDisconnected = QXmppClient::DisconnectedState, StateConnecting = QXmppClient::ConnectingState, StateConnected = QXmppClient::ConnectedState }; Q_ENUM_NS(ConnectionState) /** * Enumeration of possible disconnection reasons */ enum class DisconnectionReason { ConnNoError, ConnUserDisconnected, ConnAuthenticationFailed, ConnNotConnected, ConnTlsFailed, ConnTlsNotAvailable, ConnDnsError, ConnConnectionRefused, ConnNoSupportedAuth, ConnKeepAliveError, ConnNoNetworkPermission }; Q_ENUM_NS(DisconnectionReason) // Alias, so that qDebug outputs the full name, but it can be // abrieviated in the code using DisconnReason = DisconnectionReason; /** * Enumeration of different media/message types */ enum class MessageType { MessageUnknown = -1, MessageText, MessageFile, MessageImage, MessageVideo, MessageAudio, MessageDocument, MessageGeoLocation }; Q_ENUM_NS(MessageType) /** * Enumeration of contact availability states */ enum class AvailabilityTypes { PresError, PresUnavailable, PresOnline, PresAway, PresXA, PresDND, PresChat, PresInvisible }; Q_ENUM_NS(AvailabilityTypes) + + template ::value && std::is_enum::value)> + QString toString(const T flag) { + static const QMetaEnum e = QMetaEnum::fromType(); + return QString::fromLatin1(e.valueToKey(static_cast(flag))); + } + + template ::value)> + QString toString(const T flags) { + static const QMetaEnum e = QMetaEnum::fromType(); + return QString::fromLatin1(e.valueToKeys(static_cast(flags))); + } } // Needed workaround to trigger older CMake auto moc versions to generate moc // sources for this file (it only contains Q_NAMESPACE, which is new). #if 0 Q_OBJECT #endif #endif // ENUMS_H diff --git a/src/MediaRecorder.cpp b/src/MediaRecorder.cpp new file mode 100644 index 0000000..7e8385d --- /dev/null +++ b/src/MediaRecorder.cpp @@ -0,0 +1,1233 @@ +/* + * Kaidan - A user-friendly XMPP client for every device! + * + * Copyright (C) 2016-2019 Kaidan developers and contributors + * (see the LICENSE file for a full list of copyright authors) + * + * Kaidan 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. + * + * In addition, as a special exception, the author of Kaidan gives + * permission to link the code of its release with the OpenSSL + * project's "OpenSSL" library (or with modified versions of it that + * use the same license as the "OpenSSL" library), and distribute the + * linked executables. You must obey the GNU General Public License in + * all respects for all of the code used other than "OpenSSL". If you + * modify this file, you may extend this exception to your version of + * the file, but you are not obligated to do so. If you do not wish to + * do so, delete this exception statement from your version. + * + * Kaidan 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 Kaidan. If not, see . + */ + +#include "MediaRecorder.h" +#include "Kaidan.h" + +#include +#include + +/* + * NOTES: Codecs and containers supported list are available as soon as the object is created. + * Resolutions, frame rates etc are populated once the objects become *ready*. + */ + +#define ENABLE_DEBUG false + +#define SETTING_USER_DEFAULT QStringLiteral("User Default") +#define SETTING_DEFAULT_CAMERA_DEVICE_NAME QStringLiteral("Camera Device Name") +#define SETTING_DEFAULT_AUDIO_INPUT_DEVICE_NAME QStringLiteral("Audio Input Device Name") + +static void connectCamera(QCamera *camera, MediaRecorder *receiver) { + QObject::connect(camera, &QCamera::statusChanged, receiver, &MediaRecorder::readyChanged); +} + +static void connectImageCapturer(CameraImageCapture *capturer, MediaRecorder *receiver) { + QObject::connect(capturer, &CameraImageCapture::availabilityChanged, receiver, &MediaRecorder::availabilityStatusChanged); + QObject::connect(capturer, QOverload::of(&CameraImageCapture::error), receiver, &MediaRecorder::errorChanged); + QObject::connect(capturer, &CameraImageCapture::actualLocationChanged, receiver, &MediaRecorder::actualLocationChanged); + QObject::connect(capturer, &CameraImageCapture::readyForCaptureChanged, receiver, &MediaRecorder::readyChanged); +} + +template +static void connectMediaRecorder(T *recorder, MediaRecorder *receiver) { + QObject::connect(recorder, QOverload::of(&T::availabilityChanged), receiver, &MediaRecorder::availabilityStatusChanged); + QObject::connect(recorder, &T::stateChanged, receiver, &MediaRecorder::stateChanged); + QObject::connect(recorder, &T::statusChanged, receiver, &MediaRecorder::statusChanged); + QObject::connect(recorder, QOverload::of(&T::error), receiver, &MediaRecorder::errorChanged); + QObject::connect(recorder, &T::actualLocationChanged, receiver, &MediaRecorder::actualLocationChanged); + QObject::connect(recorder, &T::durationChanged, receiver, &MediaRecorder::durationChanged); + QObject::connect(recorder, &T::mutedChanged, receiver, &MediaRecorder::mutedChanged); + QObject::connect(recorder, &T::volumeChanged, receiver, &MediaRecorder::volumeChanged); + QObject::connect(recorder, QOverload::of(&T::availabilityChanged), receiver, &MediaRecorder::readyChanged); + QObject::connect(recorder, &T::statusChanged, receiver, &MediaRecorder::readyChanged); +} + +template <> +void connectMediaRecorder(QAudioRecorder *recorder, MediaRecorder *receiver) { + connectMediaRecorder(qobject_cast(recorder), receiver); +} + +template +static QStringList buildCodecList(const QStringList &codecs, F toString, + MediaRecorder *recorder, const QString &startsWith = QString()) { + QStringList entries(codecs); + + if (!startsWith.isEmpty()) { + entries.erase(std::remove_if(entries.begin(), entries.end(), [&startsWith](const QString &entry) { + return !entry.startsWith(startsWith, Qt::CaseInsensitive); + }), entries.end()); + } + + std::sort(entries.begin(), entries.end(), [&toString, recorder](const QString &left, const QString &right) { + return toString(left, recorder).compare(toString(right, recorder), Qt::CaseInsensitive) < 0; + }); + + entries.prepend(QString()); + + return entries; +} + +static QList buildResolutionList(const QList &resolutions) { + QList entries(resolutions); + + std::sort(entries.begin(), entries.end(), [](const QSize &left, const QSize &right) { + return left.width() == right.width() + ? left.height() < right.height() + : left.width() < right.width(); + }); + + entries.prepend(QSize()); + + return entries; +} + +static QList buildQualityList() { + const QList entries { + CommonEncoderSettings::EncodingQuality::VeryLowQuality, + CommonEncoderSettings::EncodingQuality::LowQuality, + CommonEncoderSettings::EncodingQuality::NormalQuality, + CommonEncoderSettings::EncodingQuality::HighQuality, + CommonEncoderSettings::EncodingQuality::VeryHighQuality + }; + + return entries; +} + +static QList buildSampleRateList(const QList &sampleRates) { + QList entries(sampleRates); + + if (entries.isEmpty()) { + entries = { + 8000, + 16000, + 22050, + 32000, + 37800, + 44100, + 48000, + 96000, + 192000 + }; + } + + std::sort(entries.begin(), entries.end(), [](const int left, const int right) { + return left < right; + }); + + entries.prepend(-1); + + return entries; +} + +static QList buildFrameRateList(const QList &frameRates) { + QList entries(frameRates); + + std::sort(entries.begin(), entries.end(), [](const qreal left, const qreal right) { + return left < right; + }); + + entries.prepend(0.0); + + return entries; +} + +MediaRecorder::MediaRecorder(QObject *parent) + : QObject(parent) + , m_cameraModel(new CameraModel(this)) + , m_audioDeviceModel(new AudioDeviceModel(this)) + , m_containerModel(new MediaSettingsContainerModel(this, this)) + , m_imageCodecModel(new MediaSettingsImageCodecModel(this, this)) + , m_imageResolutionModel(new MediaSettingsResolutionModel(this, this)) + , m_imageQualityModel(new MediaSettingsQualityModel(this, this)) + , m_audioCodecModel(new MediaSettingsAudioCodecModel(this, this)) + , m_audioSampleRateModel(new MediaSettingsAudioSampleRateModel(this, this)) + , m_audioQualityModel(new MediaSettingsQualityModel(this, this)) + , m_videoCodecModel(new MediaSettingsVideoCodecModel(this, this)) + , m_videoResolutionModel(new MediaSettingsResolutionModel(this, this)) + , m_videoFrameRateModel(new MediaSettingsVideoFrameRateModel(this, this)) + , m_videoQualityModel(new MediaSettingsQualityModel(this, this)) +{ + connect(this, &MediaRecorder::readyChanged, this, [this]() { + if (!isReady()) { + if (m_type == MediaRecorder::Type::Invalid) { + m_cameraModel->setCurrentIndex(-1); + m_audioDeviceModel->setCurrentIndex(-1); + m_containerModel->clear(); + + m_imageCodecModel->clear(); + m_imageResolutionModel->clear(); + m_imageQualityModel->clear(); + + m_audioCodecModel->clear(); + m_audioSampleRateModel->clear(); + m_audioQualityModel->clear(); + + m_videoCodecModel->clear(); + m_videoResolutionModel->clear(); + m_videoFrameRateModel->clear(); + m_videoQualityModel->clear(); + } + + return; + } + +#if ENABLE_DEBUG + qDebug("syncProperties Begin"); +#endif + + switch (m_type) { + case MediaRecorder::Type::Invalid: { + Q_UNREACHABLE(); + break; + } + case MediaRecorder::Type::Image: { + m_cameraModel->setCurrentCamera(m_mediaSettings.camera); + m_imageCodecModel->setValuesAndCurrentValue(buildCodecList(m_imageCapturer->supportedImageCodecs(), imageEncoderCodec, this), + m_imageEncoderSettings.codec); + m_imageResolutionModel->setValuesAndCurrentValue(buildResolutionList(m_imageCapturer->supportedResolutions()), + m_imageEncoderSettings.resolution); + m_imageQualityModel->setValuesAndCurrentValue(buildQualityList(), + m_imageEncoderSettings.quality); + break; + } + case MediaRecorder::Type::Audio: { + m_audioDeviceModel->setCurrentAudioDevice(m_mediaSettings.audioInputDevice); + m_containerModel->setValuesAndCurrentValue(buildCodecList(m_audioRecorder->supportedContainers(), encoderContainer, this, QStringLiteral("audio")), + m_mediaSettings.container); + m_audioCodecModel->setValuesAndCurrentValue(buildCodecList(m_audioRecorder->supportedAudioCodecs(), audioEncoderCodec, this), + m_audioEncoderSettings.codec); + m_audioSampleRateModel->setValuesAndCurrentValue(buildSampleRateList(m_audioRecorder->supportedAudioSampleRates()), + m_audioEncoderSettings.sampleRate); + m_audioQualityModel->setValuesAndCurrentValue(buildQualityList(), + m_audioEncoderSettings.quality); + break; + } + case MediaRecorder::Type::Video: { + m_cameraModel->setCurrentCamera(m_mediaSettings.camera); + m_containerModel->setValuesAndCurrentValue(buildCodecList(m_videoRecorder->supportedContainers(), encoderContainer, this, QStringLiteral("video")), + m_mediaSettings.container); + m_audioCodecModel->setValuesAndCurrentValue(buildCodecList(m_videoRecorder->supportedAudioCodecs(), audioEncoderCodec, this), + m_audioEncoderSettings.codec); + m_audioSampleRateModel->setValuesAndCurrentValue(buildSampleRateList(m_videoRecorder->supportedAudioSampleRates()), + m_audioEncoderSettings.sampleRate); + m_audioQualityModel->setValuesAndCurrentValue(buildQualityList(), + m_audioEncoderSettings.quality); + m_videoCodecModel->setValuesAndCurrentValue(buildCodecList(m_videoRecorder->supportedVideoCodecs(), videoEncoderCodec, this), + m_videoEncoderSettings.codec); + m_videoResolutionModel->setValuesAndCurrentValue(buildResolutionList(m_videoRecorder->supportedResolutions()), + m_videoEncoderSettings.resolution); + m_videoFrameRateModel->setValuesAndCurrentValue(buildFrameRateList(m_videoRecorder->supportedFrameRates()), + m_videoEncoderSettings.frameRate); + m_videoQualityModel->setValuesAndCurrentValue(buildQualityList(), + m_videoEncoderSettings.quality); + break; + } + } + +#if ENABLE_DEBUG + qDebug("syncProperties End"); +#endif + }); +} + +MediaRecorder::~MediaRecorder() +{ + if (isAvailable() && !isReady()) { + cancel(); + } +} + +MediaRecorder::Type MediaRecorder::type() const +{ + return m_type; +} + +void MediaRecorder::setType(MediaRecorder::Type type) +{ + if (m_type == type) { + return; + } + + setupRecorder(type); +} + +QMediaObject *MediaRecorder::mediaObject() const +{ + switch (m_type) { + case MediaRecorder::Type::Image: + case MediaRecorder::Type::Video: + return m_camera; + case MediaRecorder::Type::Invalid: + case MediaRecorder::Type::Audio: + break; + } + + return nullptr; +} + +QString MediaRecorder::currentSettingsKey() const +{ + switch (m_type) { + case MediaRecorder::Type::Invalid: + break; + case MediaRecorder::Type::Image: + Q_ASSERT(!m_mediaSettings.camera.isNull()); + return settingsKey(m_type, m_mediaSettings.camera.deviceName()); + case MediaRecorder::Type::Audio: + Q_ASSERT(!m_mediaSettings.audioInputDevice.isNull()); + return settingsKey(m_type, m_mediaSettings.audioInputDevice.deviceName()); + case MediaRecorder::Type::Video: + Q_ASSERT(!m_mediaSettings.camera.isNull()); + return settingsKey(m_type, m_mediaSettings.camera.deviceName()); + } + + return { }; +} + +MediaRecorder::AvailabilityStatus MediaRecorder::availabilityStatus() const +{ + switch (m_type) { + case MediaRecorder::Type::Invalid: + break; + case MediaRecorder::Type::Image: + return static_cast(m_imageCapturer->availability()); + case MediaRecorder::Type::Audio: + return static_cast(m_audioRecorder->availability()); + case MediaRecorder::Type::Video: + return static_cast(m_videoRecorder->availability()); + } + + return MediaRecorder::AvailabilityStatus::ServiceMissing; +} + +MediaRecorder::State MediaRecorder::state() const +{ + switch (m_type) { + case MediaRecorder::Type::Invalid: + case MediaRecorder::Type::Image: + break; + case MediaRecorder::Type::Audio: + return static_cast(m_audioRecorder->state()); + case MediaRecorder::Type::Video: + return static_cast(m_videoRecorder->state()); + } + + return MediaRecorder::State::StoppedState; +} + +MediaRecorder::Status MediaRecorder::status() const +{ + switch (m_type) { + case MediaRecorder::Type::Invalid: + case MediaRecorder::Type::Image: + break; + case MediaRecorder::Type::Audio: + return static_cast(m_audioRecorder->status()); + case MediaRecorder::Type::Video: + return static_cast(m_videoRecorder->status()); + } + + return MediaRecorder::Status::UnavailableStatus; +} + +bool MediaRecorder::isAvailable() const +{ + switch (m_type) { + case MediaRecorder::Type::Invalid: + break; + case MediaRecorder::Type::Image: + return m_imageCapturer->isAvailable(); + case MediaRecorder::Type::Audio: + return m_audioRecorder->isAvailable(); + case MediaRecorder::Type::Video: + return m_videoRecorder->isAvailable(); + } + + return false; +} + +bool MediaRecorder::isReady() const +{ + switch (m_type) { + case MediaRecorder::Type::Invalid: + break; + case MediaRecorder::Type::Image: + return m_imageCapturer->isReadyForCapture(); + case MediaRecorder::Type::Audio: + return isAvailable() + && status() >= MediaRecorder::Status::UnloadedStatus + && status() <= MediaRecorder::Status::LoadedStatus; + case MediaRecorder::Type::Video: + return isAvailable() + && m_camera->status() == QCamera::Status::ActiveStatus + && status() >= MediaRecorder::Status::UnloadedStatus + && status() <= MediaRecorder::Status::LoadedStatus; + } + + return false; +} + +MediaRecorder::Error MediaRecorder::error() const +{ + static const QMap capturerMapping = { + { QCameraImageCapture::Error::NoError, MediaRecorder::Error::NoError }, + { QCameraImageCapture::Error::NotReadyError, MediaRecorder::Error::NotReadyError }, + { QCameraImageCapture::Error::ResourceError, MediaRecorder::Error::ResourceError }, + { QCameraImageCapture::Error::OutOfSpaceError, MediaRecorder::Error::OutOfSpaceError }, + { QCameraImageCapture::Error::NotSupportedFeatureError, MediaRecorder::Error::NotSupportedFeatureError }, + { QCameraImageCapture::Error::FormatError, MediaRecorder::Error::FormatError } + }; + static const QMap recorderMapping = { + { QMediaRecorder::Error::NoError, MediaRecorder::Error::NoError }, + { QMediaRecorder::Error::ResourceError, MediaRecorder::Error::ResourceError }, + { QMediaRecorder::Error::FormatError, MediaRecorder::Error::FormatError }, + { QMediaRecorder::Error::OutOfSpaceError, MediaRecorder::Error::OutOfSpaceError } + }; + + switch (m_type) { + case MediaRecorder::Type::Invalid: + break; + case MediaRecorder::Type::Image: { + const auto it = capturerMapping.constFind(m_imageCapturer->error()); + Q_ASSERT(it != capturerMapping.constEnd()); + return it.value(); + } + case MediaRecorder::Type::Audio: { + const auto it = recorderMapping.constFind(m_audioRecorder->error()); + Q_ASSERT(it != recorderMapping.constEnd()); + return it.value(); + } + case MediaRecorder::Type::Video: { + const auto it = recorderMapping.constFind(m_videoRecorder->error()); + Q_ASSERT(it != recorderMapping.constEnd()); + return it.value(); + } + } + + return MediaRecorder::Error::NoError; +} + +QString MediaRecorder::errorString() const +{ + switch (m_type) { + case MediaRecorder::Type::Invalid: + break; + case MediaRecorder::Type::Image: + return m_imageCapturer->errorString(); + case MediaRecorder::Type::Audio: + return m_audioRecorder->errorString(); + case MediaRecorder::Type::Video: + return m_videoRecorder->errorString(); + } + + return { }; +} + +QUrl MediaRecorder::actualLocation() const +{ + static const auto urlExists = [](const QUrl &url) { + if (url.isEmpty() || (!url.scheme().isEmpty() && !url.isLocalFile())) { + return url; + } + + const QUrl u(url.isLocalFile() ? url : QUrl::fromLocalFile(url.toString())); + return QFile::exists(u.toLocalFile()) ? u : QUrl(); + }; + + switch (m_type) { + case MediaRecorder::Type::Invalid: + break; + case MediaRecorder::Type::Image: + return urlExists(m_imageCapturer->actualLocation()); + case MediaRecorder::Type::Audio: + return urlExists(m_audioRecorder->actualLocation()); + case MediaRecorder::Type::Video: + return urlExists(m_videoRecorder->actualLocation()); + } + + return { }; +} + +qint64 MediaRecorder::duration() const +{ + switch (m_type) { + case MediaRecorder::Type::Invalid: + case MediaRecorder::Type::Image: + break; + case MediaRecorder::Type::Audio: + return m_audioRecorder->duration(); + case MediaRecorder::Type::Video: + return m_videoRecorder->duration(); + } + + return 0; +} + +bool MediaRecorder::isMuted() const +{ + switch (m_type) { + case MediaRecorder::Type::Invalid: + case MediaRecorder::Type::Image: + break; + case MediaRecorder::Type::Audio: + return m_audioRecorder->isMuted(); + case MediaRecorder::Type::Video: + return m_videoRecorder->isMuted(); + } + + return false; +} + +void MediaRecorder::setMuted(bool muted) +{ + switch (m_type) { + case MediaRecorder::Type::Invalid: + case MediaRecorder::Type::Image: + Q_UNREACHABLE(); + break; + case MediaRecorder::Type::Audio: + m_audioRecorder->setMuted(muted); + break; + case MediaRecorder::Type::Video: + m_videoRecorder->setMuted(muted); + break; + } +} + +qreal MediaRecorder::volume() const +{ + switch (m_type) { + case MediaRecorder::Type::Invalid: + case MediaRecorder::Type::Image: + break; + case MediaRecorder::Type::Audio: + return m_audioRecorder->volume(); + case MediaRecorder::Type::Video: + return m_videoRecorder->volume(); + } + + return false; +} + +void MediaRecorder::setVolume(qreal volume) +{ + switch (m_type) { + case MediaRecorder::Type::Invalid: + case MediaRecorder::Type::Image: + Q_UNREACHABLE(); + break; + case MediaRecorder::Type::Audio: + m_audioRecorder->setVolume(volume); + break; + case MediaRecorder::Type::Video: + m_videoRecorder->setVolume(volume); + break; + } +} + +MediaSettings MediaRecorder::mediaSettings() const +{ + return m_mediaSettings; +} + +void MediaRecorder::setMediaSettings(const MediaSettings &settings) +{ +#if ENABLE_DEBUG + qDebug(Q_FUNC_INFO); +#endif + + if (settings == m_mediaSettings) { + return; + } + + const auto oldSettings = m_mediaSettings; + +#if ENABLE_DEBUG + settings.dumpProperties(QStringLiteral("New")); + oldSettings.dumpProperties(QStringLiteral("Old")); +#endif + + m_mediaSettings = settings; + + if (!m_initializing) { + switch (m_type) { + case MediaRecorder::Type::Invalid: + Q_UNREACHABLE(); + break; + case MediaRecorder::Type::Image: + if (oldSettings.camera != settings.camera) { + setupRecorder(m_type); + } + + break; + case MediaRecorder::Type::Audio: + if (oldSettings.audioInputDevice != settings.audioInputDevice) { + setupRecorder(m_type); + } else { + m_audioRecorder->setContainerFormat(m_mediaSettings.container); + } + + break; + case MediaRecorder::Type::Video: + if (oldSettings.camera != settings.camera) { + setupRecorder(m_type); + } else { + m_videoRecorder->setContainerFormat(m_mediaSettings.container); + } + + break; + } + + emit mediaSettingsChanged(); + } +} + +ImageEncoderSettings MediaRecorder::imageEncoderSettings() const +{ + return m_imageEncoderSettings; +} + +void MediaRecorder::setImageEncoderSettings(const ImageEncoderSettings &settings) +{ +#if ENABLE_DEBUG + qDebug(Q_FUNC_INFO); +#endif + + switch (m_type) { + case MediaRecorder::Type::Image: { + if (settings != m_imageEncoderSettings) { +#if ENABLE_DEBUG + settings.dumpProperties(QStringLiteral("New")); + m_imageEncoderSettings.dumpProperties(QStringLiteral("Old")); +#endif + + m_imageEncoderSettings = settings; + + if (!m_initializing) { + m_imageCapturer->setEncodingSettings(m_imageEncoderSettings.toQImageEncoderSettings()); + emit imageEncoderSettingsChanged(); + } + } + + break; + } + case MediaRecorder::Type::Invalid: + case MediaRecorder::Type::Audio: + case MediaRecorder::Type::Video: + Q_UNREACHABLE(); + break; + } +} + +AudioEncoderSettings MediaRecorder::audioEncoderSettings() const +{ + return m_audioEncoderSettings; +} + +void MediaRecorder::setAudioEncoderSettings(const AudioEncoderSettings &settings) +{ +#if ENABLE_DEBUG + qDebug(Q_FUNC_INFO); +#endif + + switch (m_type) { + case MediaRecorder::Type::Audio: + case MediaRecorder::Type::Video: { + if (settings != m_audioEncoderSettings) { +#if ENABLE_DEBUG + settings.dumpProperties(QStringLiteral("New")); + m_audioEncoderSettings.dumpProperties(QStringLiteral("Old")); +#endif + + m_audioEncoderSettings = settings; + + if (!m_initializing) { + if (m_audioRecorder) { + m_audioRecorder->setAudioSettings(m_audioEncoderSettings.toQAudioEncoderSettings()); + } else if (m_videoRecorder) { + m_videoRecorder->setAudioSettings(m_audioEncoderSettings.toQAudioEncoderSettings()); + } + + emit audioEncoderSettingsChanged(); + + // Changing audio settings does not trigger ready changed. + if (m_type == MediaRecorder::Type::Audio) { + emit readyChanged(); + } + } + } + + break; + } + case MediaRecorder::Type::Image: + case MediaRecorder::Type::Invalid: + Q_UNREACHABLE(); + break; + } +} + +VideoEncoderSettings MediaRecorder::videoEncoderSettings() const +{ + return m_videoEncoderSettings; +} + +void MediaRecorder::setVideoEncoderSettings(const VideoEncoderSettings &settings) +{ +#if ENABLE_DEBUG + qDebug(Q_FUNC_INFO); +#endif + + switch (m_type) { + case MediaRecorder::Type::Video: { + if (settings != m_videoEncoderSettings) { +#if ENABLE_DEBUG + settings.dumpProperties(QStringLiteral("New")); + m_videoEncoderSettings.dumpProperties(QStringLiteral("Old")); +#endif + + m_videoEncoderSettings = settings; + + if (!m_initializing) { + m_videoRecorder->setVideoSettings(m_videoEncoderSettings.toQVideoEncoderSettings()); + emit videoEncoderSettingsChanged(); + } + } + + break; + } + case MediaRecorder::Type::Image: + case MediaRecorder::Type::Audio: + case MediaRecorder::Type::Invalid: + Q_UNREACHABLE(); + break; + } +} + +void MediaRecorder::resetSettingsToDefaults() +{ +#if ENABLE_DEBUG + qDebug(Q_FUNC_INFO); +#endif + + switch (m_type) { + case MediaRecorder::Type::Invalid: + Q_UNREACHABLE(); + break; + case MediaRecorder::Type::Image: + setMediaSettings(MediaSettings()); + setImageEncoderSettings(ImageEncoderSettings()); + break; + case MediaRecorder::Type::Audio: + setMediaSettings(MediaSettings()); + setAudioEncoderSettings(AudioEncoderSettings()); + break; + case MediaRecorder::Type::Video: + setMediaSettings(MediaSettings()); + setAudioEncoderSettings(AudioEncoderSettings()); + setVideoEncoderSettings(VideoEncoderSettings()); + break; + } +} + +void MediaRecorder::resetUserSettings() +{ + resetSettings(userDefaultCamera(), userDefaultAudioInput()); +} + +void MediaRecorder::saveUserSettings() +{ +#if ENABLE_DEBUG + qDebug(Q_FUNC_INFO); +#endif + + switch (m_type) { + case MediaRecorder::Type::Invalid: + Q_UNREACHABLE(); + break; + case MediaRecorder::Type::Image: { + QSettings &settings(*Kaidan::instance()->getSettings()); + + settings.beginGroup(settingsKey(m_type, SETTING_USER_DEFAULT)); + settings.setValue(SETTING_DEFAULT_CAMERA_DEVICE_NAME, m_mediaSettings.camera.deviceName()); + settings.endGroup(); + + settings.beginGroup(currentSettingsKey()); + m_mediaSettings.saveSettings(&settings); + m_imageEncoderSettings.saveSettings(&settings); + settings.endGroup(); + break; + } + case MediaRecorder::Type::Audio: { + QSettings &settings(*Kaidan::instance()->getSettings()); + + settings.beginGroup(settingsKey(m_type, SETTING_USER_DEFAULT)); + settings.setValue(SETTING_DEFAULT_AUDIO_INPUT_DEVICE_NAME, m_mediaSettings.audioInputDevice.deviceName()); + settings.endGroup(); + + settings.beginGroup(currentSettingsKey()); + m_mediaSettings.saveSettings(&settings); + m_audioEncoderSettings.saveSettings(&settings); + settings.endGroup(); + break; + } + case MediaRecorder::Type::Video: { + QSettings &settings(*Kaidan::instance()->getSettings()); + + settings.beginGroup(settingsKey(m_type, SETTING_USER_DEFAULT)); + settings.setValue(SETTING_DEFAULT_CAMERA_DEVICE_NAME, m_mediaSettings.camera.deviceName()); + settings.endGroup(); + + settings.beginGroup(currentSettingsKey()); + m_mediaSettings.saveSettings(&settings); + m_audioEncoderSettings.saveSettings(&settings); + m_videoEncoderSettings.saveSettings(&settings); + settings.endGroup(); + break; + } + } +} + +void MediaRecorder::record() +{ + switch (m_type) { + case MediaRecorder::Type::Invalid: + Q_UNREACHABLE(); + break; + case MediaRecorder::Type::Image: +#if ENABLE_DEBUG + m_mediaSettings.dumpProperties("Capture"); + m_imageEncoderSettings.dumpProperties("Capture"); +#endif + m_imageCapturer->capture(); + break; + case MediaRecorder::Type::Audio: +#if ENABLE_DEBUG + m_mediaSettings.dumpProperties("Capture"); + m_audioEncoderSettings.dumpProperties("Capture"); +#endif + m_audioRecorder->record(); + break; + case MediaRecorder::Type::Video: +#if ENABLE_DEBUG + m_mediaSettings.dumpProperties("Capture"); + m_audioEncoderSettings.dumpProperties("Capture"); + m_videoEncoderSettings.dumpProperties("Capture"); +#endif + m_videoRecorder->record(); + break; + } +} + +void MediaRecorder::pause() +{ + switch (m_type) { + case MediaRecorder::Type::Invalid: + case MediaRecorder::Type::Image: + Q_UNREACHABLE(); + break; + case MediaRecorder::Type::Audio: + m_audioRecorder->pause(); + break; + case MediaRecorder::Type::Video: + m_videoRecorder->pause(); + break; + } +} + +void MediaRecorder::stop() +{ + switch (m_type) { + case MediaRecorder::Type::Invalid: + case MediaRecorder::Type::Image: + Q_UNREACHABLE(); + break; + case MediaRecorder::Type::Audio: + m_audioRecorder->stop(); + break; + case MediaRecorder::Type::Video: + m_videoRecorder->stop(); + break; + } +} + +void MediaRecorder::cancel() +{ + switch (m_type) { + case MediaRecorder::Type::Invalid: + Q_UNREACHABLE(); + break; + case MediaRecorder::Type::Image: + m_imageCapturer->cancelCapture(); + deleteActualLocation(); + break; + case MediaRecorder::Type::Audio: + case MediaRecorder::Type::Video: { + if (state() == MediaRecorder::State::RecordingState) { + stop(); + } + + deleteActualLocation(); + break; + } + } +} + +bool MediaRecorder::setMediaObject(QMediaObject *object) +{ + Q_UNUSED(object); + return false; +} + +QString MediaRecorder::settingsKey(MediaRecorder::Type type, const QString &deviceName) const +{ + Q_ASSERT(type != MediaRecorder::Type::Invalid); + + const QString deviceKey = QString(deviceName) + .replace(QLatin1Char('/'), QLatin1Char(' ')) + .replace(QLatin1Char('\\'), QLatin1Char(' ')) + .replace(QLatin1Char('('), QLatin1Char(' ')) + .replace(QLatin1Char(')'), QLatin1Char(' ')) + .replace(QLatin1Char('='), QLatin1Char(' ')) + .replace(QLatin1Char(':'), QLatin1Char(' ')) + .replace(QLatin1Char('\n'), QLatin1Char(' ')) + .simplified(); + return QString::fromLatin1("Multimedia/%1/%2").arg(Enums::toString(type), deviceKey); +} + +CameraInfo MediaRecorder::userDefaultCamera() const +{ + if (m_type == MediaRecorder::Type::Invalid) { + return CameraInfo(); + } + + QSettings &settings(*Kaidan::instance()->getSettings()); + CameraInfo cameraInfo = m_cameraModel->defaultCamera(); + + settings.beginGroup(settingsKey(m_type, SETTING_USER_DEFAULT)); + + if (settings.contains(SETTING_DEFAULT_CAMERA_DEVICE_NAME)) { + const CameraInfo userCamera(settings.value(SETTING_DEFAULT_CAMERA_DEVICE_NAME).toString()); + + if (!userCamera.isNull()) { + cameraInfo = userCamera; + } + } + + settings.endGroup(); + + return cameraInfo; +} + +AudioDeviceInfo MediaRecorder::userDefaultAudioInput() const +{ + if (m_type == MediaRecorder::Type::Invalid) { + return AudioDeviceInfo(); + } + + QSettings &settings(*Kaidan::instance()->getSettings()); + AudioDeviceInfo audioInput = m_audioDeviceModel->defaultAudioInputDevice(); + + settings.beginGroup(settingsKey(m_type, SETTING_USER_DEFAULT)); + + if (settings.contains(SETTING_DEFAULT_AUDIO_INPUT_DEVICE_NAME)) { + const AudioDeviceInfo userAudioInput(AudioDeviceModel::audioInputDevice(settings.value(SETTING_DEFAULT_AUDIO_INPUT_DEVICE_NAME).toString())); + + if (!userAudioInput.isNull()) { + audioInput = userAudioInput; + } + } + + settings.endGroup(); + + return audioInput; +} + +void MediaRecorder::resetSettings(const CameraInfo &camera, const AudioDeviceInfo &audioInput) +{ +#if ENABLE_DEBUG + qDebug(Q_FUNC_INFO); +#endif + + switch (m_type) { + case MediaRecorder::Type::Invalid: + m_mediaSettings = MediaSettings(); + m_imageEncoderSettings = ImageEncoderSettings(); + m_audioEncoderSettings = AudioEncoderSettings(); + m_videoEncoderSettings = VideoEncoderSettings(); + break; + case MediaRecorder::Type::Image: { + QSettings &settings(*Kaidan::instance()->getSettings()); + MediaSettings mediaSettings(camera, AudioDeviceInfo()); + ImageEncoderSettings imageSettings; + + settings.beginGroup(settingsKey(m_type, mediaSettings.camera.deviceName())); + mediaSettings.loadSettings(&settings); + imageSettings.loadSettings(&settings); + settings.endGroup(); + + setMediaSettings(mediaSettings); + setImageEncoderSettings(imageSettings); + break; + } + case MediaRecorder::Type::Audio: { + QSettings &settings(*Kaidan::instance()->getSettings()); + MediaSettings mediaSettings(CameraInfo(), audioInput); + AudioEncoderSettings audioSettings; + + settings.beginGroup(settingsKey(m_type, mediaSettings.audioInputDevice.deviceName())); + mediaSettings.loadSettings(&settings); + audioSettings.loadSettings(&settings); + settings.endGroup(); + + setMediaSettings(mediaSettings); + setAudioEncoderSettings(audioSettings); + break; + } + case MediaRecorder::Type::Video: { + QSettings &settings(*Kaidan::instance()->getSettings()); + MediaSettings mediaSettings(camera, AudioDeviceInfo()); + AudioEncoderSettings audioSettings; + VideoEncoderSettings videoSettings; + + settings.beginGroup(settingsKey(m_type, mediaSettings.camera.deviceName())); + mediaSettings.loadSettings(&settings); + audioSettings.loadSettings(&settings); + videoSettings.loadSettings(&settings); + settings.endGroup(); + + setMediaSettings(mediaSettings); + setAudioEncoderSettings(audioSettings); + setVideoEncoderSettings(videoSettings); + break; + } + } +} + +void MediaRecorder::setupCamera() +{ + Q_ASSERT(!m_mediaSettings.camera.isNull()); + m_camera = new QCamera(m_mediaSettings.camera, this); + + switch (m_type) { + case MediaRecorder::Type::Invalid: + case MediaRecorder::Type::Audio: + Q_UNREACHABLE(); + break; + case MediaRecorder::Type::Image: + m_camera->setCaptureMode(QCamera::CaptureMode::CaptureStillImage); + m_camera->imageProcessing()->setWhiteBalanceMode(QCameraImageProcessing::WhiteBalanceMode::WhiteBalanceFlash); + m_camera->exposure()->setExposureCompensation(-1.0); + m_camera->exposure()->setExposureMode(QCameraExposure::ExposureMode::ExposurePortrait); + m_camera->exposure()->setFlashMode(QCameraExposure::FlashMode::FlashRedEyeReduction); + break; + case MediaRecorder::Type::Video: + m_camera->setCaptureMode(QCamera::CaptureMode::CaptureVideo); + m_camera->imageProcessing()->setWhiteBalanceMode(QCameraImageProcessing::WhiteBalanceMode::WhiteBalanceAuto); + m_camera->exposure()->setExposureCompensation(0); + m_camera->exposure()->setExposureMode(QCameraExposure::ExposureMode::ExposureAuto); + m_camera->exposure()->setFlashMode(QCameraExposure::FlashMode::FlashOff); + break; + } + + connectCamera(m_camera, this); +} + +void MediaRecorder::setupCapturer() +{ + Q_ASSERT(m_camera); + m_imageCapturer = new CameraImageCapture(m_camera, this); + m_imageCapturer->setEncodingSettings(m_imageEncoderSettings.toQImageEncoderSettings()); + + connectImageCapturer(m_imageCapturer, this); +} + +void MediaRecorder::setupAudioRecorder() +{ + Q_ASSERT(!m_mediaSettings.audioInputDevice.isNull()); + m_audioRecorder = new QAudioRecorder(this); + m_audioRecorder->setAudioInput(m_mediaSettings.audioInputDevice.deviceName()); + m_audioRecorder->setContainerFormat(m_mediaSettings.container); + m_audioRecorder->setAudioSettings(m_audioEncoderSettings.toQAudioEncoderSettings()); + + connectMediaRecorder(m_audioRecorder, this); +} + +void MediaRecorder::setupVideoRecorder() +{ + Q_ASSERT(m_camera); + m_videoRecorder = new QMediaRecorder(m_camera, this); + m_videoRecorder->setContainerFormat(m_mediaSettings.container); + m_videoRecorder->setAudioSettings(m_audioEncoderSettings.toQAudioEncoderSettings()); + m_videoRecorder->setVideoSettings(m_videoEncoderSettings.toQVideoEncoderSettings()); + + connectMediaRecorder(m_videoRecorder, this); +} + +void MediaRecorder::setupRecorder(MediaRecorder::Type type) +{ + if (isAvailable() && !isReady()) { + cancel(); + } + + delete m_imageCapturer; m_imageCapturer = nullptr; + delete m_audioRecorder; m_audioRecorder = nullptr; + delete m_videoRecorder; m_videoRecorder = nullptr; + delete m_camera; m_camera = nullptr; + + if (m_type != type) { + m_type = type; + m_initializing = true; + resetUserSettings(); + m_initializing = false; + } else { + m_initializing = true; + resetSettings(m_mediaSettings.camera, m_mediaSettings.audioInputDevice); + m_initializing = false; + } + + switch (m_type) { + case MediaRecorder::Type::Invalid: + emit imageEncoderSettingsChanged(); + emit audioEncoderSettingsChanged(); + emit videoEncoderSettingsChanged(); + break; + case MediaRecorder::Type::Image: + setupCamera(); + setupCapturer(); + break; + case MediaRecorder::Type::Audio: + setupAudioRecorder(); + break; + case MediaRecorder::Type::Video: + setupCamera(); + setupVideoRecorder(); + break; + } + + emit typeChanged(); + emit availabilityStatusChanged(); + emit stateChanged(); + emit statusChanged(); + emit readyChanged(); + emit errorChanged(); + emit actualLocationChanged(); + emit durationChanged(); + emit mutedChanged(); + emit volumeChanged(); + + if (m_camera && m_camera->state() != QCamera::State::ActiveState) { + m_camera->start(); + } +} + +void MediaRecorder::deleteActualLocation() +{ + const QUrl url(actualLocation()); + + if (!url.isEmpty() && url.isLocalFile()) { + const QString filePath(url.toLocalFile()); + QFile file(filePath); + + if (file.exists()) { + if (file.remove()) { + emit actualLocationChanged(); + } else { + qWarning("Can not delete record '%s'", qUtf8Printable(filePath)); + } + } + } +} + +QString MediaRecorder::encoderContainer(const QString &container, const void *userData) +{ + const auto *recorder = static_cast(userData); + const auto *mediaRecorder = recorder->m_audioRecorder ? recorder->m_audioRecorder : recorder->m_videoRecorder; + return container.isEmpty() + ? tr("Default") + : QString::fromLatin1("%1 (%2)").arg(mediaRecorder->containerDescription(container), container); +} + +QString MediaRecorder::encoderResolution(const QSize &size, const void *userData) +{ + Q_UNUSED(userData); + return size.isEmpty() + ? tr("Default") + : QString::fromLatin1("%1x%2").arg(QString::number(size.width()), QString::number(size.height())); +} + +QString MediaRecorder::encoderQuality(const CommonEncoderSettings::EncodingQuality quality, const void *userData) +{ + Q_UNUSED(userData); + return CommonEncoderSettings::toString(quality); +} + +QString MediaRecorder::imageEncoderCodec(const QString &codec, const void *userData) +{ + const auto *recorder = static_cast(userData); + const auto *capturer = recorder->m_imageCapturer; + return codec.isEmpty() + ? tr("Default") + : QString::fromLatin1("%1 (%2)").arg(capturer->imageCodecDescription(codec), codec); +} + +QString MediaRecorder::audioEncoderCodec(const QString &codec, const void *userData) +{ + const auto *recorder = static_cast(userData); + const auto *mediaRecorder = recorder->m_audioRecorder ? recorder->m_audioRecorder : recorder->m_videoRecorder; + return codec.isEmpty() + ? tr("Default") + : QString::fromLatin1("%1 (%2)").arg(mediaRecorder->audioCodecDescription(codec), codec); +} + +QString MediaRecorder::audioEncoderSampleRate(const int sampleRate, const void *userData) +{ + Q_UNUSED(userData); + return sampleRate == -1 + ? tr("Default") + : QString::fromLatin1("%1 Hz").arg(sampleRate); +} + +QString MediaRecorder::videoEncoderCodec(const QString &codec, const void *userData) +{ + const auto *recorder = static_cast(userData); + const auto *videoRecorder = recorder->m_videoRecorder; + return codec.isEmpty() + ? tr("Default") + : QString::fromLatin1("%1 (%2)").arg(videoRecorder->videoCodecDescription(codec), codec); +} + +QString MediaRecorder::videoEncoderFrameRate(const qreal frameRate, const void *userData) +{ + Q_UNUSED(userData); + return qIsNull(frameRate) + ? tr("Default") + : QString::fromLatin1("%1 FPS").arg(frameRate); +} diff --git a/src/MediaRecorder.h b/src/MediaRecorder.h new file mode 100644 index 0000000..e8852e9 --- /dev/null +++ b/src/MediaRecorder.h @@ -0,0 +1,279 @@ +/* + * Kaidan - A user-friendly XMPP client for every device! + * + * Copyright (C) 2016-2019 Kaidan developers and contributors + * (see the LICENSE file for a full list of copyright authors) + * + * Kaidan 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. + * + * In addition, as a special exception, the author of Kaidan gives + * permission to link the code of its release with the OpenSSL + * project's "OpenSSL" library (or with modified versions of it that + * use the same license as the "OpenSSL" library), and distribute the + * linked executables. You must obey the GNU General Public License in + * all respects for all of the code used other than "OpenSSL". If you + * modify this file, you may extend this exception to your version of + * the file, but you are not obligated to do so. If you do not wish to + * do so, delete this exception statement from your version. + * + * Kaidan 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 Kaidan. If not, see . + */ + +#pragma once + +#include "CameraModel.h" +#include "AudioDeviceModel.h" +#include "MediaSettings.h" +#include "MediaSettingModel.h" +#include "CameraImageCapture.h" + +#include + +class MediaSettingsContainerModel; +class MediaSettingsQualityModel; +class MediaSettingsResolutionModel; +class MediaSettingsImageCodecModel; +class MediaSettingsAudioCodecModel; +class MediaSettingsAudioSampleRateModel; +class MediaSettingsVideoCodecModel; +class MediaSettingsVideoFrameRateModel; + +class MediaRecorder : public QObject, public QMediaBindableInterface +{ + friend MediaSettingsContainerModel; + friend MediaSettingsQualityModel; + friend MediaSettingsResolutionModel; + friend MediaSettingsImageCodecModel; + friend MediaSettingsAudioCodecModel; + friend MediaSettingsAudioSampleRateModel; + friend MediaSettingsVideoCodecModel; + friend MediaSettingsVideoFrameRateModel; + + Q_OBJECT + Q_INTERFACES(QMediaBindableInterface) + + Q_PROPERTY(MediaRecorder::Type type READ type WRITE setType NOTIFY typeChanged) + Q_PROPERTY(QMediaObject *mediaObject READ mediaObject NOTIFY typeChanged) + Q_PROPERTY(QString currentSettingsKey READ currentSettingsKey NOTIFY typeChanged) + + Q_PROPERTY(MediaRecorder::AvailabilityStatus availabilityStatus READ availabilityStatus NOTIFY availabilityStatusChanged) + Q_PROPERTY(MediaRecorder::State state READ state NOTIFY stateChanged) + Q_PROPERTY(MediaRecorder::Status status READ status NOTIFY statusChanged) + Q_PROPERTY(bool isAvailable READ isAvailable NOTIFY availabilityStatusChanged) + Q_PROPERTY(bool isReady READ isReady NOTIFY readyChanged) + Q_PROPERTY(MediaRecorder::Error error READ error NOTIFY errorChanged) + Q_PROPERTY(QString errorString READ errorString NOTIFY errorChanged) + Q_PROPERTY(QUrl actualLocation READ actualLocation NOTIFY actualLocationChanged) + Q_PROPERTY(qint64 duration READ duration NOTIFY durationChanged) + + Q_PROPERTY(bool muted READ isMuted WRITE setMuted NOTIFY mutedChanged) + Q_PROPERTY(qreal volume READ volume WRITE setVolume NOTIFY volumeChanged) + + Q_PROPERTY(MediaSettings mediaSettings READ mediaSettings WRITE setMediaSettings NOTIFY mediaSettingsChanged) + Q_PROPERTY(CameraModel *cameraModel MEMBER m_cameraModel CONSTANT) + Q_PROPERTY(AudioDeviceModel *audioDeviceModel MEMBER m_audioDeviceModel CONSTANT) + Q_PROPERTY(MediaSettingsContainerModel *containerModel MEMBER m_containerModel CONSTANT) + + Q_PROPERTY(ImageEncoderSettings imageEncoderSettings READ imageEncoderSettings WRITE setImageEncoderSettings NOTIFY imageEncoderSettingsChanged) + Q_PROPERTY(MediaSettingsImageCodecModel *imageCodecModel MEMBER m_imageCodecModel CONSTANT) + Q_PROPERTY(MediaSettingsResolutionModel *imageResolutionModel MEMBER m_imageResolutionModel CONSTANT) + Q_PROPERTY(MediaSettingsQualityModel *imageQualityModel MEMBER m_imageQualityModel CONSTANT) + + Q_PROPERTY(AudioEncoderSettings audioEncoderSettings READ audioEncoderSettings WRITE setAudioEncoderSettings NOTIFY audioEncoderSettingsChanged) + Q_PROPERTY(MediaSettingsAudioCodecModel *audioCodecModel MEMBER m_audioCodecModel CONSTANT) + Q_PROPERTY(MediaSettingsAudioSampleRateModel *audioSampleRateModel MEMBER m_audioSampleRateModel CONSTANT) + Q_PROPERTY(MediaSettingsQualityModel *audioQualityModel MEMBER m_audioQualityModel CONSTANT) + + Q_PROPERTY(VideoEncoderSettings videoEncoderSettings READ videoEncoderSettings WRITE setVideoEncoderSettings NOTIFY videoEncoderSettingsChanged) + Q_PROPERTY(MediaSettingsVideoCodecModel *videoCodecModel MEMBER m_videoCodecModel CONSTANT) + Q_PROPERTY(MediaSettingsResolutionModel *videoResolutionModel MEMBER m_videoResolutionModel CONSTANT) + Q_PROPERTY(MediaSettingsVideoFrameRateModel *videoFrameRateModel MEMBER m_videoFrameRateModel CONSTANT) + Q_PROPERTY(MediaSettingsQualityModel *videoQualityModel MEMBER m_videoQualityModel CONSTANT) + +public: + enum class Type { + Invalid, + Image, + Audio, + Video + }; + Q_ENUM(Type) + + enum class AvailabilityStatus { + Available = QMultimedia::Available, + ServiceMissing = QMultimedia::ServiceMissing, + Busy = QMultimedia::Busy, + ResourceError = QMultimedia::ResourceError + }; + Q_ENUM(AvailabilityStatus) + + enum class State { + StoppedState = QMediaRecorder::StoppedState, + RecordingState = QMediaRecorder::RecordingState, + PausedState = QMediaRecorder::PausedState + }; + Q_ENUM(State) + + enum class Status { + UnavailableStatus = QMediaRecorder::UnavailableStatus, + UnloadedStatus = QMediaRecorder::UnloadedStatus, + LoadingStatus = QMediaRecorder::LoadingStatus, + LoadedStatus = QMediaRecorder::LoadedStatus, + StartingStatus = QMediaRecorder::StartingStatus, + RecordingStatus = QMediaRecorder::RecordingStatus, + PausedStatus = QMediaRecorder::PausedStatus, + FinalizingStatus = QMediaRecorder::FinalizingStatus + }; + Q_ENUM(Status) + + enum class Error { + NoError = QCameraImageCapture::NoError, + NotReadyError = QCameraImageCapture::NotReadyError, + ResourceError = QCameraImageCapture::ResourceError, + OutOfSpaceError = QCameraImageCapture::OutOfSpaceError, + NotSupportedFeatureError = QCameraImageCapture::NotSupportedFeatureError, + FormatError = QCameraImageCapture::FormatError + }; + Q_ENUM(Error) + + explicit MediaRecorder(QObject *parent = nullptr); + ~MediaRecorder() override; + + MediaRecorder::Type type() const; + void setType(MediaRecorder::Type type); + + QMediaObject *mediaObject() const override; + QString currentSettingsKey() const; + + MediaRecorder::AvailabilityStatus availabilityStatus() const; + MediaRecorder::State state() const; + MediaRecorder::Status status() const; + bool isAvailable() const; + bool isReady() const; + MediaRecorder::Error error() const; + QString errorString() const; + QUrl actualLocation() const; + qint64 duration() const; + + bool isMuted() const; + void setMuted(bool isMuted); + + qreal volume() const; + void setVolume(qreal volume); + + MediaSettings mediaSettings() const; + void setMediaSettings(const MediaSettings &settings); + + ImageEncoderSettings imageEncoderSettings() const; + void setImageEncoderSettings(const ImageEncoderSettings &settings); + + AudioEncoderSettings audioEncoderSettings() const; + void setAudioEncoderSettings(const AudioEncoderSettings &settings); + + VideoEncoderSettings videoEncoderSettings() const; + void setVideoEncoderSettings(const VideoEncoderSettings &settings); + + // Reset to system default + Q_INVOKABLE void resetSettingsToDefaults(); + // Reset to user default + Q_INVOKABLE void resetUserSettings(); + // Save user default + Q_INVOKABLE void saveUserSettings(); + +public slots: + void record(); + void pause(); + void stop(); + void cancel(); + +signals: + void typeChanged(); + void availabilityStatusChanged(); + void stateChanged(); + void statusChanged(); + void readyChanged(); + void errorChanged(); + void actualLocationChanged(); + void durationChanged(); + void mutedChanged(); + void volumeChanged(); + void mediaSettingsChanged(); + void imageEncoderSettingsChanged(); + void audioEncoderSettingsChanged(); + void videoEncoderSettingsChanged(); + +protected: + bool setMediaObject(QMediaObject *object) override; + + QString settingsKey(MediaRecorder::Type type, const QString &deviceName) const; + CameraInfo userDefaultCamera() const; + AudioDeviceInfo userDefaultAudioInput() const; + void resetSettings(const CameraInfo &camera, const AudioDeviceInfo &audioInput); + + void setupCamera(); + void setupCapturer(); + void setupAudioRecorder(); + void setupVideoRecorder(); + void setupRecorder(MediaRecorder::Type type); + + void deleteActualLocation(); + + static QString encoderContainer(const QString &container, const void *userData); + static QString encoderResolution(const QSize &size, const void *userData); + static QString encoderQuality(const CommonEncoderSettings::EncodingQuality quality, const void *userData); + static QString imageEncoderCodec(const QString &codec, const void *userData); + static QString audioEncoderCodec(const QString &codec, const void *userData); + static QString audioEncoderSampleRate(const int sampleRate, const void *userData); + static QString videoEncoderCodec(const QString &codec, const void *userData); + static QString videoEncoderFrameRate(const qreal frameRate, const void *userData); + +private: + MediaRecorder::Type m_type = MediaRecorder::Type::Invalid; + bool m_initializing = false; + QCamera *m_camera = nullptr; + CameraImageCapture *m_imageCapturer = nullptr; + QAudioRecorder *m_audioRecorder = nullptr; + QMediaRecorder *m_videoRecorder = nullptr; + + MediaSettings m_mediaSettings; + CameraModel *m_cameraModel = nullptr; + AudioDeviceModel *m_audioDeviceModel = nullptr; + MediaSettingsContainerModel *m_containerModel = nullptr; + + ImageEncoderSettings m_imageEncoderSettings; + MediaSettingsImageCodecModel *m_imageCodecModel = nullptr; + MediaSettingsResolutionModel *m_imageResolutionModel = nullptr; + MediaSettingsQualityModel *m_imageQualityModel = nullptr; + + AudioEncoderSettings m_audioEncoderSettings; + MediaSettingsAudioCodecModel *m_audioCodecModel = nullptr; + MediaSettingsAudioSampleRateModel *m_audioSampleRateModel = nullptr; + MediaSettingsQualityModel *m_audioQualityModel = nullptr; + + VideoEncoderSettings m_videoEncoderSettings; + MediaSettingsVideoCodecModel *m_videoCodecModel = nullptr; + MediaSettingsResolutionModel *m_videoResolutionModel = nullptr; + MediaSettingsVideoFrameRateModel *m_videoFrameRateModel = nullptr; + MediaSettingsQualityModel *m_videoQualityModel = nullptr; +}; + +DECL_MEDIA_SETTING_MODEL(Container, QString, MediaRecorder::encoderContainer); +DECL_MEDIA_SETTING_MODEL(Quality, CommonEncoderSettings::EncodingQuality, MediaRecorder::encoderQuality); +DECL_MEDIA_SETTING_MODEL(Resolution, QSize, MediaRecorder::encoderResolution); + +DECL_MEDIA_SETTING_MODEL(ImageCodec, QString, MediaRecorder::imageEncoderCodec); + +DECL_MEDIA_SETTING_MODEL(AudioCodec, QString, MediaRecorder::audioEncoderCodec); +DECL_MEDIA_SETTING_MODEL(AudioSampleRate, int, MediaRecorder::audioEncoderSampleRate); + +DECL_MEDIA_SETTING_MODEL(VideoCodec, QString, MediaRecorder::videoEncoderCodec); +DECL_MEDIA_SETTING_MODEL(VideoFrameRate, qreal, MediaRecorder::videoEncoderFrameRate); diff --git a/src/MediaSettingModel.h b/src/MediaSettingModel.h new file mode 100644 index 0000000..9324c96 --- /dev/null +++ b/src/MediaSettingModel.h @@ -0,0 +1,282 @@ +/* + * Kaidan - A user-friendly XMPP client for every device! + * + * Copyright (C) 2016-2019 Kaidan developers and contributors + * (see the LICENSE file for a full list of copyright authors) + * + * Kaidan 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. + * + * In addition, as a special exception, the author of Kaidan gives + * permission to link the code of its release with the OpenSSL + * project's "OpenSSL" library (or with modified versions of it that + * use the same license as the "OpenSSL" library), and distribute the + * linked executables. You must obey the GNU General Public License in + * all respects for all of the code used other than "OpenSSL". If you + * modify this file, you may extend this exception to your version of + * the file, but you are not obligated to do so. If you do not wish to + * do so, delete this exception statement from your version. + * + * Kaidan 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 Kaidan. If not, see . + */ + +#pragma once + +#include +#include + +#include + +template +class MediaSettingModel : public QAbstractListModel +{ +public: + enum CustomRoles { + ValueRole = Qt::UserRole, + DescriptionRole + }; + + using ToString = std::function; + + using QAbstractListModel::QAbstractListModel; + + explicit MediaSettingModel(MediaSettingModel::ToString toString, + const void *userData = nullptr, QObject *parent = nullptr) + : QAbstractListModel(parent) + , m_toString(toString) + , m_userData(userData) { + } + + int rowCount(const QModelIndex &parent = QModelIndex()) const override { + return parent == QModelIndex() ? m_values.count() : 0; + } + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override { + if (hasIndex(index.row(), index.column(), index.parent())) { + switch (role) { + case MediaSettingModel::CustomRoles::ValueRole: + return QVariant::fromValue(m_values[index.row()]); + case MediaSettingModel::CustomRoles::DescriptionRole: + return toString(m_values[index.row()]); + } + } + + return { }; + } + + QHash roleNames() const override { + static const QHash roles { + { MediaSettingModel::CustomRoles::ValueRole, QByteArrayLiteral("value") }, + { MediaSettingModel::CustomRoles::DescriptionRole, QByteArrayLiteral("description") } + }; + + return roles; + } + + MediaSettingModel::ToString toString() const { + return m_toString; + } + + void setToString(MediaSettingModel::ToString toString) { + m_toString = toString; + + emit toStringChanged(); + + const int count = rowCount(); + + if (count > 0) { + emit dataChanged(index(0, 0), index(count -1, 0)); + } + } + + const void *userData() const { + return m_userData; + } + + void setUserData(const void *userData) { + if (m_userData == userData) { + return; + } + + m_userData = userData; + + emit userDataChanged(); + + const int count = rowCount(); + + if (count > 0) { + emit dataChanged(index(0, 0), index(count -1, 0)); + } + } + + QList values() const { + return m_values; + } + + void setValues(const QList &values) { + if (m_values == values) { + return; + } + + const int newCurrentIndex = m_currentIndex != -1 ? values.indexOf(currentValue()) : -1; + const bool curIdxChanged = newCurrentIndex != m_currentIndex; + + beginResetModel(); + m_values = values; + m_currentIndex = newCurrentIndex; + endResetModel(); + + emit valuesChanged(); + + if (curIdxChanged) { + emit currentIndexChanged(); + } + } + + int currentIndex() const { + return m_currentIndex; + } + + void setCurrentIndex(int currentIndex) { + if (currentIndex < 0 || currentIndex >= m_values.count() + || m_currentIndex == currentIndex) { + return; + } + + m_currentIndex = currentIndex; + emit currentIndexChanged(); + } + + T currentValue() const { + return m_currentIndex >= 0 && m_currentIndex < m_values.count() + ? m_values[m_currentIndex] + : T(); + } + + void setCurrentValue(const T ¤tValue) { + setCurrentIndex(indexOf(currentValue)); + } + + QString currentDescription() const { + return m_currentIndex >= 0 && m_currentIndex < m_values.count() + ? toString(currentValue()) + : QString(); + } + + void setValuesAndCurrentIndex(const QList &values, int currentIndex) { + if (m_values == values && m_currentIndex == currentIndex) { + return; + } + + beginResetModel(); + m_values = values; + m_currentIndex = currentIndex >= 0 && currentIndex < m_values.count() + ? currentIndex + : -1; + endResetModel(); + + emit valuesChanged(); + emit currentIndexChanged(); + } + + void setValuesAndCurrentValue(const QList &values, const T ¤tValue) { + setValuesAndCurrentIndex(values, values.indexOf(currentValue)); + } + + // Invokables + virtual void clear() { + beginResetModel(); + m_currentIndex = -1; + m_values.clear(); + endResetModel(); + + emit valuesChanged(); + emit currentIndexChanged(); + } + + virtual int indexOf(const T &value) const { + return m_values.indexOf(value); + } + + virtual T value(int index) const { + if (index < 0 || index >= m_values.count()) { + return { }; + } + + return m_values[index]; + } + + virtual QString description(int index) const { + if (index < 0 || index >= m_values.count()) { + return { }; + } + + return toString(m_values[index]); + } + + virtual QString toString(const T &value) const { + if (m_toString) { + return m_toString(value, m_userData); + } + + return QVariant::fromValue(value).toString(); + } + + // Signals + virtual void toStringChanged() = 0; + virtual void userDataChanged() = 0; + virtual void valuesChanged() = 0; + virtual void currentIndexChanged() = 0; + +private: + MediaSettingModel::ToString m_toString; + const void *m_userData = nullptr; + int m_currentIndex = -1; + QList m_values; +}; + +#define DECL_MEDIA_SETTING_MODEL(NAME, TYPE, TO_STRING) \ +class MediaSettings##NAME##Model : public MediaSettingModel { \ + Q_OBJECT \ + \ + Q_PROPERTY(MediaSettings##NAME##Model::ToString toString READ toString WRITE setToString NOTIFY toStringChanged) \ + Q_PROPERTY(const void *userData READ userData WRITE setUserData NOTIFY userDataChanged) \ + Q_PROPERTY(QList values READ values WRITE setValues NOTIFY valuesChanged) \ + Q_PROPERTY(int rowCount READ rowCount NOTIFY valuesChanged) \ + Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged) \ + Q_PROPERTY(TYPE currentValue READ currentValue WRITE setCurrentValue NOTIFY currentIndexChanged) \ + Q_PROPERTY(QString currentDescription READ currentDescription NOTIFY currentIndexChanged) \ + \ + using MSMT = MediaSettingModel; \ + \ +public: \ + using MSMT::MSMT; \ + explicit MediaSettings##NAME##Model(const void *userData, QObject *parent = nullptr) \ + : MSMT(TO_STRING, userData, parent) \ + { } \ + \ + explicit MediaSettings##NAME##Model(QObject *parent = nullptr) \ + : MSMT(parent) \ + { } \ + \ + using MSMT::toString; \ + Q_INVOKABLE void clear() override { MSMT::clear(); } \ + Q_INVOKABLE int indexOf(const TYPE &value) const override { return MSMT::indexOf(value); } \ + Q_INVOKABLE TYPE value(int index) const override { return MSMT::value(index); } \ + Q_INVOKABLE QString description(int index) const override { return MSMT::description(index); } \ + Q_INVOKABLE QString toString(const TYPE &value) const override { return MSMT::toString(value); } \ + \ +Q_SIGNALS: \ + void toStringChanged() override; \ + void userDataChanged() override; \ + void valuesChanged() override; \ + void currentIndexChanged() override; \ +} diff --git a/src/MediaSettings.cpp b/src/MediaSettings.cpp new file mode 100644 index 0000000..cb12f0f --- /dev/null +++ b/src/MediaSettings.cpp @@ -0,0 +1,433 @@ +/* + * Kaidan - A user-friendly XMPP client for every device! + * + * Copyright (C) 2016-2019 Kaidan developers and contributors + * (see the LICENSE file for a full list of copyright authors) + * + * Kaidan 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. + * + * In addition, as a special exception, the author of Kaidan gives + * permission to link the code of its release with the OpenSSL + * project's "OpenSSL" library (or with modified versions of it that + * use the same license as the "OpenSSL" library), and distribute the + * linked executables. You must obey the GNU General Public License in + * all respects for all of the code used other than "OpenSSL". If you + * modify this file, you may extend this exception to your version of + * the file, but you are not obligated to do so. If you do not wish to + * do so, delete this exception statement from your version. + * + * Kaidan 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 Kaidan. If not, see . + */ + +#include "MediaSettings.h" + +#include +#include +#include + +#define MEDIA_SETTINGS_GROUP QStringLiteral("Media Settings") +#define IMAGE_SETTINGS_GROUP QStringLiteral("Image Settings") +#define AUDIO_SETTINGS_GROUP QStringLiteral("Audio Settings") +#define VIDEO_SETTINGS_GROUP QStringLiteral("Video Settings") + +#define SETTING_CONTAINER QStringLiteral("Container") +#define SETTING_ENCODING_QUALITY QStringLiteral("Quality") +#define SETTING_ENCODING_MODE QStringLiteral("Mode") +#define SETTING_ENCODING_OPTIONS QStringLiteral("Options") +#define SETTING_CODEC QStringLiteral("Codec") +#define SETTING_RESOLUTION QStringLiteral("Resolution") +#define SETTING_BIT_RATE QStringLiteral("Bit Rate") +#define SETTING_SAMPLE_RATE QStringLiteral("Sample Rate") +#define SETTING_FRAME_RATE QStringLiteral("Frame Rate") +#define SETTING_CHANNEL_COUNT QStringLiteral("Channel Count") + +static QString toString(const QVariantMap &options) { + if (options.isEmpty()) { + return QString(); + } + + const QJsonDocument document(QJsonObject::fromVariantMap(options)); + return QString::fromUtf8(document.toJson(QJsonDocument::JsonFormat::Indented)).trimmed(); +} + +static QString toString(const QSize &size) { + return QString::fromLatin1("%1x%2").arg(QString::number(size.width()), + QString::number(size.height())); +} + +MediaSettings::MediaSettings(const CameraInfo &camera, const AudioDeviceInfo &audioInputDevice) + : camera(camera) + , audioInputDevice(audioInputDevice) +{ +} + +void MediaSettings::loadSettings(QSettings *settings) +{ + settings->beginGroup(MEDIA_SETTINGS_GROUP); + + if (settings->contains(SETTING_CONTAINER)) { + container = settings->value(SETTING_CONTAINER).toString(); + } + + settings->endGroup(); +} + +void MediaSettings::saveSettings(QSettings *settings) +{ + settings->beginGroup(MEDIA_SETTINGS_GROUP); + + settings->setValue(SETTING_CONTAINER, container); + + settings->endGroup(); +} + +void MediaSettings::dumpProperties(const QString &context) const +{ + qDebug("%s - %s", Q_FUNC_INFO, qUtf8Printable(context)); + qDebug("Camera: %s", qUtf8Printable(camera.deviceName())); + qDebug("Audio Input Device: %s", qUtf8Printable(audioInputDevice.deviceName())); + qDebug("Container: %s", qUtf8Printable(container)); +} + +bool MediaSettings::operator==(const MediaSettings &other) const +{ + return camera == other.camera + && audioInputDevice == other.audioInputDevice + && container == other.container; +} + +bool MediaSettings::operator!=(const MediaSettings &other) const +{ + return !operator==(other); +} + +CommonEncoderSettings::CommonEncoderSettings(const QString &codec, + CommonEncoderSettings::EncodingQuality quality, + const QVariantMap &options) + : codec(codec) + , quality(quality) + , options(options) +{ +} + +void CommonEncoderSettings::loadSettings(QSettings *settings) +{ + if (settings->contains(SETTING_CODEC)) { + codec = settings->value(SETTING_CODEC).toString(); + } + + if (settings->contains(SETTING_ENCODING_QUALITY)) { + const int value = settings->value(SETTING_ENCODING_QUALITY).toInt(); + quality = static_cast(value); + } + + if (settings->contains(SETTING_ENCODING_OPTIONS)) { + options = settings->value(SETTING_ENCODING_OPTIONS).toMap(); + } +} + +void CommonEncoderSettings::saveSettings(QSettings *settings) +{ + settings->setValue(SETTING_CODEC, codec); + settings->setValue(SETTING_ENCODING_QUALITY, static_cast(quality)); + settings->setValue(SETTING_ENCODING_OPTIONS, options); +} + +bool CommonEncoderSettings::operator==(const CommonEncoderSettings &other) const +{ + return codec == other.codec + && quality == other.quality + &&options == other.options; +} + +bool CommonEncoderSettings::operator!=(const CommonEncoderSettings &other) const +{ + return !operator==(other); +} + +QString CommonEncoderSettings::toString(CommonEncoderSettings::EncodingQuality quality) +{ + switch (quality) { + case CommonEncoderSettings::EncodingQuality::VeryLowQuality: + return tr("Very low"); + case CommonEncoderSettings::EncodingQuality::LowQuality: + return tr("Low"); + case CommonEncoderSettings::EncodingQuality::NormalQuality: + return tr("Normal"); + case CommonEncoderSettings::EncodingQuality::HighQuality: + return tr("High"); + case CommonEncoderSettings::EncodingQuality::VeryHighQuality: + return tr("Very high"); + } + + Q_UNREACHABLE(); + return { }; +} + +QString CommonEncoderSettings::toString(CommonEncoderSettings::EncodingMode mode) +{ + switch (mode) { + case CommonEncoderSettings::EncodingMode::ConstantQualityEncoding: + return tr("Constant quality"); + case CommonEncoderSettings::EncodingMode::ConstantBitRateEncoding: + return tr("Constant bit rate"); + case CommonEncoderSettings::EncodingMode::AverageBitRateEncoding: + return tr("Average bit rate"); + case CommonEncoderSettings::EncodingMode::TwoPassEncoding: + return tr("Two pass"); + } + + Q_UNREACHABLE(); + return { }; +} + +ImageEncoderSettings::ImageEncoderSettings(const QImageEncoderSettings &settings) + : CommonEncoderSettings(settings.codec(), + static_cast(settings.quality()), + settings.encodingOptions()) + , resolution(settings.resolution()) +{ +} + +void ImageEncoderSettings::loadSettings(QSettings *settings) +{ + settings->beginGroup(IMAGE_SETTINGS_GROUP); + + CommonEncoderSettings::loadSettings(settings); + + if (settings->contains(SETTING_RESOLUTION)) { + resolution = settings->value(SETTING_RESOLUTION).toSize(); + } + + settings->endGroup(); +} + +void ImageEncoderSettings::saveSettings(QSettings *settings) +{ + settings->beginGroup(IMAGE_SETTINGS_GROUP); + + CommonEncoderSettings::saveSettings(settings); + + settings->setValue(SETTING_RESOLUTION, resolution); + + settings->endGroup(); +} + +void ImageEncoderSettings::dumpProperties(const QString &context) const +{ + qDebug("%s - %s", Q_FUNC_INFO, qUtf8Printable(context)); + qDebug("Codec: %s", qUtf8Printable(codec)); + qDebug("Quality: %s", qUtf8Printable(toString(quality))); + qDebug("Options: %s", qUtf8Printable(::toString(options))); + qDebug("Resolution: %s", qUtf8Printable(::toString(resolution))); +} + +bool ImageEncoderSettings::operator==(const ImageEncoderSettings &other) const +{ + return CommonEncoderSettings::operator==(other) + && resolution == other.resolution; +} + +bool ImageEncoderSettings::operator!=(const ImageEncoderSettings &other) const +{ + return !operator==(other); +} + +QImageEncoderSettings ImageEncoderSettings::toQImageEncoderSettings() const +{ + QImageEncoderSettings settings; + settings.setCodec(codec); + settings.setQuality(static_cast(quality)); + settings.setEncodingOptions(options); + settings.setResolution(resolution); + return settings; +} + +AudioEncoderSettings::AudioEncoderSettings(const QAudioEncoderSettings &settings) + : CommonEncoderSettings(settings.codec(), + static_cast(settings.quality()), + settings.encodingOptions()) + , mode(static_cast(settings.encodingMode())) + , bitRate(settings.bitRate()) + , sampleRate(settings.sampleRate()) + , channelCount(settings.channelCount()) +{ +} + +void AudioEncoderSettings::loadSettings(QSettings *settings) +{ + settings->beginGroup(AUDIO_SETTINGS_GROUP); + + CommonEncoderSettings::loadSettings(settings); + + if (settings->contains(SETTING_ENCODING_MODE)) { + const int value = settings->value(SETTING_ENCODING_MODE).toInt(); + mode = static_cast(value); + } + + if (settings->contains(SETTING_BIT_RATE)) { + bitRate = settings->value(SETTING_BIT_RATE).toInt(); + } + + if (settings->contains(SETTING_SAMPLE_RATE)) { + sampleRate = settings->value(SETTING_SAMPLE_RATE).toInt(); + } + + if (settings->contains(SETTING_CHANNEL_COUNT)) { + channelCount = settings->value(SETTING_CHANNEL_COUNT).toInt(); + } + + settings->endGroup(); +} + +void AudioEncoderSettings::saveSettings(QSettings *settings) +{ + settings->beginGroup(AUDIO_SETTINGS_GROUP); + + CommonEncoderSettings::saveSettings(settings); + + settings->setValue(SETTING_ENCODING_MODE, static_cast(mode)); + settings->setValue(SETTING_BIT_RATE, bitRate); + settings->setValue(SETTING_SAMPLE_RATE, sampleRate); + settings->setValue(SETTING_CHANNEL_COUNT, channelCount); + + settings->endGroup(); +} + +void AudioEncoderSettings::dumpProperties(const QString &context) const +{ + qDebug("%s - %s", Q_FUNC_INFO, qUtf8Printable(context)); + qDebug("Codec: %s", qUtf8Printable(codec)); + qDebug("Quality: %s", qUtf8Printable(toString(quality))); + qDebug("Options: %s", qUtf8Printable(::toString(options))); + qDebug("Mode: %s", qUtf8Printable(toString(mode))); + qDebug("BitRate: %i", bitRate); + qDebug("SampleRate: %i", sampleRate); + qDebug("ChannelCount: %i", channelCount); +} + +bool AudioEncoderSettings::operator==(const AudioEncoderSettings &other) const +{ + return CommonEncoderSettings::operator==(other) + && mode == other.mode + && bitRate == other.bitRate + && sampleRate == other.sampleRate + && channelCount == other.channelCount; +} + +bool AudioEncoderSettings::operator!=(const AudioEncoderSettings &other) const +{ + return !operator==(other); +} + +QAudioEncoderSettings AudioEncoderSettings::toQAudioEncoderSettings() const +{ + QAudioEncoderSettings settings; + settings.setCodec(codec); + settings.setQuality(static_cast(quality)); + settings.setEncodingOptions(options); + settings.setEncodingMode(static_cast(mode)); + settings.setBitRate(bitRate); + settings.setSampleRate(sampleRate); + settings.setChannelCount(channelCount); + return settings; +} + +VideoEncoderSettings::VideoEncoderSettings(const QVideoEncoderSettings &settings) + : CommonEncoderSettings(settings.codec(), + static_cast(settings.quality()), + settings.encodingOptions()) + , mode(static_cast(settings.encodingMode())) + , bitRate(settings.bitRate()) + , frameRate(settings.frameRate()) + , resolution(settings.resolution()) +{ +} + +void VideoEncoderSettings::loadSettings(QSettings *settings) +{ + settings->beginGroup(VIDEO_SETTINGS_GROUP); + + CommonEncoderSettings::loadSettings(settings); + + if (settings->contains(SETTING_ENCODING_MODE)) { + const int value = settings->value(SETTING_ENCODING_MODE).toInt(); + mode = static_cast(value); + } + + if (settings->contains(SETTING_BIT_RATE)) { + bitRate = settings->value(SETTING_BIT_RATE).toInt(); + } + + if (settings->contains(SETTING_FRAME_RATE)) { + frameRate = settings->value(SETTING_FRAME_RATE).toInt(); + } + + if (settings->contains(SETTING_RESOLUTION)) { + resolution = settings->value(SETTING_RESOLUTION).toSize(); + } + + settings->endGroup(); +} + +void VideoEncoderSettings::saveSettings(QSettings *settings) +{ + settings->beginGroup(VIDEO_SETTINGS_GROUP); + + CommonEncoderSettings::saveSettings(settings); + + settings->setValue(SETTING_ENCODING_MODE, static_cast(mode)); + settings->setValue(SETTING_BIT_RATE, bitRate); + settings->setValue(SETTING_FRAME_RATE, frameRate); + settings->setValue(SETTING_RESOLUTION, resolution); + + settings->endGroup(); +} + +void VideoEncoderSettings::dumpProperties(const QString &context) const +{ + qDebug("%s - %s", Q_FUNC_INFO, qUtf8Printable(context)); + qDebug("Codec: %s", qUtf8Printable(codec)); + qDebug("Quality: %s", qUtf8Printable(toString(quality))); + qDebug("Options: %s", qUtf8Printable(::toString(options))); + qDebug("Mode: %s", qUtf8Printable(toString(mode))); + qDebug("BitRate: %i", bitRate); + qDebug("FrameRate: %f", frameRate); + qDebug("Resolution: %s", qUtf8Printable(::toString(resolution))); +} + +bool VideoEncoderSettings::operator==(const VideoEncoderSettings &other) const +{ + return CommonEncoderSettings::operator==(other) + && mode == other.mode + && bitRate == other.bitRate + && frameRate == other.frameRate + && resolution == other.resolution; +} + +bool VideoEncoderSettings::operator!=(const VideoEncoderSettings &other) const +{ + return !operator==(other); +} + +QVideoEncoderSettings VideoEncoderSettings::toQVideoEncoderSettings() const +{ + QVideoEncoderSettings settings; + settings.setCodec(codec); + settings.setQuality(static_cast(quality)); + settings.setEncodingOptions(options); + settings.setEncodingMode(static_cast(mode)); + settings.setBitRate(bitRate); + settings.setFrameRate(frameRate); + settings.setResolution(resolution); + return settings; +} diff --git a/src/MediaSettings.h b/src/MediaSettings.h new file mode 100644 index 0000000..1fc570c --- /dev/null +++ b/src/MediaSettings.h @@ -0,0 +1,192 @@ +/* + * Kaidan - A user-friendly XMPP client for every device! + * + * Copyright (C) 2016-2019 Kaidan developers and contributors + * (see the LICENSE file for a full list of copyright authors) + * + * Kaidan 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. + * + * In addition, as a special exception, the author of Kaidan gives + * permission to link the code of its release with the OpenSSL + * project's "OpenSSL" library (or with modified versions of it that + * use the same license as the "OpenSSL" library), and distribute the + * linked executables. You must obey the GNU General Public License in + * all respects for all of the code used other than "OpenSSL". If you + * modify this file, you may extend this exception to your version of + * the file, but you are not obligated to do so. If you do not wish to + * do so, delete this exception statement from your version. + * + * Kaidan 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 Kaidan. If not, see . + */ + +#pragma once + +#include +#include +#include +#include + +#include "CameraModel.h" +#include "AudioDeviceModel.h" + +class QSettings; + +class MediaSettings +{ + Q_GADGET + + Q_PROPERTY(CameraInfo camera MEMBER camera) + Q_PROPERTY(AudioDeviceInfo audioInputDevice MEMBER audioInputDevice) + Q_PROPERTY(QString container MEMBER container) + +public: + MediaSettings() = default; + explicit MediaSettings(const CameraInfo &camera, const AudioDeviceInfo &audioInputDevice); + + void loadSettings(QSettings *settings); + void saveSettings(QSettings *settings); + void dumpProperties(const QString &context) const; + + bool operator==(const MediaSettings &other) const; + bool operator!=(const MediaSettings &other) const; + + CameraInfo camera; + AudioDeviceInfo audioInputDevice; + QString container; +}; + +class CommonEncoderSettings +{ + Q_GADGET + Q_DECLARE_TR_FUNCTIONS(CommonEncoderSettings) + + Q_PROPERTY(QString codec MEMBER codec) + Q_PROPERTY(CommonEncoderSettings::EncodingQuality quality MEMBER quality) + Q_PROPERTY(QVariantMap options MEMBER options) + +public: + enum class EncodingQuality { + VeryLowQuality = QMultimedia::EncodingQuality::VeryLowQuality, + LowQuality = QMultimedia::EncodingQuality::LowQuality, + NormalQuality = QMultimedia::EncodingQuality::NormalQuality, + HighQuality = QMultimedia::EncodingQuality::HighQuality, + VeryHighQuality = QMultimedia::EncodingQuality::VeryHighQuality + }; + Q_ENUM(EncodingQuality) + + enum class EncodingMode { + ConstantQualityEncoding = QMultimedia::EncodingMode::ConstantQualityEncoding, + ConstantBitRateEncoding = QMultimedia::EncodingMode::ConstantBitRateEncoding, + AverageBitRateEncoding = QMultimedia::EncodingMode::AverageBitRateEncoding, + TwoPassEncoding = QMultimedia::EncodingMode::TwoPassEncoding + }; + Q_ENUM(EncodingMode) + + explicit CommonEncoderSettings(const QString &codec, + CommonEncoderSettings::EncodingQuality quality, + const QVariantMap &options); + virtual ~CommonEncoderSettings() = default; + + virtual void loadSettings(QSettings *settings); + virtual void saveSettings(QSettings *settings); + virtual void dumpProperties(const QString &context) const = 0; + + bool operator==(const CommonEncoderSettings &other) const; + bool operator!=(const CommonEncoderSettings &other) const; + + static QString toString(CommonEncoderSettings::EncodingQuality quality); + static QString toString(CommonEncoderSettings::EncodingMode mode); + + QString codec; + CommonEncoderSettings::EncodingQuality quality; + QVariantMap options; +}; + +class ImageEncoderSettings : public CommonEncoderSettings +{ + Q_GADGET + + Q_PROPERTY(QSize resolution MEMBER resolution) + +public: + explicit ImageEncoderSettings(const QImageEncoderSettings &settings = { }); + + void loadSettings(QSettings *settings) override; + void saveSettings(QSettings *settings) override; + void dumpProperties(const QString &context) const override; + + bool operator==(const ImageEncoderSettings &other) const; + bool operator!=(const ImageEncoderSettings &other) const; + + QImageEncoderSettings toQImageEncoderSettings() const; + + QSize resolution; +}; + +class AudioEncoderSettings : public CommonEncoderSettings +{ + Q_GADGET + + Q_PROPERTY(CommonEncoderSettings::EncodingMode mode MEMBER mode) + Q_PROPERTY(int bitRate MEMBER bitRate) + Q_PROPERTY(int sampleRate MEMBER sampleRate) + Q_PROPERTY(int channelCount MEMBER channelCount) + +public: + explicit AudioEncoderSettings(const QAudioEncoderSettings &settings = { }); + + void loadSettings(QSettings *settings) override; + void saveSettings(QSettings *settings) override; + void dumpProperties(const QString &context) const override; + + bool operator==(const AudioEncoderSettings &other) const; + bool operator!=(const AudioEncoderSettings &other) const; + + QAudioEncoderSettings toQAudioEncoderSettings() const; + + CommonEncoderSettings::EncodingMode mode; + int bitRate; + int sampleRate; + int channelCount; +}; + +class VideoEncoderSettings : public CommonEncoderSettings +{ + Q_GADGET + + Q_PROPERTY(CommonEncoderSettings::EncodingMode mode MEMBER mode) + Q_PROPERTY(int bitRate MEMBER bitRate) + Q_PROPERTY(qreal frameRate MEMBER frameRate) + Q_PROPERTY(QSize resolution MEMBER resolution) + +public: + explicit VideoEncoderSettings(const QVideoEncoderSettings &settings = { }); + + void loadSettings(QSettings *settings) override; + void saveSettings(QSettings *settings) override; + void dumpProperties(const QString &context) const override; + + bool operator==(const VideoEncoderSettings &other) const; + bool operator!=(const VideoEncoderSettings &other) const; + + QVideoEncoderSettings toQVideoEncoderSettings() const; + + CommonEncoderSettings::EncodingMode mode; + int bitRate; + qreal frameRate; + QSize resolution; +}; + +Q_DECLARE_METATYPE(MediaSettings) +Q_DECLARE_METATYPE(ImageEncoderSettings) +Q_DECLARE_METATYPE(AudioEncoderSettings) +Q_DECLARE_METATYPE(VideoEncoderSettings) diff --git a/src/MediaUtils.cpp b/src/MediaUtils.cpp index b2fcd74..41ac69c 100644 --- a/src/MediaUtils.cpp +++ b/src/MediaUtils.cpp @@ -1,460 +1,459 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 Kaidan developers and contributors * (see the LICENSE file for a full list of copyright authors) * * Kaidan 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. * * In addition, as a special exception, the author of Kaidan gives * permission to link the code of its release with the OpenSSL * project's "OpenSSL" library (or with modified versions of it that * use the same license as the "OpenSSL" library), and distribute the * linked executables. You must obey the GNU General Public License in * all respects for all of the code used other than "OpenSSL". If you * modify this file, you may extend this exception to your version of * the file, but you are not obligated to do so. If you do not wish to * do so, delete this exception statement from your version. * * Kaidan 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 Kaidan. If not, see . */ #include "MediaUtils.h" #include #include #include #include static QList mimeTypes(const QList &mimeTypes, const QString &parent); const QMimeDatabase MediaUtils::s_mimeDB; const QList MediaUtils::s_allMimeTypes(MediaUtils::s_mimeDB.allMimeTypes()); const QList MediaUtils::s_imageTypes(::mimeTypes(MediaUtils::s_allMimeTypes, QStringLiteral("image"))); const QList MediaUtils::s_audioTypes(::mimeTypes(MediaUtils::s_allMimeTypes, QStringLiteral("audio"))); const QList MediaUtils::s_videoTypes(::mimeTypes(MediaUtils::s_allMimeTypes, QStringLiteral("video"))); const QList MediaUtils::s_documentTypes { s_mimeDB.mimeTypeForName(QStringLiteral("application/vnd.oasis.opendocument.presentation")), s_mimeDB.mimeTypeForName(QStringLiteral("application/vnd.oasis.opendocument.spreadsheet")), s_mimeDB.mimeTypeForName(QStringLiteral("application/vnd.oasis.opendocument.text")), s_mimeDB.mimeTypeForName(QStringLiteral("application/vnd.openxmlformats-officedocument.presentationml.presentation")), s_mimeDB.mimeTypeForName(QStringLiteral("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")), s_mimeDB.mimeTypeForName(QStringLiteral("application/vnd.openxmlformats-officedocument.wordprocessingml.document")), s_mimeDB.mimeTypeForName(QStringLiteral("application/pdf")), s_mimeDB.mimeTypeForName(QStringLiteral("text/plain")) }; const QList MediaUtils::s_geoTypes { s_mimeDB.mimeTypeForName(QStringLiteral("application/geo+json")) }; const QRegularExpression MediaUtils::s_geoLocationRegExp(QStringLiteral("geo:([-+]?[0-9]*\\.?[0-9]+),([-+]?[0-9]*\\.?[0-9]+)")); static QList mimeTypes(const QList &mimeTypes, const QString &parent) { QList mimes; for (const QMimeType &mimeType: mimeTypes) { if (mimeType.name().section(QLatin1Char('/'), 0, 0) == parent) { mimes << mimeType; } } return mimes; } static QString iconName(const QList &mimeTypes) { if (!mimeTypes.isEmpty()) { for (const QMimeType &type: mimeTypes) { const QString name = type.iconName(); if (!name.isEmpty()) { return name; } } } return QStringLiteral("application-octet-stream"); } static QString filter(const QList &mimeTypes) { QStringList filters; filters.reserve(mimeTypes.size() * 2); for (const QMimeType &mimeType : mimeTypes) { QString filter = mimeType.filterString(); if (filter.isEmpty()) { filters << QStringLiteral("*"); continue; } const int start = filter.lastIndexOf(QLatin1Char('(')); const int end = filter.lastIndexOf(QLatin1Char(')')); Q_ASSERT(start != -1); Q_ASSERT(end != -1); filters << filter.mid(start + 1, filter.size() - start - 1 - 1).split(QLatin1Char(' ')); } if (filters.size() > 1) { filters.removeDuplicates(); filters.removeAll(QStringLiteral("*")); } filters.reserve(filters.size()); return filters.join(QLatin1Char(' ')); } static QString prettyFormat(int durationMsecs, QTime *outTime = nullptr) { const QTime time(QTime(0, 0, 0).addMSecs(durationMsecs)); QString format; if (time.hour() > 0) { format.append(QStringLiteral("H")); } - if (time.minute() > 0 || !format.isEmpty()) { - format.append(format.isEmpty() ? QStringLiteral("m") : QStringLiteral(":mm")); - } - - if (time.second() > 0 || !format.isEmpty()) { - format.append(format.isEmpty() ? QStringLiteral("s") : QStringLiteral(":ss")); - } + format.append(format.isEmpty() ? QStringLiteral("m") : QStringLiteral(":mm")); + format.append(format.isEmpty() ? QStringLiteral("s") : QStringLiteral(":ss")); if (outTime) { *outTime = time; } return format; } QString MediaUtils::prettyDuration(int msecs) { QTime time; const QString format = prettyFormat(msecs, &time); return time.toString(format); } QString MediaUtils::prettyDuration(int msecs, int durationMsecs) { const QString format = prettyFormat(durationMsecs); return QTime(0, 0, 0).addMSecs(msecs).toString(format); } bool MediaUtils::isHttp(const QString &content) { const QUrl url(content); return url.isValid() && url.scheme().startsWith(QStringLiteral("http")); } bool MediaUtils::isGeoLocation(const QString &content) { return s_geoLocationRegExp.match(content).hasMatch(); } QGeoCoordinate MediaUtils::locationCoordinate(const QString &content) { const QRegularExpressionMatch match = s_geoLocationRegExp.match(content); return match.hasMatch() ? QGeoCoordinate(match.captured(1).toDouble(), match.captured(2).toDouble()) : QGeoCoordinate(); } bool MediaUtils::localFileAvailable(const QString &filePath) { if (filePath.isEmpty()) { return false; } const QUrl url(filePath); return url.isValid() && url.isLocalFile() ? localFileAvailable(url) : QFile::exists(filePath); } bool MediaUtils::localFileAvailable(const QUrl &url) { if (url.isValid() && url.isLocalFile()) { return localFileAvailable(url.toLocalFile()); } return false; } QUrl MediaUtils::fromLocalFile(const QString &filePath) { return QUrl::fromLocalFile(filePath); } QMimeType MediaUtils::mimeType(const QString &filePath) { if (filePath.isEmpty()) { return {}; } const QUrl url(filePath); return url.isValid() && !url.scheme().isEmpty() ? mimeType(url) : s_mimeDB.mimeTypeForFile(filePath); } QMimeType MediaUtils::mimeType(const QUrl &url) { if (!url.isValid()) { return {}; } if (url.isLocalFile()) { return mimeType(url.toLocalFile()); } if (url.scheme().compare(QStringLiteral("geo")) == 0) { return s_geoTypes.first(); } return mimeType(url.fileName()); } QString MediaUtils::iconName(const QString &filePath) { if (filePath.isEmpty()) { return {}; } const QUrl url(filePath); return url.isValid() && !url.scheme().isEmpty() ? iconName(url) : ::iconName({ mimeType(filePath) }); } QString MediaUtils::iconName(const QUrl &url) { if (!url.isValid()) { return {}; } if (url.isLocalFile()) { return iconName(url.toLocalFile()); } return iconName(url.fileName()); } QString MediaUtils::newMediaLabel(Enums::MessageType hint) { switch (hint) { case Enums::MessageType::MessageImage: return tr("Take picture"); case Enums::MessageType::MessageVideo: return tr("Record video"); case Enums::MessageType::MessageAudio: return tr("Record voice"); case Enums::MessageType::MessageGeoLocation: return tr("Send location"); case Enums::MessageType::MessageText: case Enums::MessageType::MessageFile: case Enums::MessageType::MessageDocument: case Enums::MessageType::MessageUnknown: break; } Q_UNREACHABLE(); + return { }; } QString MediaUtils::newMediaIconName(Enums::MessageType hint) { switch (hint) { case Enums::MessageType::MessageImage: return QStringLiteral("camera-photo-symbolic"); case Enums::MessageType::MessageVideo: return QStringLiteral("camera-video-symbolic"); case Enums::MessageType::MessageAudio: return QStringLiteral("microphone"); case Enums::MessageType::MessageGeoLocation: return QStringLiteral("gps"); case Enums::MessageType::MessageText: case Enums::MessageType::MessageFile: -// return QStringLiteral("file-search-symbolic"); case Enums::MessageType::MessageDocument: -// return QStringLiteral("document-open"); case Enums::MessageType::MessageUnknown: break; } Q_UNREACHABLE(); + return { }; } QString MediaUtils::label(Enums::MessageType hint) { switch (hint) { case Enums::MessageType::MessageFile: return tr("Choose file"); case Enums::MessageType::MessageImage: return tr("Choose image"); case Enums::MessageType::MessageVideo: return tr("Choose video"); case Enums::MessageType::MessageAudio: return tr("Choose audio file"); case Enums::MessageType::MessageDocument: return tr("Choose document"); case Enums::MessageType::MessageGeoLocation: case Enums::MessageType::MessageText: case Enums::MessageType::MessageUnknown: break; } Q_UNREACHABLE(); + return { }; } QString MediaUtils::iconName(Enums::MessageType hint) { return ::iconName(mimeTypes(hint)); } QString MediaUtils::filterName(Enums::MessageType hint) { switch (hint) { case Enums::MessageType::MessageText: break; case Enums::MessageType::MessageFile: return tr("All files"); case Enums::MessageType::MessageImage: return tr("Images"); case Enums::MessageType::MessageVideo: return tr("Videos"); case Enums::MessageType::MessageAudio: return tr("Audio files"); case Enums::MessageType::MessageDocument: return tr("Documents"); case Enums::MessageType::MessageGeoLocation: case Enums::MessageType::MessageUnknown: break; } Q_UNREACHABLE(); + return { }; } QString MediaUtils::filter(Enums::MessageType hint) { return ::filter(mimeTypes(hint)); } QString MediaUtils::namedFilter(Enums::MessageType hint) { switch (hint) { case Enums::MessageType::MessageText: break; case Enums::MessageType::MessageFile: case Enums::MessageType::MessageImage: case Enums::MessageType::MessageVideo: case Enums::MessageType::MessageAudio: case Enums::MessageType::MessageDocument: return tr("%1 (%2)").arg(filterName(hint), filter(hint)); case Enums::MessageType::MessageGeoLocation: case Enums::MessageType::MessageUnknown: break; } Q_UNREACHABLE(); + return { }; } QList MediaUtils::mimeTypes(Enums::MessageType hint) { switch (hint) { case Enums::MessageType::MessageImage: return s_imageTypes; case Enums::MessageType::MessageVideo: return s_videoTypes; case Enums::MessageType::MessageAudio: return s_audioTypes; case Enums::MessageType::MessageDocument: return s_documentTypes; case Enums::MessageType::MessageGeoLocation: return s_geoTypes; case Enums::MessageType::MessageText: case Enums::MessageType::MessageUnknown: break; case Enums::MessageType::MessageFile: return { s_mimeDB.mimeTypeForName(QStringLiteral("application/octet-stream")) }; } Q_UNREACHABLE(); + return { }; } Enums::MessageType MediaUtils::messageType(const QString &filePath) { if (filePath.isEmpty()) { return Enums::MessageType::MessageUnknown; } const QUrl url(filePath); return url.isValid() && !url.scheme().isEmpty() ? messageType(url) : messageType(s_mimeDB.mimeTypeForFile(filePath)); } Enums::MessageType MediaUtils::messageType(const QUrl &url) { if (!url.isValid()) { return Enums::MessageType::MessageUnknown; } if (url.isLocalFile()) { return messageType(url.toLocalFile()); } QList mimeTypes; if (url.scheme().compare(QStringLiteral("geo")) == 0) { mimeTypes = s_geoTypes; } else { const QFileInfo fileInfo(url.fileName()); if (fileInfo.completeSuffix().isEmpty()) { return Enums::MessageType::MessageUnknown; } mimeTypes = s_mimeDB.mimeTypesForFileName(fileInfo.fileName()); } for (const QMimeType &mimeType: mimeTypes) { const Enums::MessageType messageType = MediaUtils::messageType(mimeType); if (messageType != Enums::MessageType::MessageUnknown) { return messageType; } } return Enums::MessageType::MessageUnknown; } Enums::MessageType MediaUtils::messageType(const QMimeType &mimeType) { if (!mimeType.isValid()) { return Enums::MessageType::MessageUnknown; } if (mimeTypes(Enums::MessageType::MessageImage).contains(mimeType)) { return Enums::MessageType::MessageImage; } else if (mimeTypes(Enums::MessageType::MessageAudio).contains(mimeType)) { return Enums::MessageType::MessageAudio; } else if (mimeTypes(Enums::MessageType::MessageVideo).contains(mimeType)) { return Enums::MessageType::MessageVideo; } else if (mimeTypes(Enums::MessageType::MessageGeoLocation).contains(mimeType)) { return Enums::MessageType::MessageGeoLocation; } else if (mimeTypes(Enums::MessageType::MessageDocument).contains(mimeType)) { return Enums::MessageType::MessageDocument; } return Enums::MessageType::MessageFile; } diff --git a/src/MessageHandler.cpp b/src/MessageHandler.cpp index 0301c05..4ed0e6a 100644 --- a/src/MessageHandler.cpp +++ b/src/MessageHandler.cpp @@ -1,285 +1,296 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 Kaidan developers and contributors * (see the LICENSE file for a full list of copyright authors) * * Kaidan 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. * * In addition, as a special exception, the author of Kaidan gives * permission to link the code of its release with the OpenSSL * project's "OpenSSL" library (or with modified versions of it that * use the same license as the "OpenSSL" library), and distribute the * linked executables. You must obey the GNU General Public License in * all respects for all of the code used other than "OpenSSL". If you * modify this file, you may extend this exception to your version of * the file, but you are not obligated to do so. If you do not wish to * do so, delete this exception statement from your version. * * Kaidan 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 Kaidan. If not, see . */ #include "MessageHandler.h" // Qt #include #include #include // QXmpp #include #include #include #include #include // Kaidan #include "Kaidan.h" #include "Message.h" #include "MessageModel.h" #include "Notifications.h" #include "MediaUtils.h" MessageHandler::MessageHandler(Kaidan *kaidan, QXmppClient *client, MessageModel *model, QObject *parent) : QObject(parent), kaidan(kaidan), client(client), model(model) { connect(client, &QXmppClient::messageReceived, this, &MessageHandler::handleMessage); connect(kaidan, &Kaidan::sendMessage, this, &MessageHandler::sendMessage); connect(kaidan, &Kaidan::correctMessage, this, &MessageHandler::correctMessage); client->addExtension(&receiptManager); connect(&receiptManager, &QXmppMessageReceiptManager::messageDelivered, this, [=] (const QString&, const QString &id) { emit model->setMessageAsDeliveredRequested(id); }); carbonManager = new QXmppCarbonManager(); client->addExtension(carbonManager); // messages sent to our account (forwarded from another client) connect(carbonManager, &QXmppCarbonManager::messageReceived, client, &QXmppClient::messageReceived); // messages sent from our account (but another client) connect(carbonManager, &QXmppCarbonManager::messageSent, client, &QXmppClient::messageReceived); // carbons discovery auto *discoManager = client->findExtension(); if (!discoManager) return; connect(discoManager, &QXmppDiscoveryManager::infoReceived, this, &MessageHandler::handleDiscoInfo); } MessageHandler::~MessageHandler() { delete carbonManager; } void MessageHandler::handleMessage(const QXmppMessage &msg) { if (msg.body().isEmpty() || msg.type() == QXmppMessage::Error) return; Message message; message.setFrom(QXmppUtils::jidToBareJid(msg.from())); message.setTo(QXmppUtils::jidToBareJid(msg.to())); message.setSentByMe(msg.from() == client->configuration().jidBare()); message.setId(msg.id()); message.setBody(msg.body()); message.setMediaType(MessageType::MessageText); // default to text message without media #if (QXMPP_VERSION) >= QT_VERSION_CHECK(1, 1, 0) message.setIsSpoiler(msg.isSpoiler()); message.setSpoilerHint(msg.spoilerHint()); #else for (const QXmppElement &extension : msg.extensions()) { if (extension.tagName() == "spoiler" && extension.attribute("xmlns") == NS_SPOILERS) { message.setIsSpoiler(true); message.setSpoilerHint(extension.value()); break; } } #endif // check if message contains a link and also check out of band url QStringList bodyWords = message.body().split(" "); bodyWords.prepend(msg.outOfBandUrl()); for (const QString &word : qAsConst(bodyWords)) { - if (!MediaUtils::isHttp(word)) { + if (!MediaUtils::isHttp(word) && !MediaUtils::isGeoLocation(word)) { continue; } // check message type by file name in link // This is hacky, but needed without SIMS or an additional HTTP request. // Also, this can be useful when a user manually posts an HTTP url. const QUrl url(word); const QMimeType mimeType = MediaUtils::mimeType(url); const MessageType messageType = MediaUtils::messageType(mimeType); switch (messageType) { case MessageType::MessageImage: case MessageType::MessageAudio: case MessageType::MessageVideo: case MessageType::MessageDocument: case MessageType::MessageFile: + case MessageType::MessageGeoLocation: message.setMediaType(messageType); + if (messageType == MessageType::MessageGeoLocation) { + message.setMediaLocation(url.toEncoded()); + } message.setMediaContentType(mimeType.name()); message.setOutOfBandUrl(url.toEncoded()); break; case MessageType::MessageText: - case MessageType::MessageGeoLocation: case MessageType::MessageUnknown: continue; } break; // we can only handle one link } // get possible delay (timestamp) message.setStamp((msg.stamp().isNull() || !msg.stamp().isValid()) ? QDateTime::currentDateTimeUtc() : msg.stamp().toUTC()); // save the message to the database // in case of message correction, replace old message if (msg.replaceId().isEmpty()) { emit model->addMessageRequested(message); } else { message.setIsEdited(true); message.setId(QString()); emit model->updateMessageRequested(msg.replaceId(), [=] (Message &m) { // replace completely m = message; }); } // Send a message notification // The contact can differ if the message is really from a contact or just // a forward of another of the user's clients. QString contactJid = message.sentByMe() ? message.to() : message.from(); // resolve user-defined name of this JID QString contactName = client->rosterManager().getRosterEntry(contactJid).name(); if (contactName.isEmpty()) contactName = contactJid; if (!message.sentByMe()) Notifications::sendMessageNotification(contactJid, contactName, msg.body()); // TODO: Move back following call to RosterManager::handleMessage when spoiler // messages are implemented in QXmpp const QString lastMessage = message.isSpoiler() ? message.spoilerHint().isEmpty() ? tr("Spoiler") : message.spoilerHint() : msg.body(); emit kaidan->getRosterModel()->updateItemRequested( contactJid, [=] (RosterItem &item) { item.setLastMessage(lastMessage); } ); } void MessageHandler::sendMessage(const QString& toJid, const QString& body, bool isSpoiler, const QString& spoilerHint) { // TODO: Add offline message cache and send when connnected again if (client->state() != QXmppClient::ConnectedState) { emit kaidan->passiveNotificationRequested( tr("Could not send message, as a result of not being connected.") ); qWarning() << "[client] [MessageHandler] Could not send message, as a result of " "not being connected."; return; } Message msg; msg.setFrom(client->configuration().jidBare()); msg.setTo(toJid); msg.setBody(body); msg.setId(QXmppUtils::generateStanzaHash(28)); msg.setReceiptRequested(true); msg.setSentByMe(true); msg.setMediaType(MessageType::MessageText); // text message without media msg.setStamp(QDateTime::currentDateTimeUtc()); if (isSpoiler) { msg.setIsSpoiler(isSpoiler); msg.setSpoilerHint(spoilerHint); // parsing/serialization of spoilers isn't implemented in QXmpp QXmppElementList extensions = msg.extensions(); QXmppElement spoiler = QXmppElement(); spoiler.setTagName("spoiler"); spoiler.setValue(msg.spoilerHint()); spoiler.setAttribute("xmlns", NS_SPOILERS); extensions.append(spoiler); msg.setExtensions(extensions); + } else if (MediaUtils::isGeoLocation(msg.body())) { + const QUrl url(msg.body()); + const QMimeType mimeType = MediaUtils::mimeType(url); + const MessageType messageType = MediaUtils::messageType(mimeType); + msg.setMediaType(messageType); + msg.setMediaLocation(msg.body()); + msg.setMediaContentType(mimeType.name()); + msg.setOutOfBandUrl(msg.body()); } emit model->addMessageRequested(msg); if (client->sendPacket(static_cast(msg))) emit model->setMessageAsSentRequested(msg.id()); else emit kaidan->passiveNotificationRequested(tr("Message could not be sent.")); // TODO: handle error } void MessageHandler::correctMessage(const QString& toJid, const QString& msgId, const QString& body) { // TODO: load old message from model and put everything into the new message // instead of only the new body // TODO: Add offline message cache and send when connnected again if (client->state() != QXmppClient::ConnectedState) { emit kaidan->passiveNotificationRequested( tr("Could not correct message, as a result of not being connected.") ); qWarning() << "[client] [MessageHandler] Could not correct message, as a result of " "not being connected."; return; } Message msg; msg.setFrom(client->configuration().jidBare()); msg.setTo(toJid); msg.setId(QXmppUtils::generateStanzaHash(28)); msg.setBody(body); msg.setReceiptRequested(true); msg.setSentByMe(true); msg.setMediaType(MessageType::MessageText); // text message without media msg.setIsEdited(true); msg.setReplaceId(msgId); emit model->updateMessageRequested(msgId, [=] (Message &msg) { msg.setBody(body); }); if (client->sendPacket(msg)) emit model->setMessageAsSentRequested(msg.id()); else emit kaidan->passiveNotificationRequested( tr("Message correction was not successful.")); } void MessageHandler::handleDiscoInfo(const QXmppDiscoveryIq &info) { if (info.from() != client->configuration().domain()) return; // enable carbons, if feature found if (info.features().contains(NS_CARBONS)) carbonManager->setCarbonsEnabled(true); } diff --git a/src/QmlUtils.cpp b/src/QmlUtils.cpp index 224cac4..269e180 100644 --- a/src/QmlUtils.cpp +++ b/src/QmlUtils.cpp @@ -1,204 +1,207 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 Kaidan developers and contributors * (see the LICENSE file for a full list of copyright authors) * * Kaidan 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. * * In addition, as a special exception, the author of Kaidan gives * permission to link the code of its release with the OpenSSL * project's "OpenSSL" library (or with modified versions of it that * use the same license as the "OpenSSL" library), and distribute the * linked executables. You must obey the GNU General Public License in * all respects for all of the code used other than "OpenSSL". If you * modify this file, you may extend this exception to your version of * the file, but you are not obligated to do so. If you do not wish to * do so, delete this exception statement from your version. * * Kaidan 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 Kaidan. If not, see . */ #include "QmlUtils.h" // Qt #include #include #include #include #include #include #include #include #include #include // QXmpp #include "qxmpp-exts/QXmppColorGenerator.h" QmlUtils::QmlUtils(QObject *parent) : QObject(parent) { } QString QmlUtils::presenceTypeToIcon(Enums::AvailabilityTypes type) { switch (type) { case AvailabilityTypes::PresOnline: return "im-user-online"; case AvailabilityTypes::PresChat: return "im-user-online"; case AvailabilityTypes::PresAway: return "im-user-away"; case AvailabilityTypes::PresDND: return "im-kick-user"; case AvailabilityTypes::PresXA: return "im-user-away"; case AvailabilityTypes::PresUnavailable: return "im-user-offline"; case AvailabilityTypes::PresError: return "im-ban-kick-user"; case AvailabilityTypes::PresInvisible: return "im-invisible-user"; } Q_UNREACHABLE(); + return { }; } QString QmlUtils::presenceTypeToText(AvailabilityTypes type) { switch (type) { case AvailabilityTypes::PresOnline: return tr("Available"); case AvailabilityTypes::PresChat: return tr("Free for chat"); case AvailabilityTypes::PresAway: return tr("Away"); case AvailabilityTypes::PresDND: return tr("Do not disturb"); case AvailabilityTypes::PresXA: return tr("Away for longer"); case AvailabilityTypes::PresUnavailable: return tr("Offline"); case AvailabilityTypes::PresError: return tr("Error"); case AvailabilityTypes::PresInvisible: return tr("Invisible"); } Q_UNREACHABLE(); + return { }; } QColor QmlUtils::presenceTypeToColor(AvailabilityTypes type) { switch (type) { case AvailabilityTypes::PresOnline: return {"green"}; case AvailabilityTypes::PresChat: return {"darkgreen"}; case AvailabilityTypes::PresAway: return {"orange"}; case AvailabilityTypes::PresDND: return QColor::fromRgb(218, 68, 83); case AvailabilityTypes::PresXA: return {"orange"}; case AvailabilityTypes::PresError: return {"red"}; case AvailabilityTypes::PresUnavailable: return {"silver"}; case AvailabilityTypes::PresInvisible: return {"grey"}; } Q_UNREACHABLE(); + return { }; } QString QmlUtils::getResourcePath(const QString &name) { // We generally prefer to first search for files in application resources if (QFile::exists(":/" + name)) return QString("qrc:/" + name); // list of file paths where to search for the resource file QStringList pathList; // add relative path from binary (only works if installed) pathList << QCoreApplication::applicationDirPath() + QString("/../share/") + QString(APPLICATION_NAME); // get the standard app data locations for current platform pathList << QStandardPaths::standardLocations(QStandardPaths::AppDataLocation); #ifdef UBUNTU_TOUCH pathList << QString("./share/") + QString(APPLICATION_NAME); #endif #ifndef NDEBUG #ifdef DEBUG_SOURCE_PATH // add source directory (only for debug builds) pathList << QString(DEBUG_SOURCE_PATH) + QString("/data"); #endif #endif // search for file in directories for (int i = 0; i < pathList.size(); i++) { // open directory QDir directory(pathList.at(i)); // look up the file if (directory.exists(name)) { // found the file, return the path return QUrl::fromLocalFile(directory.absoluteFilePath(name)).toString(); } } // no file found qWarning() << "[main] Could NOT find media file:" << name; return QString(); } bool QmlUtils::isImageFile(const QUrl &fileUrl) { QMimeType type = QMimeDatabase().mimeTypeForUrl(fileUrl); return type.inherits("image/jpeg") || type.inherits("image/png"); } void QmlUtils::copyToClipboard(const QString &text) { QGuiApplication::clipboard()->setText(text); } QString QmlUtils::fileNameFromUrl(const QUrl &url) { return QUrl(url).fileName(); } QString QmlUtils::fileSizeFromUrl(const QUrl &url) { return QLocale::system().formattedDataSize( QFileInfo(QUrl(url).toLocalFile()).size()); } QString QmlUtils::formatMessage(const QString &message) { // escape all special XML chars (like '<' and '>') // and spilt into words for processing return processMsgFormatting(message.toHtmlEscaped().split(" ")); } QColor QmlUtils::getUserColor(const QString &nickName) { QXmppColorGenerator::RGBColor color = QXmppColorGenerator::generateColor(nickName); return {color.red, color.green, color.blue}; } QString QmlUtils::processMsgFormatting(const QStringList &list, bool isFirst) { if (list.isEmpty()) return QString(); // link highlighting if (list.first().startsWith("https://") || list.first().startsWith("http://")) return (isFirst ? QString() : " ") + QString("%1").arg(list.first()) + processMsgFormatting(list.mid(1), false); return (isFirst ? QString() : " ") + list.first() + processMsgFormatting(list.mid(1), false); } diff --git a/src/main.cpp b/src/main.cpp index de3a772..546565f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,318 +1,355 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 Kaidan developers and contributors * (see the LICENSE file for a full list of copyright authors) * * Kaidan 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. * * In addition, as a special exception, the author of Kaidan gives * permission to link the code of its release with the OpenSSL * project's "OpenSSL" library (or with modified versions of it that * use the same license as the "OpenSSL" library), and distribute the * linked executables. You must obey the GNU General Public License in * all respects for all of the code used other than "OpenSSL". If you * modify this file, you may extend this exception to your version of * the file, but you are not obligated to do so. If you do not wish to * do so, delete this exception statement from your version. * * Kaidan 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 Kaidan. If not, see . */ // Qt #include #include #include #include #include #include #include #include #include #include // QXmpp #include "qxmpp-exts/QXmppUploadManager.h" #include // Kaidan #include "AvatarFileStorage.h" #include "EmojiModel.h" #include "Enums.h" #include "Globals.h" #include "Kaidan.h" #include "Message.h" #include "MessageModel.h" #include "PresenceCache.h" #include "QmlUtils.h" #include "RosterModel.h" #include "RosterFilterProxyModel.h" #include "StatusBar.h" #include "UploadManager.h" #include "EmojiModel.h" #include "Utils.h" #include "QrCodeScannerFilter.h" #include "VCardModel.h" +#include "CameraModel.h" +#include "AudioDeviceModel.h" +#include "MediaSettingModel.h" #include "MediaUtils.h" +#include "MediaRecorder.h" #ifdef STATIC_BUILD #include "static_plugins.h" #endif #ifndef QAPPLICATION_CLASS #define QAPPLICATION_CLASS QApplication #endif #include QT_STRINGIFY(QAPPLICATION_CLASS) #if !defined(Q_OS_IOS) && !defined(Q_OS_ANDROID) // SingleApplication (Qt5 replacement for QtSingleApplication) #include "singleapp/singleapplication.h" #endif #ifdef STATIC_BUILD #define KIRIGAMI_BUILD_TYPE_STATIC #include "./3rdparty/kirigami/src/kirigamiplugin.h" #endif #ifdef Q_OS_ANDROID #include #endif #ifdef Q_OS_WIN #include #endif enum CommandLineParseResult { CommandLineOk, CommandLineError, CommandLineVersionRequested, CommandLineHelpRequested }; CommandLineParseResult parseCommandLine(QCommandLineParser &parser, QString *errorMessage) { // application description parser.setApplicationDescription(QString(APPLICATION_DISPLAY_NAME) + " - " + QString(APPLICATION_DESCRIPTION)); // add all possible arguments QCommandLineOption helpOption = parser.addHelpOption(); QCommandLineOption versionOption = parser.addVersionOption(); parser.addOption({"disable-xml-log", "Disable output of full XMPP XML stream."}); parser.addOption({{"m", "multiple"}, "Allow multiple instances to be started."}); parser.addPositionalArgument("xmpp-uri", "An XMPP-URI to open (i.e. join a chat).", "[xmpp-uri]"); // parse arguments if (!parser.parse(QGuiApplication::arguments())) { *errorMessage = parser.errorText(); return CommandLineError; } // check for special cases if (parser.isSet(versionOption)) return CommandLineVersionRequested; if (parser.isSet(helpOption)) return CommandLineHelpRequested; // if nothing special happened, return OK return CommandLineOk; } Q_DECL_EXPORT int main(int argc, char *argv[]) { #ifdef Q_OS_WIN if (AttachConsole(ATTACH_PARENT_PROCESS)) { freopen("CONOUT$", "w", stdout); freopen("CONOUT$", "w", stderr); } #endif // initialize random generator qsrand(time(nullptr)); // // App // #ifdef UBUNTU_TOUCH qputenv("QT_AUTO_SCREEN_SCALE_FACTOR", "true"); qputenv("QT_QUICK_CONTROLS_MOBILE", "true"); #endif #ifdef APPIMAGE qputenv("OPENSSL_CONF", ""); #endif // name, display name, description QGuiApplication::setApplicationName(APPLICATION_NAME); QGuiApplication::setApplicationDisplayName(APPLICATION_DISPLAY_NAME); QGuiApplication::setApplicationVersion(VERSION_STRING); // attributes QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); // create a qt app #if defined(Q_OS_IOS) || defined(Q_OS_ANDROID) QGuiApplication app(argc, argv); #else SingleApplication app(argc, argv, true); #endif // register qMetaTypes qRegisterMetaType("RosterItem"); qRegisterMetaType("RosterModel*"); qRegisterMetaType("Message"); qRegisterMetaType("MessageModel*"); qRegisterMetaType("AvatarFileStorage*"); qRegisterMetaType("PresenceCache*"); qRegisterMetaType("QXmppPresence"); qRegisterMetaType("Credentials"); qRegisterMetaType("Qt::ApplicationState"); qRegisterMetaType("QXmppClient::State"); qRegisterMetaType("MessageType"); qRegisterMetaType("DisconnectionReason"); qRegisterMetaType("TransferJob*"); qRegisterMetaType("QmlUtils*"); qRegisterMetaType>("QVector"); qRegisterMetaType>("QVector"); qRegisterMetaType>("QHash"); qRegisterMetaType>("std::function"); qRegisterMetaType>("std::function"); qRegisterMetaType("ClientWorker::Credentials"); qRegisterMetaType("QXmppVCardIq"); qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); +// qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); // Enums for c++ member calls using enums qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); // Qt-Translator QTranslator qtTranslator; qtTranslator.load("qt_" + QLocale::system().name(), QLibraryInfo::location(QLibraryInfo::TranslationsPath)); QCoreApplication::installTranslator(&qtTranslator); // Kaidan-Translator QTranslator kaidanTranslator; // load the systems locale or none from resources kaidanTranslator.load(QLocale::system().name(), ":/i18n"); QCoreApplication::installTranslator(&kaidanTranslator); // // Command line arguments // // create parser and add a description QCommandLineParser parser; // parse the arguments QString commandLineErrorMessage; switch (parseCommandLine(parser, &commandLineErrorMessage)) { case CommandLineError: qWarning() << commandLineErrorMessage; return 1; case CommandLineVersionRequested: parser.showVersion(); return 0; case CommandLineHelpRequested: parser.showHelp(); return 0; case CommandLineOk: break; } #if !defined(Q_OS_IOS) && !defined(Q_OS_ANDROID) // check if another instance already runs if (app.isSecondary() && !parser.isSet("multiple")) { qDebug().noquote() << QString("Another instance of %1 is already running.") .arg(APPLICATION_DISPLAY_NAME) << "You can enable multiple instances by specifying '--multiple'."; // send a possible link to the primary instance if (!parser.positionalArguments().isEmpty()) app.sendMessage(parser.positionalArguments().first().toUtf8()); return 0; } #endif // // Kaidan back-end // Kaidan kaidan(&app, !parser.isSet("disable-xml-log")); #if !defined(Q_OS_IOS) && !defined(Q_OS_ANDROID) // receive messages from other instances of Kaidan Kaidan::connect(&app, &SingleApplication::receivedMessage, &kaidan, &Kaidan::receiveMessage); #endif // open the XMPP-URI/link (if given) if (!parser.positionalArguments().isEmpty()) kaidan.addOpenUri(parser.positionalArguments().first()); // // QML-GUI // if (QIcon::themeName().isEmpty()) { QIcon::setThemeName("breeze"); } QQmlApplicationEngine engine; // QtQuickControls2 Style if (qEnvironmentVariableIsEmpty("QT_QUICK_CONTROLS_STYLE")) { #ifdef Q_OS_WIN const QString defaultStyle = QStringLiteral("Universal"); #else const QString defaultStyle = QStringLiteral("Material"); #endif qDebug() << "QT_QUICK_CONTROLS_STYLE not set, setting to" << defaultStyle; qputenv("QT_QUICK_CONTROLS_STYLE", defaultStyle.toLatin1()); } // QML type bindings #ifdef STATIC_BUILD KirigamiPlugin::getInstance().registerTypes(); #endif qmlRegisterType("StatusBar", 0, 1, "StatusBar"); qmlRegisterType("EmojiModel", 0, 1, "EmojiModel"); qmlRegisterType("EmojiModel", 0, 1, "EmojiProxyModel"); qmlRegisterType(APPLICATION_ID, 1, 0, "QrCodeScannerFilter"); qmlRegisterType(APPLICATION_ID, 1, 0, "VCardModel"); qmlRegisterType(APPLICATION_ID, 1, 0, "RosterFilterProxyModel"); + qmlRegisterType(APPLICATION_ID, 1, 0, "CameraModel"); + qmlRegisterType(APPLICATION_ID, 1, 0, "AudioDeviceModel"); + qmlRegisterType(APPLICATION_ID, 1, 0, "MediaSettingsContainerModel"); + qmlRegisterType(APPLICATION_ID, 1, 0, "MediaSettingsResolutionModel"); + qmlRegisterType(APPLICATION_ID, 1, 0, "MediaSettingsQualityModel"); + qmlRegisterType(APPLICATION_ID, 1, 0, "MediaSettingsImageCodecModel"); + qmlRegisterType(APPLICATION_ID, 1, 0, "MediaSettingsAudioCodecModel"); + qmlRegisterType(APPLICATION_ID, 1, 0, "MediaSettingsAudioSampleRateModel"); + qmlRegisterType(APPLICATION_ID, 1, 0, "MediaSettingsVideoCodecModel"); + qmlRegisterType(APPLICATION_ID, 1, 0, "MediaSettingsVideoFrameRateModel"); + qmlRegisterType(APPLICATION_ID, 1, 0, "MediaRecorder"); qmlRegisterUncreatableType("EmojiModel", 0, 1, "QAbstractItemModel", "Used by proxy models"); qmlRegisterUncreatableType("EmojiModel", 0, 1, "Emoji", "Used by emoji models"); qmlRegisterUncreatableType(APPLICATION_ID, 1, 0, "TransferJob", "TransferJob type usable"); qmlRegisterUncreatableType(APPLICATION_ID, 1, 0, "QMimeType", "QMimeType type usable"); + qmlRegisterUncreatableType(APPLICATION_ID, 1, 0, "CameraInfo", "CameraInfo type usable"); + qmlRegisterUncreatableType(APPLICATION_ID, 1, 0, "AudioDeviceInfo", "AudioDeviceInfo type usable"); + qmlRegisterUncreatableType(APPLICATION_ID, 1, 0, "MediaSettings", "MediaSettings type usable"); + qmlRegisterUncreatableType(APPLICATION_ID, 1, 0, "CommonEncoderSettings", "CommonEncoderSettings type usable"); + qmlRegisterUncreatableType(APPLICATION_ID, 1, 0, "ImageEncoderSettings", "ImageEncoderSettings type usable"); + qmlRegisterUncreatableType(APPLICATION_ID, 1, 0, "AudioEncoderSettings", "AudioEncoderSettings type usable"); + qmlRegisterUncreatableType(APPLICATION_ID, 1, 0, "VideoEncoderSettings", "VideoEncoderSettings type usable"); qmlRegisterUncreatableMetaObject(Enums::staticMetaObject, APPLICATION_ID, 1, 0, "Enums", "Can't create object; only enums defined!"); qmlRegisterSingletonType("MediaUtils", 0, 1, "MediaUtilsInstance", [](QQmlEngine *, QJSEngine *) { QObject *instance = new MediaUtils(qApp); return instance; }); engine.rootContext()->setContextProperty("kaidan", &kaidan); engine.load(QUrl("qrc:/qml/main.qml")); if(engine.rootObjects().isEmpty()) return -1; #ifdef Q_OS_ANDROID QtAndroid::hideSplashScreen(); #endif // enter qt main loop return app.exec(); } diff --git a/src/qml/ChatPage.qml b/src/qml/ChatPage.qml index de5e626..7a6ed12 100644 --- a/src/qml/ChatPage.qml +++ b/src/qml/ChatPage.qml @@ -1,410 +1,480 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 Kaidan developers and contributors * (see the LICENSE file for a full list of copyright authors) * * Kaidan 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. * * In addition, as a special exception, the author of Kaidan gives * permission to link the code of its release with the OpenSSL * project's "OpenSSL" library (or with modified versions of it that * use the same license as the "OpenSSL" library), and distribute the * linked executables. You must obey the GNU General Public License in * all respects for all of the code used other than "OpenSSL". If you * modify this file, you may extend this exception to your version of * the file, but you are not obligated to do so. If you do not wish to * do so, delete this exception statement from your version. * * Kaidan 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 Kaidan. If not, see . */ import QtQuick 2.6 import QtQuick.Layouts 1.3 import QtGraphicalEffects 1.0 import QtQuick.Controls 2.0 as Controls +import QtMultimedia 5.10 as Multimedia import org.kde.kirigami 2.2 as Kirigami import im.kaidan.kaidan 1.0 import EmojiModel 0.1 import MediaUtils 0.1 import "elements" Kirigami.ScrollablePage { + id: root + property string chatName property bool isWritingSpoiler property string messageToCorrect + readonly property bool cameraAvailable: Multimedia.QtMultimedia.availableCameras.length > 0 + title: chatName keyboardNavigationEnabled: true contextualActions: [ Kirigami.Action { visible: !isWritingSpoiler icon.name: "password-show-off" text: qsTr("Send a spoiler message") onTriggered: isWritingSpoiler = true }, Kirigami.Action { visible: true icon.name: { kaidan.notificationsMuted(kaidan.messageModel.chatPartner) ? "player-volume" : "audio-volume-muted-symbolic" } text: { kaidan.notificationsMuted(kaidan.messageModel.chatPartner) ? qsTr("Unmute notifications") : qsTr("Mute notifications") } onTriggered: { kaidan.setNotificationsMuted( kaidan.messageModel.chatPartner, !kaidan.notificationsMuted(kaidan.messageModel.chatPartner) ) } function handleNotificationsMuted(jid) { text = kaidan.notificationsMuted(kaidan.messageModel.chatPartner) ? qsTr("Unmute notifications") : qsTr("Mute notifications") icon.name = kaidan.notificationsMuted(kaidan.messageModel.chatPartner) ? "player-volume" : "audio-volume-muted-symbolic" } Component.onCompleted: { kaidan.notificationsMutedChanged.connect(handleNotificationsMuted) } Component.onDestruction: { kaidan.notificationsMutedChanged.disconnect(handleNotificationsMuted) } }, Kirigami.Action { visible: true icon.name: "user-identity" text: qsTr("View profile") onTriggered: pageStack.push(userProfilePage, {jid: kaidan.messageModel.chatPartner, name: chatName}) + }, + Kirigami.Action { + text: qsTr("Multimedia settings") + + icon { + name: "settings-configure" + } + + onTriggered: { + pageStack.push(multimediaSettingsPage, {jid: kaidan.messageModel.chatPartner, name: chatName}) + } + }, + Kirigami.Action { + readonly property int type: Enums.MessageType.MessageImage + + text: MediaUtilsInstance.newMediaLabel(type) + enabled: root.cameraAvailable + + icon { + name: MediaUtilsInstance.newMediaIconName(type) + } + + onTriggered: { + sendMediaSheet.sendNewMessageType(kaidan.messageModel.chatPartner, type) + } + }, + Kirigami.Action { + readonly property int type: Enums.MessageType.MessageAudio + + text: MediaUtilsInstance.newMediaLabel(type) + + icon { + name: MediaUtilsInstance.newMediaIconName(type) + } + + onTriggered: { + sendMediaSheet.sendNewMessageType(kaidan.messageModel.chatPartner, type) + } + }, + Kirigami.Action { + readonly property int type: Enums.MessageType.MessageVideo + + text: MediaUtilsInstance.newMediaLabel(type) + enabled: root.cameraAvailable + + icon { + name: MediaUtilsInstance.newMediaIconName(type) + } + + onTriggered: { + sendMediaSheet.sendNewMessageType(kaidan.messageModel.chatPartner, type) + } + }, + Kirigami.Action { + readonly property int type: Enums.MessageType.MessageGeoLocation + + text: MediaUtilsInstance.newMediaLabel(type) + + icon { + name: MediaUtilsInstance.newMediaIconName(type) + } + + onTriggered: { + sendMediaSheet.sendNewMessageType(kaidan.messageModel.chatPartner, type) + } } ] SendMediaSheet { id: sendMediaSheet } FileChooser { id: fileChooser title: qsTr("Select a file") onAccepted: sendMediaSheet.sendFile(kaidan.messageModel.chatPartner, fileUrl) } function openFileDialog(filterName, filter, title) { fileChooser.filterName = filterName fileChooser.filter = filter if (title !== undefined) fileChooser.title = title fileChooser.open() mediaDrawer.close() } Kirigami.OverlayDrawer { id: mediaDrawer edge: Qt.BottomEdge height: Kirigami.Units.gridUnit * 8 contentItem: ListView { id: content orientation: Qt.Horizontal Layout.fillHeight: true Layout.fillWidth: true model: [ Enums.MessageType.MessageFile, Enums.MessageType.MessageImage, Enums.MessageType.MessageAudio, Enums.MessageType.MessageVideo, Enums.MessageType.MessageDocument ] delegate: IconButton { height: ListView.view.height width: height buttonText: MediaUtilsInstance.label(model.modelData) iconSource: MediaUtilsInstance.iconName(model.modelData) onClicked: { switch (model.modelData) { case Enums.MessageType.MessageFile: case Enums.MessageType.MessageImage: case Enums.MessageType.MessageAudio: case Enums.MessageType.MessageVideo: case Enums.MessageType.MessageDocument: openFileDialog(MediaUtilsInstance.filterName(model.modelData), MediaUtilsInstance.filter(model.modelData), MediaUtilsInstance.label(model.modelData)) break case Enums.MessageType.MessageText: case Enums.MessageType.MessageGeoLocation: case Enums.MessageType.MessageUnknown: break } } } } } background: Image { id: bgimage source: kaidan.utils.getResourcePath("images/chat.png") anchors.fill: parent fillMode: Image.Tile horizontalAlignment: Image.AlignLeft verticalAlignment: Image.AlignTop } // Chat ListView { verticalLayoutDirection: ListView.BottomToTop spacing: Kirigami.Units.smallSpacing * 2 // connect the database model: kaidan.messageModel delegate: ChatMessage { msgId: model.id sender: model.sender sentByMe: model.sentByMe messageBody: model.body dateTime: new Date(model.timestamp) isDelivered: model.isDelivered name: chatName mediaType: model.mediaType mediaGetUrl: model.mediaUrl mediaLocation: model.mediaLocation edited: model.isEdited isSpoiler: model.isSpoiler isShowingSpoiler: false spoilerHint: model.spoilerHint onMessageEditRequested: { messageToCorrect = id messageField.text = body messageField.state = "edit" } } } // Message Writing footer: Controls.Pane { id: sendingArea layer.enabled: sendingArea.enabled layer.effect: DropShadow { verticalOffset: 1 color: Kirigami.Theme.disabledTextColor samples: 20 spread: 0.3 cached: true // element is static } padding: 0 wheelEnabled: true background: Rectangle { color: Kirigami.Theme.backgroundColor } RowLayout { anchors.fill: parent Layout.preferredHeight: Kirigami.Units.gridUnit * 3 Controls.ToolButton { id: attachButton visible: kaidan.uploadServiceFound Layout.preferredWidth: Kirigami.Units.gridUnit * 3 Layout.preferredHeight: Kirigami.Units.gridUnit * 3 padding: 0 Kirigami.Icon { source: "document-send-symbolic" isMask: true smooth: true anchors.centerIn: parent width: Kirigami.Units.gridUnit * 2 height: width } onClicked: { if (Kirigami.Settings.isMobile) mediaDrawer.open() else openFileDialog(qsTr("All files"), "*", MediaUtilsInstance.label(Enums.MessageType.MessageFile)) } } ColumnLayout { Layout.minimumHeight: messageField.height + Kirigami.Units.smallSpacing * 2 Layout.fillWidth: true spacing: 0 RowLayout { visible: isWritingSpoiler Controls.TextArea { id: spoilerHintField Layout.fillWidth: true placeholderText: qsTr("Spoiler hint") wrapMode: Controls.TextArea.Wrap selectByMouse: true background: Item {} } Controls.ToolButton { Layout.preferredWidth: Kirigami.Units.gridUnit * 1.5 Layout.preferredHeight: Kirigami.Units.gridUnit * 1.5 padding: 0 Kirigami.Icon { source: "tab-close" smooth: true anchors.centerIn: parent width: Kirigami.Units.gridUnit * 1.5 height: width } onClicked: { isWritingSpoiler = false spoilerHintField.text = "" } } } Kirigami.Separator { visible: isWritingSpoiler Layout.fillWidth: true } Controls.TextArea { id: messageField Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter placeholderText: qsTr("Compose message") wrapMode: Controls.TextArea.Wrap selectByMouse: true background: Item {} state: "compose" states: [ State { name: "compose" }, State { name: "edit" } ] Keys.onReturnPressed: { if (event.key === Qt.Key_Return) { if (event.modifiers & Qt.ControlModifier) { messageField.append("") } else { sendButton.onClicked() event.accepted = true } } } } } EmojiPicker { x: -width + parent.width y: -height - 16 width: Kirigami.Units.gridUnit * 20 height: Kirigami.Units.gridUnit * 15 id: emojiPicker model: EmojiProxyModel { group: Emoji.Group.People sourceModel: EmojiModel {} } textArea: messageField } Controls.ToolButton { id: emojiPickerButton Layout.preferredWidth: Kirigami.Units.gridUnit * 3 Layout.preferredHeight: Kirigami.Units.gridUnit * 3 padding: 0 Kirigami.Icon { source: "face-smile" enabled: sendButton.enabled isMask: false smooth: true anchors.centerIn: parent width: Kirigami.Units.gridUnit * 2 height: width } onClicked: emojiPicker.visible ? emojiPicker.close() : emojiPicker.open() } Controls.ToolButton { id: sendButton Layout.preferredWidth: Kirigami.Units.gridUnit * 3 Layout.preferredHeight: Kirigami.Units.gridUnit * 3 padding: 0 Kirigami.Icon { source: { if (messageField.state == "compose") return "document-send" else if (messageField.state == "edit") return "edit-symbolic" } enabled: sendButton.enabled isMask: true smooth: true anchors.centerIn: parent width: Kirigami.Units.gridUnit * 2 height: width } onClicked: { // don't send empty messages if (!messageField.text.length) { return } // disable the button to prevent sending // the same message several times sendButton.enabled = false // send the message if (messageField.state == "compose") { kaidan.sendMessage( kaidan.messageModel.chatPartner, messageField.text, isWritingSpoiler, spoilerHintField.text ) } else if (messageField.state == "edit") { kaidan.correctMessage( kaidan.messageModel.chatPartner, messageToCorrect, messageField.text ) } // clean up the text fields messageField.text = "" messageField.state = "compose" spoilerHintField.text = "" isWritingSpoiler = false messageToCorrect = '' // reenable the button sendButton.enabled = true } } } } } diff --git a/src/qml/MultimediaSettingsPage.qml b/src/qml/MultimediaSettingsPage.qml new file mode 100644 index 0000000..e0390bc --- /dev/null +++ b/src/qml/MultimediaSettingsPage.qml @@ -0,0 +1,650 @@ +/* + * Kaidan - A user-friendly XMPP client for every device! + * + * Copyright (C) 2016-2019 Kaidan developers and contributors + * (see the LICENSE file for a full list of copyright authors) + * + * Kaidan 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. + * + * In addition, as a special exception, the author of Kaidan gives + * permission to link the code of its release with the OpenSSL + * project's "OpenSSL" library (or with modified versions of it that + * use the same license as the "OpenSSL" library), and distribute the + * linked executables. You must obey the GNU General Public License in + * all respects for all of the code used other than "OpenSSL". If you + * modify this file, you may extend this exception to your version of + * the file, but you are not obligated to do so. If you do not wish to + * do so, delete this exception statement from your version. + * + * Kaidan 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 Kaidan. If not, see . + */ + +import QtQuick 2.6 +import QtQuick.Layouts 1.3 +import QtQuick.Controls 2.4 as Controls +import QtMultimedia 5.10 as Multimedia +import org.kde.kirigami 2.2 as Kirigami + +import im.kaidan.kaidan 1.0 +import MediaUtils 0.1 + +Kirigami.Page { + id: root + + title: qsTr("Multimedia Settings") + topPadding: 0 + rightPadding: 0 + bottomPadding: 0 + leftPadding: 0 + + Timer { + id: pageTimer + interval: 10 + + onTriggered: { + if (!root.isCurrentPage) { + // Close the current page if it's not the current one after 10ms + pageStack.pop() + } + + // Stop the timer regardless of whether the page was closed or not + pageTimer.stop() + } + } + + onIsCurrentPageChanged: { + /* + * Start the timer if we are getting or loosing focus. + * Probably due to some kind of animation, isCurrentPage changes a few ms after + * this has been triggered. + */ + pageTimer.start() + } + + MediaRecorder { + id: recorder + } + + ColumnLayout { + id: mainLayout + + anchors { + fill: parent + leftMargin: 20 + topMargin: 5 + rightMargin: 20 + bottomMargin: 5 + } + + Controls.Label { + text: qsTr('Configure') + + Layout.fillWidth: true + } + + Controls.ComboBox { + id: recorderTypesComboBox + + model: ListModel { + ListElement { + label: qsTr('Image Capture') + type: MediaRecorder.Type.Image + } + + ListElement { + label: qsTr('Audio Recording') + type: MediaRecorder.Type.Audio + } + + ListElement { + label: qsTr('Video Recording') + type: MediaRecorder.Type.Video + } + } + + currentIndex: { + switch (recorder.type) { + case MediaRecorder.Type.Invalid: + break + case MediaRecorder.Type.Image: + return 0 + case MediaRecorder.Type.Audio: + return 1 + case MediaRecorder.Type.Video: + return 2 + } + + return -1 + } + + textRole: 'label' + delegate: Controls.RadioDelegate { + text: model.label + width: recorderTypesComboBox.width + highlighted: recorderTypesComboBox.highlightedIndex === model.index + checked: recorderTypesComboBox.currentIndex === model.index + } + + Layout.fillWidth: true + + onActivated: { + recorder.type = model.get(index).type + _updateTabs() + } + + onCurrentIndexChanged: { + _updateTabs() + } + + function _updateTabs() { + for (var i = 0; i < bar.contentChildren.length; ++i) { + if (bar.contentChildren[i].enabled) { + bar.currentIndex = i + return + } + } + } + } + + Controls.Label { + id: cameraLabel + + text: qsTr('Camera') + visible: recorder.type === MediaRecorder.Type.Image + || recorder.type === MediaRecorder.Type.Video + } + + Controls.ComboBox { + id: camerasComboBox + + visible: cameraLabel.visible + model: recorder.cameraModel + currentIndex: model.currentIndex + displayText: model.currentCamera.description + delegate: Controls.RadioDelegate { + text: model.description + width: camerasComboBox.width + highlighted: camerasComboBox.highlightedIndex === model.index + checked: camerasComboBox.currentIndex === model.index + } + + Layout.fillWidth: true + + onActivated: { + recorder.mediaSettings.camera = model.camera(index) + } + } + + Controls.Label { + id: audioInputLabel + + text: qsTr('Audio input') + visible: recorder.type === MediaRecorder.Type.Audio + + Layout.fillWidth: true + } + + Controls.ComboBox { + id: audioInputsComboBox + + visible: audioInputLabel.visible + model: recorder.audioDeviceModel + currentIndex: model.currentIndex + displayText: model.currentAudioDevice.description + delegate: Controls.RadioDelegate { + text: model.description + width: audioInputsComboBox.width + highlighted: audioInputsComboBox.highlightedIndex === model.index + checked: audioInputsComboBox.currentIndex === model.index + } + + Layout.fillWidth: true + + onActivated: { + recorder.mediaSettings.audioInputDevice = model.audioDevice(index) + } + } + + Controls.Label { + id: containerLabel + + text: qsTr('Container') + visible: recorder.type === MediaRecorder.Type.Audio + || recorder.type === MediaRecorder.Type.Video + + Layout.fillWidth: true + } + + Controls.ComboBox { + id: containersComboBox + + visible: containerLabel.visible + model: recorder.containerModel + currentIndex: model ? model.currentIndex : -1 + displayText: model ? model.currentDescription : '' + delegate: Controls.RadioDelegate { + text: model.description + width: containersComboBox.width + highlighted: containersComboBox.highlightedIndex === model.index + checked: containersComboBox.currentIndex === model.index + } + + Layout.fillWidth: true + + onActivated: { + recorder.mediaSettings.container = model.value(index) + } + } + + Item { + Layout.preferredHeight: parent.spacing * 2 + Layout.fillWidth: true + } + + Controls.TabBar { + id: bar + + Layout.fillWidth: true + + Controls.TabButton { + text: qsTr('Image') + enabled: recorder.type === MediaRecorder.Type.Image + } + + Controls.TabButton { + text: qsTr('Audio') + enabled: recorder.type === MediaRecorder.Type.Audio + || recorder.type === MediaRecorder.Type.Video + } + + Controls.TabButton { + text: qsTr('Video') + enabled: recorder.type === MediaRecorder.Type.Video + } + } + + Controls.ScrollView { + id: scrollView + + Layout.fillWidth: true + Layout.leftMargin: 5 + Layout.rightMargin: 5 + + Controls.SwipeView { + id: swipeView + + implicitWidth: scrollView.width + currentIndex: bar.currentIndex + enabled: recorder.isReady + interactive: false + clip: true + + ColumnLayout { + id: imageTab + + enabled: bar.contentChildren[Controls.SwipeView.index].enabled + + Controls.Label { + text: qsTr('Codec') + + Layout.fillWidth: true + } + + Controls.ComboBox { + id: imageCodecsComboBox + + model: recorder.imageCodecModel + currentIndex: model.currentIndex + displayText: model.currentDescription + delegate: Controls.RadioDelegate { + text: model.description + width: imageCodecsComboBox.width + highlighted: imageCodecsComboBox.highlightedIndex === model.index + checked: imageCodecsComboBox.currentIndex === model.index + } + + Layout.fillWidth: true + + onActivated: { + recorder.imageEncoderSettings.codec = model.value(index) + } + } + + Controls.Label { + text: qsTr('Resolution') + + Layout.fillWidth: true + } + + Controls.ComboBox { + id: imageResolutionsComboBox + + model: recorder.imageResolutionModel + currentIndex: model.currentIndex + displayText: model.currentDescription + delegate: Controls.RadioDelegate { + text: model.description + width: imageResolutionsComboBox.width + highlighted: imageResolutionsComboBox.highlightedIndex === model.index + checked: imageResolutionsComboBox.currentIndex === model.index + } + + Layout.fillWidth: true + + onActivated: { + recorder.imageEncoderSettings.resolution = model.value(index) + } + } + + Controls.Label { + text: qsTr('Quality') + + Layout.fillWidth: true + } + + Controls.ComboBox { + id: imageQualitiesComboBox + + model: recorder.imageQualityModel + currentIndex: model.currentIndex + displayText: model.currentDescription + delegate: Controls.RadioDelegate { + text: model.description + width: imageQualitiesComboBox.width + highlighted: imageQualitiesComboBox.highlightedIndex === model.index + checked: imageQualitiesComboBox.currentIndex === model.index + } + + Layout.fillWidth: true + + onActivated: { + recorder.imageEncoderSettings.quality = model.value(index) + } + } + } + + ColumnLayout { + id: audioTab + + enabled: bar.contentChildren[Controls.SwipeView.index].enabled + + Controls.Label { + text: qsTr('Codec') + + Layout.fillWidth: true + } + + Controls.ComboBox { + id: audioCodecsComboBox + + model: recorder.audioCodecModel + currentIndex: model.currentIndex + displayText: model.currentDescription + delegate: Controls.RadioDelegate { + text: model.description + width: audioCodecsComboBox.width + highlighted: audioCodecsComboBox.highlightedIndex === model.index + checked: audioCodecsComboBox.currentIndex === model.index + } + + Layout.fillWidth: true + + onActivated: { + recorder.audioEncoderSettings.codec = model.value(index) + } + } + + Controls.Label { + text: qsTr('Sample Rate') + + Layout.fillWidth: true + } + + Controls.ComboBox { + id: audioSampleRatesComboBox + + model: recorder.audioSampleRateModel + currentIndex: model.currentIndex + displayText: model.currentDescription + delegate: Controls.RadioDelegate { + text: model.description + width: audioSampleRatesComboBox.width + highlighted: audioSampleRatesComboBox.highlightedIndex === model.index + checked: audioSampleRatesComboBox.currentIndex === model.index + } + + Layout.fillWidth: true + + onActivated: { + recorder.audioEncoderSettings.sampleRate = model.value(index) + } + } + + Controls.Label { + text: qsTr('Quality') + + Layout.fillHeight: true + } + + Controls.ComboBox { + id: audioQualitiesComboBox + + model: recorder.audioQualityModel + currentIndex: model.currentIndex + displayText: model.currentDescription + delegate: Controls.RadioDelegate { + text: model.description + width: audioQualitiesComboBox.width + highlighted: audioQualitiesComboBox.highlightedIndex === model.index + checked: audioQualitiesComboBox.currentIndex === model.index + } + + Layout.fillWidth: true + + onActivated: { + recorder.audioEncoderSettings.quality = model.value(index) + } + } + } + + ColumnLayout { + id: videoTab + + enabled: bar.contentChildren[Controls.SwipeView.index].enabled + + Controls.Label { + text: qsTr('Codec') + + Layout.fillWidth: true + } + + Controls.ComboBox { + id: videoCodecsComboBox + + model: recorder.videoCodecModel + currentIndex: model.currentIndex + displayText: model.currentDescription + delegate: Controls.RadioDelegate { + text: model.description + width: videoCodecsComboBox.width + highlighted: videoCodecsComboBox.highlightedIndex === model.index + checked: videoCodecsComboBox.currentIndex === model.index + } + + Layout.fillWidth: true + + onActivated: { + recorder.videoEncoderSettings.codec = model.value(index) + } + } + + Controls.Label { + text: qsTr('Resolution') + + Layout.fillWidth: true + } + + Controls.ComboBox { + id: videoResolutionsComboBox + + model: recorder.videoResolutionModel + currentIndex: model.currentIndex + displayText: model.currentDescription + delegate: Controls.RadioDelegate { + text: model.description + width: videoResolutionsComboBox.width + highlighted: videoResolutionsComboBox.highlightedIndex === model.index + checked: videoResolutionsComboBox.currentIndex === model.index + } + + Layout.fillWidth: true + + onActivated: { + recorder.videoEncoderSettings.resolution = model.value(index) + } + } + + Controls.Label { + text: qsTr('Frame Rate') + + Layout.fillWidth: true + } + + Controls.ComboBox { + id: videoFrameRatesComboBox + + model: recorder.videoFrameRateModel + currentIndex: model.currentIndex + displayText: model.currentDescription + delegate: Controls.RadioDelegate { + text: model.description + width: videoFrameRatesComboBox.width + highlighted: videoFrameRatesComboBox.highlightedIndex === model.index + checked: videoFrameRatesComboBox.currentIndex === model.index + } + + Layout.fillWidth: true + + onActivated: { + recorder.videoEncoderSettings.frameRate = model.value(index) + } + } + + Controls.Label { + text: qsTr('Quality') + + Layout.fillHeight: true + } + + Controls.ComboBox { + id: videoQualitiesComboBox + + model: recorder.videoQualityModel + currentIndex: model.currentIndex + displayText: model.currentDescription + delegate: Controls.RadioDelegate { + text: model.description + width: videoQualitiesComboBox.width + highlighted: videoQualitiesComboBox.highlightedIndex === model.index + checked: videoQualitiesComboBox.currentIndex === model.index + } + + Layout.fillWidth: true + + onActivated: { + recorder.videoEncoderSettings.quality = model.value(index) + } + } + } + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + Multimedia.VideoOutput { + source: recorder + + width: sourceRect.width < parent.width && sourceRect.height < parent.height + ? sourceRect.width + : parent.width + height: sourceRect.width < parent.width && sourceRect.height < parent.height + ? sourceRect.height + : parent.height + + anchors { + centerIn: parent + } + } + } + + Controls.Label { + text: recorder.errorString + visible: text !== '' + color: 'red' + + font { + bold: true + } + + Layout.fillWidth: true + } + + RowLayout { + Controls.Label { + text: { + if (recorder.type === MediaRecorder.Type.Image) { + return recorder.isReady ? qsTr('Ready') : qsTr('Initializing...') + } + + switch (recorder.status) { + case MediaRecorder.Status.UnavailableStatus: + return qsTr('Unavailable') + case MediaRecorder.Status.UnloadedStatus: + case MediaRecorder.Status.LoadingStatus: + case MediaRecorder.Status.LoadedStatus: + return recorder.isReady ? qsTr('Ready') : qsTr('Initializing...') + case MediaRecorder.Status.StartingStatus: + case MediaRecorder.Status.RecordingStatus: + case MediaRecorder.Status.FinalizingStatus: + return qsTr('Recording...') + case MediaRecorder.Status.PausedStatus: + return qsTr('Paused') + } + } + } + + Controls.DialogButtonBox { + standardButtons: Controls.DialogButtonBox.RestoreDefaults + | Controls.DialogButtonBox.Reset + | Controls.DialogButtonBox.Save + + Layout.fillWidth: true + + onClicked: { + if (button === standardButton(Controls.DialogButtonBox.RestoreDefaults)) { + recorder.resetSettingsToDefaults() + } else if (button === standardButton(Controls.DialogButtonBox.Reset)) { + recorder.resetUserSettings() + } else if (button === standardButton(Controls.DialogButtonBox.Save)) { + recorder.saveUserSettings() + } + } + } + } + } + + Component.onCompleted: { + recorder.type = MediaRecorder.Type.Image + } +} diff --git a/src/qml/UserProfilePage.qml b/src/qml/UserProfilePage.qml index 5628a49..0ac9885 100644 --- a/src/qml/UserProfilePage.qml +++ b/src/qml/UserProfilePage.qml @@ -1,215 +1,215 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 Kaidan developers and contributors * (see the LICENSE file for a full list of copyright authors) * * Kaidan 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. * * In addition, as a special exception, the author of Kaidan gives * permission to link the code of its release with the OpenSSL * project's "OpenSSL" library (or with modified versions of it that * use the same license as the "OpenSSL" library), and distribute the * linked executables. You must obey the GNU General Public License in * all respects for all of the code used other than "OpenSSL". If you * modify this file, you may extend this exception to your version of * the file, but you are not obligated to do so. If you do not wish to * do so, delete this exception statement from your version. * * Kaidan 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 Kaidan. If not, see . */ -import QtQuick 2.10 +import QtQuick 2.6 import QtQuick.Controls 2.4 as Controls import QtQuick.Layouts 1.3 import QtQml 2.2 import org.kde.kirigami 2.2 as Kirigami import im.kaidan.kaidan 1.0 import "elements" Kirigami.Page { id: root title: qsTr("Profile") topPadding: 0 rightPadding: 0 bottomPadding: 0 leftPadding: 0 property string jid property string name property int presenceType: kaidan.presenceCache.getPresenceType(jid) property string statusMessage: kaidan.presenceCache.getStatusText(jid) Timer { id: pageTimer interval: 10 onTriggered: { if (!root.isCurrentPage) { // Close the current page if it's not the current one after 10ms pageStack.pop(); } // Stop the timer regardless of whether the page was closed or not pageTimer.stop(); } } onIsCurrentPageChanged: { /* * Start the timer if we are getting or loosing focus. * Probably due to some kind of animation, isCurrentPage changes a few ms after * this has been triggered. */ pageTimer.start(); } leftAction: Kirigami.Action { icon.name: "delete" onTriggered: removeSheet.open() } rightAction: Kirigami.Action { icon.name: "edit-rename" onTriggered: renameSheet.open() } RosterRemoveContactSheet { id: removeSheet jid: root.jid } RosterRenameContactSheet { id: renameSheet jid: root.jid currentName: { if (name === jid) return null; return name; } } Controls.ScrollView { anchors.fill: parent clip: true contentWidth: root.width contentHeight: content.height ColumnLayout { id: content x: 20 y: 5 width: root.width - 40 Item { Layout.preferredHeight: 10 } RowLayout { Layout.alignment: Qt.AlignTop Layout.fillWidth: true spacing: 20 Avatar { Layout.preferredHeight: Kirigami.Units.gridUnit * 10 Layout.preferredWidth: Kirigami.Units.gridUnit * 10 name: root.name avatarUrl: kaidan.avatarStorage.getAvatarUrl(jid) } ColumnLayout { Kirigami.Heading { Layout.fillWidth: true text: root.name textFormat: Text.PlainText maximumLineCount: 2 elide: Text.ElideRight } Controls.Label { text: root.jid color: Kirigami.Theme.disabledTextColor textFormat: Text.PlainText } RowLayout { spacing: Kirigami.Units.smallSpacing Kirigami.Icon { source: kaidan.utils.presenceTypeToIcon(presenceType) width: 26 height: 26 } Controls.Label { Layout.alignment: Qt.AlignVCenter text: kaidan.utils.presenceTypeToText(presenceType) color: kaidan.utils.presenceTypeToColor(presenceType) textFormat: Text.PlainText } Item { Layout.fillWidth: true } } } } Repeater { model: VCardModel { jid: root.jid } delegate: ColumnLayout { Layout.fillWidth: true Controls.Label { text: kaidan.utils.formatMessage(model.value) onLinkActivated: Qt.openUrlExternally(link) textFormat: Text.StyledText } Controls.Label { text: model.key color: Kirigami.Theme.disabledTextColor textFormat: Text.PlainText } Item { height: 3 } } } // placeholder for left, right and main action Item { visible: Kirigami.Settings.isMobile Layout.preferredHeight: 60 } } } function newPresenceArrived(jid) { if (jid === root.jid) { presenceType = kaidan.presenceCache.getPresenceType(root.jid); statusMessage = kaidan.presenceCache.getStatusText(root.jid); } } Component.onCompleted: { kaidan.presenceCache.presenceChanged.connect(newPresenceArrived); } Component.onDestruction: { kaidan.presenceCache.presenceChanged.disconnect(newPresenceArrived); } } diff --git a/src/qml/elements/ChatMessage.qml b/src/qml/elements/ChatMessage.qml index 52fa6b7..1c9c05d 100644 --- a/src/qml/elements/ChatMessage.qml +++ b/src/qml/elements/ChatMessage.qml @@ -1,303 +1,331 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 Kaidan developers and contributors * (see the LICENSE file for a full list of copyright authors) * * Kaidan 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. * * In addition, as a special exception, the author of Kaidan gives * permission to link the code of its release with the OpenSSL * project's "OpenSSL" library (or with modified versions of it that * use the same license as the "OpenSSL" library), and distribute the * linked executables. You must obey the GNU General Public License in * all respects for all of the code used other than "OpenSSL". If you * modify this file, you may extend this exception to your version of * the file, but you are not obligated to do so. If you do not wish to * do so, delete this exception statement from your version. * * Kaidan 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 Kaidan. If not, see . */ import QtQuick 2.6 import QtGraphicalEffects 1.0 import QtQuick.Layouts 1.3 import QtQuick.Controls 2.2 as Controls import org.kde.kirigami 2.0 as Kirigami import im.kaidan.kaidan 1.0 import MediaUtils 0.1 RowLayout { id: root property string msgId property string sender property bool sentByMe: true property string messageBody property date dateTime property bool isDelivered: false property int mediaType property string mediaGetUrl property string mediaLocation property bool edited property bool isLoading: kaidan.transferCache.hasUpload(msgId) property string name property TransferJob upload: { if (mediaType !== Enums.MessageType.MessageText && isLoading) { return kaidan.transferCache.jobByMessageId(model.id) } return null } property bool isSpoiler property string spoilerHint property bool isShowingSpoiler: false property string avatarUrl: kaidan.avatarStorage.getAvatarUrl(sender) signal messageEditRequested(string id, string body) // own messages are on the right, others on the left layoutDirection: sentByMe ? Qt.RightToLeft : Qt.LeftToRight spacing: 8 width: ListView.view.width // placeholder Item { Layout.preferredWidth: root.layoutDirection === Qt.LeftToRight ? 5 : 10 } Avatar { id: avatar visible: !sentByMe avatarUrl: root.avatarUrl Layout.alignment: Qt.AlignHCenter | Qt.AlignTop name: root.name Layout.preferredHeight: Kirigami.Units.gridUnit * 2.2 Layout.preferredWidth: Kirigami.Units.gridUnit * 2.2 } // message bubble/box Item { Layout.preferredWidth: content.width + 13 Layout.preferredHeight: content.height + 8 Rectangle { id: box anchors.fill: parent color: sentByMe ? Kirigami.Theme.complementaryTextColor : Kirigami.Theme.highlightColor radius: Kirigami.Units.smallSpacing * 2 layer.enabled: box.visible layer.effect: DropShadow { verticalOffset: Kirigami.Units.gridUnit * 0.08 horizontalOffset: Kirigami.Units.gridUnit * 0.08 color: Kirigami.Theme.disabledTextColor samples: 10 spread: 0.1 } MouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: { if (mouse.button === Qt.RightButton) contextMenu.popup() } onPressAndHold: { contextMenu.popup() } } Controls.Menu { id: contextMenu Controls.MenuItem { text: qsTr("Copy Message") enabled: bodyLabel.visible onTriggered: { if (!isSpoiler || isShowingSpoiler) kaidan.utils.copyToClipboard(messageBody); else kaidan.utils.copyToClipboard(spoilerHint); } } Controls.MenuItem { text: qsTr("Edit Message") enabled: kaidan.messageModel.canCorrectMessage(msgId) onTriggered: root.messageEditRequested(msgId, messageBody) } Controls.MenuItem { text: qsTr("Copy download URL") enabled: mediaGetUrl onTriggered: kaidan.utils.copyToClipboard(mediaGetUrl) } } } ColumnLayout { id: content spacing: 0 anchors.centerIn: parent anchors.margins: 4 RowLayout { id: spoilerHintRow visible: isSpoiler Controls.Label { id: dateLabeltest text: spoilerHint == "" ? qsTr("Spoiler") : spoilerHint color: sentByMe ? Kirigami.Theme.textColor : Kirigami.Theme.complementaryTextColor font.pixelSize: Kirigami.Units.gridUnit * 0.8 MouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: { if (mouse.button === Qt.LeftButton) { isShowingSpoiler = !isShowingSpoiler } } } } Item { Layout.fillWidth: true height: 1 } Kirigami.Icon { height: 28 width: 28 source: isShowingSpoiler ? "password-show-off" : "password-show-on" color: sentByMe ? Kirigami.Theme.textColor : Kirigami.Theme.complementaryTextColor } } Kirigami.Separator { visible: isSpoiler Layout.fillWidth: true color: { var bgColor = sentByMe ? Kirigami.Theme.backgroundColor : Kirigami.Theme.highlightColor var textColor = sentByMe ? Kirigami.Theme.textColor : Kirigami.Theme.highlightedTextColor return Qt.tint(textColor, Qt.rgba(bgColor.r, bgColor.g, bgColor.b, 0.7)) } } ColumnLayout { visible: isSpoiler && isShowingSpoiler || !isSpoiler Controls.ToolButton { visible: { - (mediaType !== Enums.MessageType.MessageText && !isLoading && mediaGetUrl !== "" - && (mediaLocation === "" || !MediaUtilsInstance.localFileAvailable(media.mediaSource))) + switch (root.mediaType) { + case Enums.MessageType.MessageUnknown: + case Enums.MessageType.MessageText: + case Enums.MessageType.MessageGeoLocation: + break + case Enums.MessageType.MessageImage: + case Enums.MessageType.MessageAudio: + case Enums.MessageType.MessageVideo: + case Enums.MessageType.MessageFile: + case Enums.MessageType.MessageDocument: + return !root.isLoading && root.mediaGetUrl !== "" + && (root.mediaLocation === "" || !MediaUtilsInstance.localFileAvailable(media.mediaSource)) + } + + return false } text: qsTr("Download") onClicked: { print("Downloading " + mediaGetUrl + "...") kaidan.downloadMedia(msgId, mediaGetUrl) } } MediaPreviewLoader { id: media mediaSource: { - return root.mediaLocation !== '' - ? MediaUtilsInstance.fromLocalFile(root.mediaLocation) - : root.mediaGetUrl + switch (root.mediaType) { + case Enums.MessageType.MessageUnknown: + case Enums.MessageType.MessageText: + break + case Enums.MessageType.MessageGeoLocation: + return root.mediaLocation + case Enums.MessageType.MessageImage: + case Enums.MessageType.MessageAudio: + case Enums.MessageType.MessageVideo: + case Enums.MessageType.MessageFile: + case Enums.MessageType.MessageDocument: + const localFile = root.mediaLocation !== '' + ? MediaUtilsInstance.fromLocalFile(root.mediaLocation) + : '' + return MediaUtilsInstance.localFileAvailable(localFile) ? localFile : root.mediaGetUrl + } + + return '' } mediaSourceType: root.mediaType showOpenButton: true message: root } // message body Controls.Label { id: bodyLabel - visible: messageBody !== "" && messageBody !== mediaGetUrl + visible: (root.mediaType === Enums.MessageType.MessageText || messageBody !== mediaGetUrl) && messageBody !== "" text: kaidan.utils.formatMessage(messageBody) textFormat: Text.StyledText wrapMode: Text.Wrap color: sentByMe ? Kirigami.Theme.textColor : Kirigami.Theme.complementaryTextColor onLinkActivated: Qt.openUrlExternally(link) Layout.maximumWidth: media.enabled ? media.width : root.width - Kirigami.Units.gridUnit * 6 } Kirigami.Separator { visible: isSpoiler && isShowingSpoiler Layout.fillWidth: true color: { var bgColor = sentByMe ? Kirigami.Theme.backgroundColor : Kirigami.Theme.highlightColor var textColor = sentByMe ? Kirigami.Theme.textColor : Kirigami.Theme.highlightedTextColor return Qt.tint(textColor, Qt.rgba(bgColor.r, bgColor.g, bgColor.b, 0.7)) } } } // message meta: date, isDelivered RowLayout { Controls.Label { id: dateLabel text: Qt.formatDateTime(dateTime, "dd. MMM yyyy, hh:mm") color: sentByMe ? Kirigami.Theme.disabledTextColor : Qt.darker(Kirigami.Theme.disabledTextColor, 1.3) font.pixelSize: Kirigami.Units.gridUnit * 0.8 } Image { id: checkmark visible: (sentByMe && isDelivered) source: kaidan.utils.getResourcePath("images/message_checkmark.svg") Layout.preferredHeight: Kirigami.Units.gridUnit * 0.65 Layout.preferredWidth: Kirigami.Units.gridUnit * 0.65 sourceSize.height: Kirigami.Units.gridUnit * 0.65 sourceSize.width: Kirigami.Units.gridUnit * 0.65 } Kirigami.Icon { source: "edit-symbolic" visible: edited Layout.preferredHeight: Kirigami.Units.gridUnit * 0.65 Layout.preferredWidth: Kirigami.Units.gridUnit * 0.65 } } // progress bar for upload/download status Controls.ProgressBar { visible: isLoading value: upload ? upload.progress : 0 Layout.fillWidth: true Layout.maximumWidth: Kirigami.Units.gridUnit * 14 } } } // placeholder Item { Layout.fillWidth: true } function updateIsLoading() { isLoading = kaidan.transferCache.hasUpload(msgId) } Component.onCompleted: { kaidan.transferCache.jobsChanged.connect(updateIsLoading) } Component.onDestruction: { kaidan.transferCache.jobsChanged.disconnect(updateIsLoading) } } diff --git a/src/qml/elements/MediaPreview.qml b/src/qml/elements/MediaPreview.qml index 82c926e..a15fe52 100644 --- a/src/qml/elements/MediaPreview.qml +++ b/src/qml/elements/MediaPreview.qml @@ -1,62 +1,63 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 Kaidan developers and contributors * (see the LICENSE file for a full list of copyright authors) * * Kaidan 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. * * In addition, as a special exception, the author of Kaidan gives * permission to link the code of its release with the OpenSSL * project's "OpenSSL" library (or with modified versions of it that * use the same license as the "OpenSSL" library), and distribute the * linked executables. You must obey the GNU General Public License in * all respects for all of the code used other than "OpenSSL". If you * modify this file, you may extend this exception to your version of * the file, but you are not obligated to do so. If you do not wish to * do so, delete this exception statement from your version. * * Kaidan 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 Kaidan. If not, see . */ /** * This element is used in the @see SendMediaSheet to display information about a selected file to * the user. It shows the file name, file size and a little file icon. */ import QtQuick 2.6 import QtQuick.Layouts 1.3 import QtQuick.Controls 2.0 as Controls import org.kde.kirigami 2.0 as Kirigami import im.kaidan.kaidan 1.0 Rectangle { id: root property url mediaSource property int mediaSourceType: Enums.MessageType.MessageUnknown property bool showOpenButton: false - property Item message: null property int messageSize: Kirigami.Units.gridUnit * 14 + property QtObject message + property QtObject mediaSheet color: message ? 'transparent' : Kirigami.Theme.backgroundColor Layout.fillHeight: false Layout.fillWidth: message ? false : true Layout.alignment: Qt.AlignCenter Layout.margins: 0 Layout.leftMargin: undefined Layout.topMargin: undefined Layout.rightMargin: undefined Layout.bottomMargin: undefined } diff --git a/src/qml/elements/MediaPreviewLoader.qml b/src/qml/elements/MediaPreviewLoader.qml index 5bdcbd6..e2e3ec7 100644 --- a/src/qml/elements/MediaPreviewLoader.qml +++ b/src/qml/elements/MediaPreviewLoader.qml @@ -1,139 +1,156 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 Kaidan developers and contributors * (see the LICENSE file for a full list of copyright authors) * * Kaidan 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. * * In addition, as a special exception, the author of Kaidan gives * permission to link the code of its release with the OpenSSL * project's "OpenSSL" library (or with modified versions of it that * use the same license as the "OpenSSL" library), and distribute the * linked executables. You must obey the GNU General Public License in * all respects for all of the code used other than "OpenSSL". If you * modify this file, you may extend this exception to your version of * the file, but you are not obligated to do so. If you do not wish to * do so, delete this exception statement from your version. * * Kaidan 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 Kaidan. If not, see . */ import QtQuick 2.6 import QtQuick.Layouts 1.3 import QtQuick.Controls 2.0 as Controls import QtPositioning 5.9 as Positioning import im.kaidan.kaidan 1.0 import MediaUtils 0.1 Loader { id: root property url mediaSource property int mediaSourceType property bool showOpenButton property QtObject message property QtObject mediaSheet enabled: { switch (mediaSourceType) { case Enums.MessageType.MessageUnknown: case Enums.MessageType.MessageText: - case Enums.MessageType.MessageGeoLocation: return false case Enums.MessageType.MessageImage: case Enums.MessageType.MessageAudio: case Enums.MessageType.MessageVideo: case Enums.MessageType.MessageFile: case Enums.MessageType.MessageDocument: + case Enums.MessageType.MessageGeoLocation: return mediaSource != '' && sourceComponent !== null } } visible: enabled sourceComponent: { switch (mediaSourceType) { case Enums.MessageType.MessageUnknown: case Enums.MessageType.MessageText: - case Enums.MessageType.MessageGeoLocation: return null case Enums.MessageType.MessageImage: return imagePreview case Enums.MessageType.MessageAudio: return audioPreview case Enums.MessageType.MessageVideo: return videoPreview case Enums.MessageType.MessageFile: case Enums.MessageType.MessageDocument: return otherPreview + case Enums.MessageType.MessageGeoLocation: + return locationPreview } } Layout.fillHeight: item ? item.Layout.fillHeight : false Layout.fillWidth: item ? item.Layout.fillWidth : false Layout.preferredHeight: item ? item.Layout.preferredHeight : -1 Layout.preferredWidth: item ? item.Layout.preferredWidth : -1 Layout.minimumHeight: item ? item.Layout.minimumHeight : -1 Layout.minimumWidth: item ? item.Layout.minimumWidth : -1 Layout.maximumHeight: item ? item.Layout.maximumHeight : -1 Layout.maximumWidth: item ? item.Layout.maximumWidth : -1 Layout.alignment: item ? item.Layout.alignment : Qt.AlignCenter Layout.margins: item ? item.Layout.margins : 0 Layout.leftMargin: item ? item.Layout.leftMargin : 0 Layout.topMargin: item ? item.Layout.topMargin : 0 Layout.rightMargin: item ? item.Layout.rightMargin : 0 Layout.bottomMargin: item ? item.Layout.bottomMargin : 0 Component { id: imagePreview MediaPreviewImage { mediaSource: root.mediaSource mediaSourceType: root.mediaSourceType showOpenButton: root.showOpenButton message: root.message + mediaSheet: root.mediaSheet } } Component { id: audioPreview MediaPreviewAudio { mediaSource: root.mediaSource mediaSourceType: root.mediaSourceType showOpenButton: root.showOpenButton message: root.message + mediaSheet: root.mediaSheet } } Component { id: videoPreview MediaPreviewVideo { mediaSource: root.mediaSource mediaSourceType: root.mediaSourceType showOpenButton: root.showOpenButton message: root.message + mediaSheet: root.mediaSheet } } Component { id: otherPreview MediaPreviewOther { mediaSource: root.mediaSource mediaSourceType: root.mediaSourceType showOpenButton: root.showOpenButton message: root.message + mediaSheet: root.mediaSheet + } + } + + Component { + id: locationPreview + + MediaPreviewLocation { + mediaSource: root.mediaSource + mediaSourceType: root.mediaSourceType + showOpenButton: root.showOpenButton + message: root.message + mediaSheet: root.mediaSheet } } } diff --git a/src/qml/elements/MediaPreview.qml b/src/qml/elements/MediaPreviewLocation.qml similarity index 51% copy from src/qml/elements/MediaPreview.qml copy to src/qml/elements/MediaPreviewLocation.qml index 82c926e..a609d8d 100644 --- a/src/qml/elements/MediaPreview.qml +++ b/src/qml/elements/MediaPreviewLocation.qml @@ -1,62 +1,111 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 Kaidan developers and contributors * (see the LICENSE file for a full list of copyright authors) * * Kaidan 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. * * In addition, as a special exception, the author of Kaidan gives * permission to link the code of its release with the OpenSSL * project's "OpenSSL" library (or with modified versions of it that * use the same license as the "OpenSSL" library), and distribute the * linked executables. You must obey the GNU General Public License in * all respects for all of the code used other than "OpenSSL". If you * modify this file, you may extend this exception to your version of * the file, but you are not obligated to do so. If you do not wish to * do so, delete this exception statement from your version. * * Kaidan 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 Kaidan. If not, see . */ /** - * This element is used in the @see SendMediaSheet to display information about a selected file to - * the user. It shows the file name, file size and a little file icon. + * This element is used in the @see SendMediaSheet to display information about a shared location to + * the user. It just displays the map in a rectangle. */ import QtQuick 2.6 import QtQuick.Layouts 1.3 -import QtQuick.Controls 2.0 as Controls +import QtLocation 5.9 as Location import org.kde.kirigami 2.0 as Kirigami import im.kaidan.kaidan 1.0 +import MediaUtils 0.1 -Rectangle { +MediaPreview { id: root - property url mediaSource - property int mediaSourceType: Enums.MessageType.MessageUnknown - property bool showOpenButton: false - property Item message: null - property int messageSize: Kirigami.Units.gridUnit * 14 - - color: message ? 'transparent' : Kirigami.Theme.backgroundColor - - Layout.fillHeight: false - Layout.fillWidth: message ? false : true - Layout.alignment: Qt.AlignCenter - Layout.margins: 0 - Layout.leftMargin: undefined - Layout.topMargin: undefined - Layout.rightMargin: undefined - Layout.bottomMargin: undefined + Layout.preferredHeight: message ? messageSize : Kirigami.Units.gridUnit * 18 + Layout.preferredWidth: Kirigami.Units.gridUnit * 32 + Layout.maximumWidth: message ? messageSize : -1 + + ColumnLayout { + anchors { + fill: parent + } + + Location.Map { + id: map + + zoomLevel: (maximumZoomLevel - minimumZoomLevel) / 1.2 + center: MediaUtilsInstance.locationCoordinate(root.mediaSource) + copyrightsVisible: false + + plugin: Location.Plugin { + preferred: ["osm", "mapboxgl"] + } + + gesture { + enabled: false + } + + Layout.fillHeight: true + Layout.fillWidth: true + + onErrorChanged: { + if (map.error !== Location.Map.NoError) { + console.log("***", map.errorString) + } + } + + Location.MapQuickItem { + id: positionMarker + + coordinate: map.center + anchorPoint: Qt.point(sourceItem.width / 2, sourceItem.height) + + sourceItem: Kirigami.Icon { + source: MediaUtilsInstance.newMediaIconName(Enums.MessageType.MessageGeoLocation) + height: 48 + width: height + color: "#e41e25" + smooth: true + } + } + + MouseArea { + enabled: root.showOpenButton + + anchors { + fill: parent + } + + onClicked: { + if (!Qt.openUrlExternally(root.message.messageBody)) { + Qt.openUrlExternally('https://www.openstreetmap.org/?mlat=%1&mlon=%2&zoom=18&layers=M' + .arg(root.geoLocation.latitude).arg(root.geoLocation.longitude)) + } + } + } + } + } } diff --git a/src/qml/elements/NewMedia.qml b/src/qml/elements/NewMedia.qml new file mode 100644 index 0000000..7b7f849 --- /dev/null +++ b/src/qml/elements/NewMedia.qml @@ -0,0 +1,226 @@ +/* + * Kaidan - A user-friendly XMPP client for every device! + * + * Copyright (C) 2016-2019 Kaidan developers and contributors + * (see the LICENSE file for a full list of copyright authors) + * + * Kaidan 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. + * + * In addition, as a special exception, the author of Kaidan gives + * permission to link the code of its release with the OpenSSL + * project's "OpenSSL" library (or with modified versions of it that + * use the same license as the "OpenSSL" library), and distribute the + * linked executables. You must obey the GNU General Public License in + * all respects for all of the code used other than "OpenSSL". If you + * modify this file, you may extend this exception to your version of + * the file, but you are not obligated to do so. If you do not wish to + * do so, delete this exception statement from your version. + * + * Kaidan 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 Kaidan. If not, see . + */ + +/** + * This element is used in the @see SendMediaSheet to share information about a new media (picture, audio and video) to + * the user. It just displays the camera image in a rectangle. + */ + +import QtQuick 2.7 +import QtQuick.Layouts 1.3 +import QtQuick.Controls 2.4 as Controls +import QtMultimedia 5.10 as Multimedia +import org.kde.kirigami 2.2 as Kirigami + +import im.kaidan.kaidan 1.0 +import MediaUtils 0.1 + +MediaPreview { + id: root + + readonly property bool isNewImage: mediaSourceType === Enums.MessageType.MessageImage + readonly property bool isNewAudio: mediaSourceType === Enums.MessageType.MessageAudio + readonly property bool isNewVideo: mediaSourceType === Enums.MessageType.MessageVideo + + Layout.preferredHeight: Kirigami.Units.gridUnit * 14 + Layout.preferredWidth: Kirigami.Units.gridUnit * 14 + Layout.maximumWidth: -1 + + MediaRecorder { + id: recorder + + type: { + switch (root.mediaSourceType) { + case Enums.MessageType.MessageUnknown: + case Enums.MessageType.MessageText: + case Enums.MessageType.MessageFile: + case Enums.MessageType.MessageDocument: + case Enums.MessageType.MessageGeoLocation: + return MediaRecorder.Type.Invalid + case Enums.MessageType.MessageImage: + return MediaRecorder.Type.Image + case Enums.MessageType.MessageAudio: + return MediaRecorder.Type.Audio + case Enums.MessageType.MessageVideo: + return MediaRecorder.Type.Video + } + } + } + + ColumnLayout { + anchors { + fill: parent + } + + Multimedia.VideoOutput { + source: recorder + focus : visible // to receive focus and capture key events when visible + visible: !previewLoader.visible + + Layout.fillWidth: true + Layout.fillHeight: true + + Controls.RoundButton { + display: Controls.AbstractButton.IconOnly + checkable: root.isNewVideo + width: parent.width * .2 + height: width + enabled: { + return recorder.isReady || + (recorder.status >= MediaRecorder.Status.StartingStatus + && recorder.status <= MediaRecorder.Status.FinalizingStatus) + } + + icon { + width: parent.width + height: width + name: pressed || checked ? 'media-playback-stop' : 'media-record' + } + + anchors { + horizontalCenter: parent.horizontalCenter + bottom: parent.bottom + bottomMargin: -(height / 4) + } + + onCheckedChanged: { + if (checked) { + recorder.record() + } else { + recorder.stop() + } + } + + onClicked: { + if (root.isNewImage) { + recorder.record() + } + } + + onPressAndHold: { + if (root.isNewAudio) { + recorder.record() + } + } + + onReleased: { + if (root.isNewAudio) { + recorder.stop() + } + } + } + + Controls.Label { + horizontalAlignment: Controls.Label.AlignRight + text: { + if (root.isNewImage) { + return recorder.isReady ? qsTr('Ready') : qsTr('Initializing...') + } + + switch (recorder.status) { + case MediaRecorder.Status.UnavailableStatus: + return qsTr('Unavailable') + case MediaRecorder.Status.UnloadedStatus: + case MediaRecorder.Status.LoadingStatus: + case MediaRecorder.Status.LoadedStatus: + return recorder.isReady ? qsTr('Ready') : qsTr('Initializing...') + case MediaRecorder.Status.StartingStatus: + case MediaRecorder.Status.RecordingStatus: + case MediaRecorder.Status.FinalizingStatus: + return qsTr('Recording... %1').arg(MediaUtilsInstance.prettyDuration(recorder.duration)) + case MediaRecorder.Status.PausedStatus: + return qsTr('Paused %1').arg(MediaUtilsInstance.prettyDuration(recorder.duration)) + } + } + + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + } + } + + MediaPreviewLoader { + id: previewLoader + + mediaSource: recorder.actualLocation + mediaSourceType: root.mediaSourceType + showOpenButton: root.showOpenButton + message: root.message + mediaSheet: root.mediaSheet + + visible: mediaSource != '' && recorder.isReady + + Layout.fillHeight: true + Layout.fillWidth: true + + RowLayout { + visible: root.mediaSource == '' + z: 1 + + anchors { + left: parent.left + top: parent.top + right: parent.right + } + + Controls.ToolButton { + icon.name: 'dialog-cancel' + + onClicked: { + recorder.cancel() + } + } + + Item { + Layout.fillWidth: true + } + + Controls.ToolButton { + icon.name: 'dialog-ok-apply' + + onClicked: { + root.mediaSource = previewLoader.mediaSource + } + } + } + } + } + + Connections { + target: root.mediaSheet + enabled: target + + onRejected: { + recorder.cancel() + } + } +} diff --git a/src/qml/elements/MediaPreviewLoader.qml b/src/qml/elements/NewMediaLoader.qml similarity index 83% copy from src/qml/elements/MediaPreviewLoader.qml copy to src/qml/elements/NewMediaLoader.qml index 5bdcbd6..b594c30 100644 --- a/src/qml/elements/MediaPreviewLoader.qml +++ b/src/qml/elements/NewMediaLoader.qml @@ -1,139 +1,124 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 Kaidan developers and contributors * (see the LICENSE file for a full list of copyright authors) * * Kaidan 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. * * In addition, as a special exception, the author of Kaidan gives * permission to link the code of its release with the OpenSSL * project's "OpenSSL" library (or with modified versions of it that * use the same license as the "OpenSSL" library), and distribute the * linked executables. You must obey the GNU General Public License in * all respects for all of the code used other than "OpenSSL". If you * modify this file, you may extend this exception to your version of * the file, but you are not obligated to do so. If you do not wish to * do so, delete this exception statement from your version. * * Kaidan 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 Kaidan. If not, see . */ import QtQuick 2.6 import QtQuick.Layouts 1.3 import QtQuick.Controls 2.0 as Controls import QtPositioning 5.9 as Positioning import im.kaidan.kaidan 1.0 import MediaUtils 0.1 Loader { id: root property url mediaSource property int mediaSourceType property bool showOpenButton property QtObject message property QtObject mediaSheet enabled: { switch (mediaSourceType) { case Enums.MessageType.MessageUnknown: case Enums.MessageType.MessageText: - case Enums.MessageType.MessageGeoLocation: - return false + case Enums.MessageType.MessageFile: + case Enums.MessageType.MessageDocument: + return false; case Enums.MessageType.MessageImage: case Enums.MessageType.MessageAudio: case Enums.MessageType.MessageVideo: - case Enums.MessageType.MessageFile: - case Enums.MessageType.MessageDocument: - return mediaSource != '' && sourceComponent !== null + case Enums.MessageType.MessageGeoLocation: + return mediaSheet } + } visible: enabled sourceComponent: { switch (mediaSourceType) { case Enums.MessageType.MessageUnknown: case Enums.MessageType.MessageText: - case Enums.MessageType.MessageGeoLocation: + case Enums.MessageType.MessageFile: + case Enums.MessageType.MessageDocument: return null case Enums.MessageType.MessageImage: - return imagePreview case Enums.MessageType.MessageAudio: - return audioPreview case Enums.MessageType.MessageVideo: - return videoPreview - case Enums.MessageType.MessageFile: - case Enums.MessageType.MessageDocument: - return otherPreview + return newMedia + case Enums.MessageType.MessageGeoLocation: + return newMediaLocation } } Layout.fillHeight: item ? item.Layout.fillHeight : false Layout.fillWidth: item ? item.Layout.fillWidth : false Layout.preferredHeight: item ? item.Layout.preferredHeight : -1 Layout.preferredWidth: item ? item.Layout.preferredWidth : -1 Layout.minimumHeight: item ? item.Layout.minimumHeight : -1 Layout.minimumWidth: item ? item.Layout.minimumWidth : -1 Layout.maximumHeight: item ? item.Layout.maximumHeight : -1 Layout.maximumWidth: item ? item.Layout.maximumWidth : -1 Layout.alignment: item ? item.Layout.alignment : Qt.AlignCenter Layout.margins: item ? item.Layout.margins : 0 Layout.leftMargin: item ? item.Layout.leftMargin : 0 Layout.topMargin: item ? item.Layout.topMargin : 0 Layout.rightMargin: item ? item.Layout.rightMargin : 0 Layout.bottomMargin: item ? item.Layout.bottomMargin : 0 Component { - id: imagePreview + id: newMedia - MediaPreviewImage { - mediaSource: root.mediaSource + NewMedia { mediaSourceType: root.mediaSourceType showOpenButton: root.showOpenButton message: root.message - } - } - - Component { - id: audioPreview + mediaSheet: root.mediaSheet - MediaPreviewAudio { - mediaSource: root.mediaSource - mediaSourceType: root.mediaSourceType - showOpenButton: root.showOpenButton - message: root.message + onMediaSourceChanged: { + root.mediaSheet.source = mediaSource + } } } Component { - id: videoPreview + id: newMediaLocation - MediaPreviewVideo { - mediaSource: root.mediaSource + NewMediaLocation { mediaSourceType: root.mediaSourceType showOpenButton: root.showOpenButton message: root.message - } - } - - Component { - id: otherPreview + mediaSheet: root.mediaSheet - MediaPreviewOther { - mediaSource: root.mediaSource - mediaSourceType: root.mediaSourceType - showOpenButton: root.showOpenButton - message: root.message + onMediaSourceChanged: { + root.mediaSheet.source = mediaSource + } } } } diff --git a/src/qml/elements/NewMediaLocation.qml b/src/qml/elements/NewMediaLocation.qml new file mode 100644 index 0000000..7d586d2 --- /dev/null +++ b/src/qml/elements/NewMediaLocation.qml @@ -0,0 +1,258 @@ +/* + * Kaidan - A user-friendly XMPP client for every device! + * + * Copyright (C) 2016-2019 Kaidan developers and contributors + * (see the LICENSE file for a full list of copyright authors) + * + * Kaidan 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. + * + * In addition, as a special exception, the author of Kaidan gives + * permission to link the code of its release with the OpenSSL + * project's "OpenSSL" library (or with modified versions of it that + * use the same license as the "OpenSSL" library), and distribute the + * linked executables. You must obey the GNU General Public License in + * all respects for all of the code used other than "OpenSSL". If you + * modify this file, you may extend this exception to your version of + * the file, but you are not obligated to do so. If you do not wish to + * do so, delete this exception statement from your version. + * + * Kaidan 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 Kaidan. If not, see . + */ + +/** + * This element is used in the @see SendMediaSheet to share information about a location to + * the user. It just displays the map in a rectangle. + */ + +import QtQuick 2.6 +import QtQuick.Layouts 1.3 +import QtQuick.Controls 2.0 as Controls +import QtPositioning 5.9 as Positioning +import QtLocation 5.9 as Location +import org.kde.kirigami 2.0 as Kirigami + +import im.kaidan.kaidan 1.0 +import MediaUtils 0.1 + +MediaPreview { + id: root + + property var selectedGeoLocation + + // Quicksy / Conversations compatible + mediaSource: selectedGeoLocation !== undefined && selectedGeoLocation.isValid + ? 'geo:%1,%2'.arg(selectedGeoLocation.latitude.toString()).arg(selectedGeoLocation.longitude.toString()) + : '' + + Layout.preferredHeight: message ? messageSize : Kirigami.Units.gridUnit * 18 + Layout.preferredWidth: Kirigami.Units.gridUnit * 32 + Layout.maximumWidth: message ? messageSize : -1 + + ColumnLayout { + anchors { + fill: parent + } + + Location.Map { + id: map + + zoomLevel: (maximumZoomLevel - minimumZoomLevel) / 2 + center: root.selectedGeoLocation + + plugin: Location.Plugin { + preferred: ["osm", "mapboxgl"] + } + + gesture { + flickDeceleration: 3000 + enabled: !root.message + } + + Layout.fillHeight: true + Layout.fillWidth: true + + Keys.onPressed: { + if (!root.mediaSource) { + if (event.key === Qt.Key_Plus) { + map.zoomLevel++ + } else if (event.key === Qt.Key_Minus) { + map.zoomLevel-- + } + } + } + + onCenterChanged: { + if (!followMe.checked && !root.message) { + root.selectedGeoLocation = center + } + } + + onCopyrightLinkActivated: Qt.openUrlExternally(link) + + onErrorChanged: { + if (map.error !== Location.Map.NoError) { + console.log("***", map.errorString) + } + } + + Positioning.PositionSource { + id: currentPosition + + active: !root.message + + onPositionChanged: { + if (position.coordinate.isValid) { + if (!root.message && followMe.checked) { + root.selectedGeoLocation = position.coordinate + map.center = root.selectedGeoLocation + } + } else { + console.log('***', 'Can not locate this device.') + } + } + + onSourceErrorChanged: { + if (sourceError !== Positioning.PositionSource.NoError) { + console.log("***", sourceError) + stop() + } + } + + onUpdateTimeout: { + console.log("***", "Position lookup timeout.") + } + } + + Location.MapQuickItem { + id: currentPositionMarker + + visible: !root.message + coordinate: currentPosition.position.coordinate + anchorPoint: Qt.point(sourceItem.width / 2, sourceItem.height / 2) + + sourceItem: Rectangle { + height: 12 + width: height + color: "#0080FF" + radius: height / 2 + + border { + width: 2 + color: Qt.lighter(color) + } + } + } + + Location.MapQuickItem { + id: userPositionMarker + + coordinate: followMe.checked ? root.selectedGeoLocation : map.center + anchorPoint: Qt.point(sourceItem.width / 2, sourceItem.height) + + sourceItem: Kirigami.Icon { + source: MediaUtilsInstance.newMediaIconName(Enums.MessageType.MessageGeoLocation) + height: 48 + width: height + color: "#e41e25" + smooth: true + } + } + + Controls.ToolButton { + id: followMe + + visible: !root.message + enabled: currentPosition.supportedPositioningMethods !== Positioning.PositionSource.NoPositioningMethods + checkable: true + checked: true + + icon { + name: checked + ? MediaUtilsInstance.newMediaIconName(Enums.MessageType.MessageGeoLocation) + : 'crosshairs' + } + + anchors { + right: parent.right + bottom: parent.bottom + margins: Kirigami.Units.gridUnit + } + + background: Rectangle { + radius: height / 2 + color: "green" + + border { + color: 'white' + width: 2 + } + } + + contentItem: Kirigami.Icon { + source: parent.icon.name + + anchors { + fill: parent + margins: Kirigami.Units.smallSpacing + } + } + + onCheckedChanged: { + if (checked) { + root.selectedGeoLocation = currentPosition.position.coordinate + map.center = root.selectedGeoLocation + } else { + root.selectedGeoLocation = map.center + } + } + } + + Controls.Slider { + id: zoomSlider + + visible: !root.message + from: map.minimumZoomLevel + to: map.maximumZoomLevel + orientation : Qt.Vertical + value: map.zoomLevel + width: Kirigami.Units.gridUnit * 1.4 + z: map.z + 3 + + anchors { + left: parent.left + top: parent.top + bottom: parent.bottom + margins: Kirigami.Units.gridUnit + } + + onValueChanged: { + map.zoomLevel = value + } + } + + MouseArea { + enabled: root.showOpenButton + + anchors { + fill: parent + } + + onClicked: { + if (!Qt.openUrlExternally(root.message.messageBody)) { + Qt.openUrlExternally('https://www.openstreetmap.org/?mlat=%1&mlon=%2&zoom=18&layers=M' + .arg(root.selectedGeoLocation.latitude).arg(root.selectedGeoLocation.longitude)) + } + } + } + } + } +} diff --git a/src/qml/elements/SendMediaSheet.qml b/src/qml/elements/SendMediaSheet.qml index f90173d..a952c6c 100644 --- a/src/qml/elements/SendMediaSheet.qml +++ b/src/qml/elements/SendMediaSheet.qml @@ -1,140 +1,191 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 Kaidan developers and contributors * (see the LICENSE file for a full list of copyright authors) * * Kaidan 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. * * In addition, as a special exception, the author of Kaidan gives * permission to link the code of its release with the OpenSSL * project's "OpenSSL" library (or with modified versions of it that * use the same license as the "OpenSSL" library), and distribute the * linked executables. You must obey the GNU General Public License in * all respects for all of the code used other than "OpenSSL". If you * modify this file, you may extend this exception to your version of * the file, but you are not obligated to do so. If you do not wish to * do so, delete this exception statement from your version. * * Kaidan 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 Kaidan. If not, see . */ import QtQuick 2.6 import QtQuick.Layouts 1.3 import QtQuick.Controls 2.0 as Controls import org.kde.kirigami 2.0 as Kirigami import im.kaidan.kaidan 1.0 import MediaUtils 0.1 Kirigami.OverlaySheet { id: root property string targetJid property url source property int sourceType + property bool newMedia: false + + signal rejected() + signal accepted() showCloseButton: false contentItem: ColumnLayout { // message type preview - MediaPreviewLoader { - id: mediaLoader + Loader { + id: loader + + enabled: (root.newMedia || root.source != '') && sourceComponent !== null + visible: enabled + sourceComponent: root.newMedia ? newMediaComponent : mediaPreviewComponent + + Layout.fillHeight: item ? item.Layout.fillHeight : false + Layout.fillWidth: item ? item.Layout.fillWidth : false + Layout.preferredHeight: item ? item.Layout.preferredHeight : -1 + Layout.preferredWidth: item ? item.Layout.preferredWidth : -1 + Layout.minimumHeight: item ? item.Layout.minimumHeight : -1 + Layout.minimumWidth: item ? item.Layout.minimumWidth : -1 + Layout.maximumHeight: item ? item.Layout.maximumHeight : -1 + Layout.maximumWidth: item ? item.Layout.maximumWidth : -1 + Layout.alignment: item ? item.Layout.alignment : Qt.AlignCenter + Layout.margins: item ? item.Layout.margins : 0 + Layout.leftMargin: item ? item.Layout.leftMargin : 0 + Layout.topMargin: item ? item.Layout.topMargin : 0 + Layout.rightMargin: item ? item.Layout.rightMargin : 0 + Layout.bottomMargin: item ? item.Layout.bottomMargin : 0 + + Component { + id: newMediaComponent + + NewMediaLoader { + mediaSourceType: root.sourceType + mediaSheet: root + } + } + + Component { + id: mediaPreviewComponent - mediaSheet: root - mediaSource: root.source - mediaSourceType: root.sourceType + MediaPreviewLoader { + mediaSource: root.source + mediaSourceType: root.sourceType + mediaSheet: root + } + } } // TODO: - Maybe add option to change file name // - Enabled/Disable image compression // caption/description text field // disabled for now; most other clients (currently) don't support this Controls.TextField { id: descField visible: false placeholderText: qsTr("Caption") selectByMouse: true Layout.fillWidth: true Layout.topMargin: Kirigami.Units.largeSpacing } // buttons for send/cancel RowLayout { Layout.topMargin: Kirigami.Units.largeSpacing Layout.fillWidth: true Controls.Button { text: qsTr("Cancel") Layout.fillWidth: true - onClicked: close() + onClicked: { + close() + root.rejected() + } } Controls.Button { id: sendButton + enabled: root.source != '' text: qsTr("Send") Layout.fillWidth: true onClicked: { switch (root.sourceType) { case Enums.MessageType.MessageUnknown: case Enums.MessageType.MessageText: - case Enums.MessageType.MessageGeoLocation: break case Enums.MessageType.MessageImage: case Enums.MessageType.MessageAudio: case Enums.MessageType.MessageVideo: case Enums.MessageType.MessageFile: case Enums.MessageType.MessageDocument: kaidan.sendFile(root.targetJid, root.source, descField.text) break + case Enums.MessageType.MessageGeoLocation: + kaidan.sendMessage(root.targetJid, root.source, false, '') + break } close() + root.accepted() } } } Keys.onPressed: { if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { sendButton.clicked() } } } onSheetOpenChanged: { if (!sheetOpen) { targetJid = '' source = '' sourceType = Enums.MessageType.MessageUnknown + newMedia = false descField.clear() } } function sendMessageType(jid, type) { targetJid = jid sourceType = type open() } + function sendNewMessageType(jid, type) { + newMedia = true + sendMessageType(jid, type) + } + function sendFile(jid, url) { source = url sendMessageType(jid, MediaUtilsInstance.messageType(url)) } } diff --git a/src/qml/main.qml b/src/qml/main.qml index 5e8c2f9..8c78ed5 100644 --- a/src/qml/main.qml +++ b/src/qml/main.qml @@ -1,133 +1,134 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 Kaidan developers and contributors * (see the LICENSE file for a full list of copyright authors) * * Kaidan 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. * * In addition, as a special exception, the author of Kaidan gives * permission to link the code of its release with the OpenSSL * project's "OpenSSL" library (or with modified versions of it that * use the same license as the "OpenSSL" library), and distribute the * linked executables. You must obey the GNU General Public License in * all respects for all of the code used other than "OpenSSL". If you * modify this file, you may extend this exception to your version of * the file, but you are not obligated to do so. If you do not wish to * do so, delete this exception statement from your version. * * Kaidan 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 Kaidan. If not, see . */ import QtQuick 2.7 import QtQuick.Controls.Material 2.0 import org.kde.kirigami 2.3 as Kirigami import StatusBar 0.1 import im.kaidan.kaidan 1.0 import "elements" import "settings" Kirigami.ApplicationWindow { id: root StatusBar { color: Material.color(Material.Green, Material.Shade700) } // Global and Contextual Drawers globalDrawer: GlobalDrawer {} contextDrawer: Kirigami.ContextDrawer { id: contextDrawer } AboutDialog { id: aboutDialog focus: true x: (parent.width - width) / 2 y: (parent.height - height) / 2 } SubRequestAcceptSheet { id: subReqAcceptSheet } // when the window was closed, disconnect from jabber server onClosing: { kaidan.mainDisconnect() } // load all pages Component {id: chatPage; ChatPage {}} Component {id: loginPage; LoginPage {}} Component {id: rosterPage; RosterPage {}} Component {id: emptyChatPage; EmptyChatPage {}} Component {id: settingsPage; SettingsPage {}} Component {id: qrCodeScannerPage; QrCodeScannerPage {}} Component {id: userProfilePage; UserProfilePage {}} + Component {id: multimediaSettingsPage; MultimediaSettingsPage {}} function passiveNotification(text) { showPassiveNotification(text, "long") } function openLogInPage() { // close all pages (we don't know on which page we're on, // thus we don't use replace) while (pageStack.depth > 0) pageStack.pop() // toggle global drawer globalDrawer.enabled = false globalDrawer.visible = false // push new page pageStack.push(loginPage) } function closeLogInPage() { // toggle global drawer globalDrawer.enabled = true // replace page with roster page pageStack.replace(rosterPage) if (!Kirigami.Settings.isMobile) pageStack.push(emptyChatPage) } function handleSubRequest(from, message) { kaidan.vCardRequested(from) subReqAcceptSheet.from = from subReqAcceptSheet.message = message subReqAcceptSheet.open() } Component.onCompleted: { kaidan.passiveNotificationRequested.connect(passiveNotification) kaidan.newCredentialsNeeded.connect(openLogInPage) kaidan.logInWorked.connect(closeLogInPage) kaidan.subscriptionRequestReceived.connect(handleSubRequest) // push roster page (trying normal start up) pageStack.push(rosterPage) if (!Kirigami.Settings.isMobile) pageStack.push(emptyChatPage) // Annouce that we're ready and the back-end can start with connecting kaidan.start() } Component.onDestruction: { kaidan.passiveNotificationRequested.disconnect(passiveNotification) kaidan.newCredentialsNeeded.disconnect(openLogInPage) kaidan.logInWorked.disconnect(closeLogInPage) kaidan.subscriptionRequestReceived.disconnect(handleSubRequest) } } diff --git a/utils/build-linux-appimage.sh b/utils/build-linux-appimage.sh index ca1103b..a0b0de2 100755 --- a/utils/build-linux-appimage.sh +++ b/utils/build-linux-appimage.sh @@ -1,102 +1,136 @@ #!/bin/bash -e # NOTE: To use this script, you need to set $QT_LINUX to your Qt for Linux installation # Path to Qt installation QT_LINUX=${QT_LINUX:-/usr} echo Using Qt installation from $QT_LINUX # Build type is one of: # Debug, Release, RelWithDebInfo and MinSizeRel BUILD_TYPE="${BUILD_TYPE:-Debug}" KAIDAN_SOURCES=$(dirname "$(readlink -f "${0}")")/.. echo "-- Starting $BUILD_TYPE build of Kaidan --" echo "*****************************************" echo "Fetching dependencies if required" echo "*****************************************" if [ ! -f "$KAIDAN_SOURCES/3rdparty/kirigami/.git" ] || [ ! -f "$KAIDAN_SOURCES/3rdparty/breeze-icons/.git" ]; then echo "Cloning Kirigami and Breeze icons" git submodule update --init fi if [ ! -e "$KAIDAN_SOURCES/3rdparty/qxmpp/.git" ]; then echo "Cloning QXmpp" git clone https://github.com/qxmpp-project/qxmpp.git 3rdparty/qxmpp fi cdnew() { if [ -d "$1" ]; then rm -rf "$1" fi mkdir $1 cd $1 } +join_by() { + local IFS="$1" + shift + echo "$*" +} + if [ ! -f "$KAIDAN_SOURCES/3rdparty/linuxdeployqt/squashfs-root/AppRun" ]; then echo "Downloading linuxdeployqt" wget --continue -P $KAIDAN_SOURCES/3rdparty/ https://github.com/probonopd/linuxdeployqt/releases/download/continuous/linuxdeployqt-continuous-x86_64.AppImage chmod +x $KAIDAN_SOURCES/3rdparty/linuxdeployqt-continuous-x86_64.AppImage echo "Extracting linuxdeployqt" cdnew $KAIDAN_SOURCES/3rdparty/linuxdeployqt $KAIDAN_SOURCES/3rdparty/linuxdeployqt-continuous-x86_64.AppImage --appimage-extract cd $KAIDAN_SOURCES fi export QT_SELECT=qt5 if [ ! -f "$KAIDAN_SOURCES/build/bin/kaidan" ]; then echo "*****************************************" echo "Building Kaidan" echo "*****************************************" { cdnew $KAIDAN_SOURCES/build cmake .. \ -DECM_DIR=/usr/share/ECM/cmake \ -DCMAKE_PREFIX_PATH=$QT_LINUX\; \ -DI18N=1 \ -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DCMAKE_INSTALL_PREFIX=/usr \ -DQUICK_COMPILER=ON -DAPPIMAGE=ON -DBUNDLE_ICONS=ON make -j$(nproc) } fi if [ ! -f "$KAIDAN_SOURCES/AppDir/usr/bin/kaidan" ]; then echo "*****************************************" echo "Installing Kaidan" echo "*****************************************" { cd $KAIDAN_SOURCES/build DESTDIR=$KAIDAN_SOURCES/AppDir cmake --build . --target install } fi echo "*****************************************" echo "Packing into AppImage" echo "*****************************************" { cd $KAIDAN_SOURCES export LD_LIBRARY_PATH=$QT_LINUX/lib/:$LD_LIBRARY_PATH export PATH=$QT_LINUX/bin/:$PATH # set qmake binary when using portable Qt; linuxdeployqt will find it on its # own on global installs if [ -f $QT_LINUX/bin/qmake ]; then QMAKE_BINARY="-qmake=$QT_LINUX/bin/qmake" fi export VERSION="continuous" + extra_plugins=( + # Image formats + imageformats/libqsvg.so + imageformats/libqjpeg.so + imageformats/libqgif.so + imageformats/libqwebp.so + # Icon formats + iconengines/libqsvgicon.so + # QtMultimedia + audio/libqtaudio_alsa.so + audio/libqtmedia_pulse.so + playlistformats/libqtmultimedia_m3u.so + mediaservice/libgstaudiodecoder.so + mediaservice/libgstcamerabin.so + mediaservice/libgstmediacapture.so + mediaservice/libgstmediaplayer.so + # QtLocation + geoservices/libqtgeoservices_esri.so + geoservices/libqtgeoservices_itemsoverlay.so + geoservices/libqtgeoservices_mapbox.so + geoservices/libqtgeoservices_mapboxgl.so + geoservices/libqtgeoservices_nokia.so + geoservices/libqtgeoservices_osm.so + # QtPositioning + position/libqtposition_geoclue.so + position/libqtposition_positionpoll.so + position/libqtposition_serialnmea.so + ) $KAIDAN_SOURCES/3rdparty/linuxdeployqt/squashfs-root/AppRun \ $KAIDAN_SOURCES/AppDir/usr/share/applications/kaidan.desktop \ -qmldir=$KAIDAN_SOURCES/src/qml/ \ -qmlimport=/opt/kf5/lib/x86_64-linux-gnu/qml \ - -extra-plugins="imageformats/libqsvg.so,imageformats/libqjpeg.so,iconengines/libqsvgicon.so" \ + -extra-plugins="$(join_by , "${extra_plugins[@]}")" \ -appimage -no-copy-copyright-files \ $QMAKE_BINARY }