diff --git a/src/qml/ChatPage.qml b/src/qml/ChatPage.qml
index 84cebdf..23a4931 100644
--- a/src/qml/ChatPage.qml
+++ b/src/qml/ChatPage.qml
@@ -1,673 +1,662 @@
/*
* Kaidan - A user-friendly XMPP client for every device!
*
* Copyright (C) 2016-2020 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.Layouts 1.3
import QtQuick.Controls 2.3 as Controls
import QtGraphicalEffects 1.0
import QtMultimedia 5.8 as Multimedia
import org.kde.kirigami 2.8 as Kirigami
import im.kaidan.kaidan 1.0
import EmojiModel 0.1
import MediaUtils 0.1
import "elements"
ChatPageBase {
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: [
// Action to toggle the message search bar
Kirigami.Action {
id: searchAction
text: qsTr("Search")
icon.name: "search"
onTriggered: {
if (searchBar.active)
searchBar.close()
else
searchBar.open()
}
},
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)
}
},
Kirigami.Action {
visible: !isWritingSpoiler
icon.name: "password-show-off"
text: qsTr("Send a spoiler message")
onTriggered: isWritingSpoiler = true
}
]
// Message search bar
header: Item {
id: searchBar
height: active ? searchField.height + 2 * Kirigami.Units.largeSpacing : 0
clip: true
visible: height != 0
property bool active: false
Behavior on height {
SmoothedAnimation {
velocity: 200
}
}
// Background of the message search bar
Rectangle {
anchors.fill: parent
color: Kirigami.Theme.backgroundColor
}
/**
* Searches for a message containing the entered text in the search field.
*
* If a message is found for the entered text, that message is highlighted.
* If the upwards search reaches the top of the message list view, the search is restarted at the bottom to search for messages which were not included in the search yet because they were below the message at the start index.
* That behavior is not applied to an upwards search starting from the index of the most recent message (0) to avoid searching twice.
* If the downwards search reaches the bottom of the message list view, the search is restarted at the top to search for messages which were not included in the search yet because they were above the message at the start index.
*
* @param searchUpwards true for searching upwards or false for searching downwards
* @param startIndex index index of the first message to search for the entered text
*/
function search(searchUpwards, startIndex) {
let newIndex = -1
if (searchBar.active && searchField.text.length > 0) {
if (searchUpwards) {
if (startIndex === 0) {
newIndex = kaidan.messageModel.searchForMessageFromNewToOld(searchField.text)
} else {
newIndex = kaidan.messageModel.searchForMessageFromNewToOld(searchField.text, startIndex)
if (newIndex === -1)
newIndex = kaidan.messageModel.searchForMessageFromNewToOld(searchField.text, 0)
}
} else {
newIndex = kaidan.messageModel.searchForMessageFromOldToNew(searchField.text, startIndex)
if (newIndex === -1)
newIndex = kaidan.messageModel.searchForMessageFromOldToNew(searchField.text)
}
}
messageListView.currentIndex = newIndex
}
/**
* Hides the search bar and resets the last search result.
*/
function close() {
messageListView.currentIndex = -1
searchBar.active = false
}
/**
* Shows the search bar and focuses the search field.
*/
function open() {
searchField.forceActiveFocus()
searchBar.active = true
}
/**
* Searches upwards for a message containing the entered text in the search field starting from the current index of the message list view.
*/
function searchUpwardsFromBottom() {
searchBar.search(true, 0)
}
/**
* Searches upwards for a message containing the entered text in the search field starting from the current index of the message list view.
*/
function searchUpwardsFromCurrentIndex() {
searchBar.search(true, messageListView.currentIndex + 1)
}
/**
* Searches downwards for a message containing the entered text in the search field starting from the current index of the message list view.
*/
function searchDownwardsFromCurrentIndex() {
searchBar.search(false, messageListView.currentIndex - 1)
}
// Search field and ist corresponding buttons
RowLayout {
// Anchoring like this binds it to the top of the chat page.
// It makes it look like the search bar slides down from behind of the upper element.
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Kirigami.Units.largeSpacing
Controls.Button {
text: qsTr("Close")
icon.name: "dialog-close"
onClicked: searchBar.close()
display: Controls.Button.IconOnly
flat: true
}
Kirigami.SearchField {
id: searchField
Layout.fillWidth: true
focusSequence: ""
onVisibleChanged: text = ""
onTextChanged: searchBar.searchUpwardsFromBottom()
onAccepted: searchBar.searchUpwardsFromCurrentIndex()
Keys.onUpPressed: searchBar.searchUpwardsFromCurrentIndex()
Keys.onDownPressed: searchBar.searchDownwardsFromCurrentIndex()
Keys.onEscapePressed: searchBar.close()
}
Controls.Button {
text: qsTr("Search up")
icon.name: "go-up"
display: Controls.Button.IconOnly
flat: true
onClicked: {
searchBar.searchUpwardsFromCurrentIndex()
searchField.forceActiveFocus()
}
}
Controls.Button {
text: qsTr("Search down")
icon.name: "go-down"
display: Controls.Button.IconOnly
flat: true
onClicked: {
searchBar.searchDownwardsFromCurrentIndex()
searchField.forceActiveFocus()
}
}
}
}
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
}
}
}
}
}
// View containing the messages
ListView {
id: messageListView
verticalLayoutDirection: ListView.BottomToTop
spacing: Kirigami.Units.smallSpacing * 1.5
// Highlighting of the message containing a searched string.
highlight: Component {
id: highlightBar
Rectangle {
height: messageListView.currentIndex === -1 ? 0 : messageListView.currentItem.height + Kirigami.Units.smallSpacing * 2
width: messageListView.currentIndex === -1 ? 0 : messageListView.currentItem.width + Kirigami.Units.smallSpacing * 2
color: Kirigami.Theme.hoverColor
// This is used to make the highlight bar a little bit bigger than the highlighted message.
// It works only together with "messageListView.highlightFollowsCurrentItem: false".
y: messageListView.currentIndex === -1 ? 0 : messageListView.currentItem.y - Kirigami.Units.smallSpacing
x: messageListView.currentIndex === -1 ? 0 : messageListView.currentItem.x
Behavior on y {
SmoothedAnimation {
velocity: 1000
duration: 500
}
}
Behavior on height {
SmoothedAnimation {
velocity: 1000
duration: 500
}
}
}
}
// This is used to make the highlight bar a little bit bigger than the highlighted message.
highlightFollowsCurrentItem: false
// Initially highlighted value
currentIndex: -1
// Connect to 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"
}
onQuoteRequested: {
var quotedText = ""
var lines = body.split("\n")
for (var line in lines) {
quotedText += "> " + lines[line] + "\n"
}
messageField.insert(0, quotedText)
}
}
}
// area for writing and sending a message
footer: Controls.Pane {
id: sendingArea
padding: 0
wheelEnabled: true
background: Rectangle {
id: sendingAreaBackground
color: Kirigami.Theme.backgroundColor
}
layer.enabled: sendingArea.enabled
layer.effect: DropShadow {
verticalOffset: 1
color: Qt.darker(sendingAreaBackground.color, 1.2)
samples: 20
spread: 0.3
cached: true // element is static
}
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/main.qml b/src/qml/main.qml
index c191f80..16900ab 100644
--- a/src/qml/main.qml
+++ b/src/qml/main.qml
@@ -1,161 +1,160 @@
/*
* Kaidan - A user-friendly XMPP client for every device!
*
* Copyright (C) 2016-2020 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.3
import org.kde.kirigami 2.8 as Kirigami
import StatusBar 0.1
import im.kaidan.kaidan 1.0
import "elements"
import "settings"
Kirigami.ApplicationWindow {
id: root
minimumHeight: 300
minimumWidth: 280
// radius for using rounded corners
readonly property int roundedCornersRadius: Kirigami.Units.smallSpacing * 1.5
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 {}}
/**
* Shows a passive notification for a long period.
*/
function passiveNotification(text) {
showPassiveNotification(text, "long")
}
function showPassiveNotificationForConnectionError() {
passiveNotification(Utils.connectionErrorMessage(kaidan.connectionError))
}
function openLoginPage() {
globalDrawer.enabled = false
globalDrawer.visible = false
popLayersAboveLowest()
popAllPages()
pageStack.push(loginPage)
}
/**
* Opens the view with the roster and chat page.
*/
function openChatView() {
globalDrawer.enabled = true
popAllPages()
pageStack.push(rosterPage)
if (!Kirigami.Settings.isMobile)
pageStack.push(emptyChatPage)
}
/**
* Pops all layers except the layer with index 0 from the page stack.
*/
function popLayersAboveLowest() {
while (pageStack.layers.depth > 1)
pageStack.layers.pop()
}
/**
* Pops all pages from the page stack.
*/
function popAllPages() {
while (pageStack.depth > 0)
pageStack.pop()
}
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(openChatView)
kaidan.subscriptionRequestReceived.connect(handleSubRequest)
openChatView()
// Announce that the user interface is ready and the application can start connecting.
kaidan.start()
}
Component.onDestruction: {
kaidan.passiveNotificationRequested.disconnect(passiveNotification)
kaidan.newCredentialsNeeded.disconnect(openLoginPage)
kaidan.logInWorked.disconnect(openChatView)
kaidan.subscriptionRequestReceived.disconnect(handleSubRequest)
}
}
diff --git a/src/qml/qml.qrc b/src/qml/qml.qrc
index cb243a1..ad46b51 100644
--- a/src/qml/qml.qrc
+++ b/src/qml/qml.qrc
@@ -1,59 +1,59 @@
main.qml
RosterPage.qml
LoginPage.qml
ChatPageBase.qml
ChatPage.qml
EmptyChatPage.qml
AboutDialog.qml
GlobalDrawer.qml
QrCodeScannerPage.qml
UserProfilePage.qml
- MultimediaSettingsPage.qml
elements/SubRequestAcceptSheet.qml
elements/RosterAddContactSheet.qml
elements/RosterRenameContactSheet.qml
elements/RosterRemoveContactSheet.qml
elements/RosterListItem.qml
elements/MessageCounter.qml
elements/ChatMessage.qml
elements/RoundedImage.qml
elements/RoundImage.qml
elements/Button.qml
elements/CenteredAdaptiveButton.qml
elements/CenteredAdaptiveHighlightedButton.qml
elements/IconButton.qml
elements/FileChooser.qml
elements/FileChooserDesktop.qml
elements/FileChooserMobile.qml
elements/SendMediaSheet.qml
elements/MediaPreview.qml
elements/MediaPreviewImage.qml
elements/MediaPreviewAudio.qml
elements/MediaPreviewVideo.qml
elements/MediaPreviewOther.qml
elements/MediaPreviewLocation.qml
elements/MediaPreviewLoader.qml
elements/NewMedia.qml
elements/NewMediaLocation.qml
elements/NewMediaLoader.qml
elements/EmojiPicker.qml
elements/TextAvatar.qml
elements/Avatar.qml
elements/fields/Field.qml
elements/fields/CredentialsField.qml
elements/fields/JidField.qml
elements/fields/PasswordField.qml
settings/SettingsItem.qml
settings/SettingsPage.qml
settings/SettingsSheet.qml
settings/ChangePassword.qml
+ settings/MultimediaSettings.qml
diff --git a/src/qml/MultimediaSettingsPage.qml b/src/qml/settings/MultimediaSettings.qml
similarity index 96%
rename from src/qml/MultimediaSettingsPage.qml
rename to src/qml/settings/MultimediaSettings.qml
index 6952666..fe6ca5d 100644
--- a/src/qml/MultimediaSettingsPage.qml
+++ b/src/qml/settings/MultimediaSettings.qml
@@ -1,650 +1,627 @@
/*
* Kaidan - A user-friendly XMPP client for every device!
*
* Copyright (C) 2016-2020 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.Layouts 1.3
import QtQuick.Controls 2.3 as Controls
import QtMultimedia 5.8 as Multimedia
import org.kde.kirigami 2.8 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)) {
+ stack.pop()
recorder.saveUserSettings()
}
}
}
}
}
Component.onCompleted: {
recorder.type = MediaRecorder.Type.Image
}
}
diff --git a/src/qml/settings/SettingsItem.qml b/src/qml/settings/SettingsItem.qml
index fb7c4a6..23ab81e 100644
--- a/src/qml/settings/SettingsItem.qml
+++ b/src/qml/settings/SettingsItem.qml
@@ -1,63 +1,61 @@
/*
* Kaidan - A user-friendly XMPP client for every device!
*
* Copyright (C) 2016-2020 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 2.3 as Controls
import QtQuick.Layouts 1.3
import org.kde.kirigami 2.8 as Kirigami
Kirigami.BasicListItem {
property string name
property string description
- reserveSpaceForIcon: false
+ reserveSpaceForIcon: icon
- RowLayout {
- ColumnLayout {
- Kirigami.Heading {
- text: name
- textFormat: Text.PlainText
- elide: Text.ElideRight
- maximumLineCount: 1
- level: 2
- Layout.fillWidth: true
- Layout.maximumHeight: Kirigami.Units.gridUnit * 1.5
- }
+ ColumnLayout {
+ Layout.fillHeight: true
+ Kirigami.Heading {
+ text: name
+ textFormat: Text.PlainText
+ elide: Text.ElideRight
+ maximumLineCount: 1
+ level: 2
+ Layout.fillWidth: true
+ Layout.maximumHeight: Kirigami.Units.gridUnit * 1.5
+ }
- Controls.Label {
- Layout.fillWidth: true
- text: description
- wrapMode: Text.WordWrap
- textFormat: Text.PlainText
- font.pixelSize: 14
- }
+ Controls.Label {
+ Layout.fillWidth: true
+ text: description
+ wrapMode: Text.WordWrap
+ textFormat: Text.PlainText
}
}
}
diff --git a/src/qml/settings/SettingsPage.qml b/src/qml/settings/SettingsPage.qml
index f3faf1e..6e21951 100644
--- a/src/qml/settings/SettingsPage.qml
+++ b/src/qml/settings/SettingsPage.qml
@@ -1,71 +1,74 @@
/*
* Kaidan - A user-friendly XMPP client for every device!
*
* Copyright (C) 2016-2020 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 2.3 as Controls
import QtQuick.Layouts 1.3
import org.kde.kirigami 2.8 as Kirigami
import im.kaidan.kaidan 1.0
/**
* The settings page contains options to configure Kaidan.
*
* It is used on a new layer on mobile and inside of a Sheet on desktop.
*/
Kirigami.Page {
title: qsTr("Settings")
leftPadding: 0
topPadding: 0
rightPadding: 0
bottomPadding: 0
Controls.StackView {
id: stack
anchors.fill: parent
initialItem: settingsContent
clip: true
}
Component {
id: settingsContent
- ColumnLayout {
+ Column {
spacing: 0
SettingsItem {
- Layout.alignment: Qt.AlignTop
name: qsTr("Change password")
description: qsTr("Changes your account's password. You will need to re-enter it on your other devices.")
- onClicked: stack.push(changePassword)
+ onClicked: stack.push("ChangePassword.qml")
icon: "lock"
- reserveSpaceForIcon: true
+ }
+ SettingsItem {
+ name: qsTr("Multimedia Settings")
+ description: qsTr("Configure photo, video and audio recording settings")
+ onClicked: stack.push("MultimediaSettings.qml")
+ icon: "settings-configure"
}
}
}
- Component {id: changePassword; ChangePassword {}}
}
diff --git a/src/qml/settings/SettingsSheet.qml b/src/qml/settings/SettingsSheet.qml
index ab236e9..3460870 100644
--- a/src/qml/settings/SettingsSheet.qml
+++ b/src/qml/settings/SettingsSheet.qml
@@ -1,53 +1,53 @@
/*
* Kaidan - A user-friendly XMPP client for every device!
*
* Copyright (C) 2016-2020 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 2.3 as Controls
import QtQuick.Layouts 1.3
import org.kde.kirigami 2.8 as Kirigami
/**
* This sheet is used on desktop systems instead of a new layer. It doesn't
* fill the complete width, so it looks a bit nicer on large screens.
*/
Kirigami.OverlaySheet {
- ColumnLayout {
- Kirigami.Heading {
- Layout.fillWidth: true
- text: settingsPage.title
- }
-
+ header: Kirigami.Heading {
+ text: settingsPage.title
+ }
+ contentItem: ColumnLayout {
SettingsPage {
Layout.preferredHeight: root.height * 0.9
- Layout.maximumHeight: 500
+ Layout.preferredWidth: Layout.maximumWidth
+ Layout.maximumWidth: 600
+ Layout.maximumHeight: 600
id: settingsPage
}
}
}