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
}