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 } } }