Merge account settings Encryption & Sessions page
This commit is contained in:
		@@ -12,15 +12,15 @@ Item {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    Item {
 | 
					    Item {
 | 
				
			||||||
        id: clipArea
 | 
					        id: clipArea
 | 
				
			||||||
        anchors.bottom: parent.bottom
 | 
					        anchors.bottom: parent ? parent.bottom : undefined
 | 
				
			||||||
        width: parent.width
 | 
					        width: parent ? parent.width : 0
 | 
				
			||||||
        height: 1
 | 
					        height: 1
 | 
				
			||||||
        clip: true
 | 
					        clip: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Rectangle {
 | 
					        Rectangle {
 | 
				
			||||||
            id: borderRectangle
 | 
					            id: borderRectangle
 | 
				
			||||||
            anchors.bottom: parent.bottom
 | 
					            anchors.bottom: parent ? parent.bottom : undefined
 | 
				
			||||||
            width: parent.width
 | 
					            width: parent ? parent.width : 0
 | 
				
			||||||
            height: root.height
 | 
					            height: root.height
 | 
				
			||||||
            radius: rectangle.radius
 | 
					            radius: rectangle.radius
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,12 +19,10 @@ HPage {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        header: HTabBar {
 | 
					        header: HTabBar {
 | 
				
			||||||
            HTabButton { text: qsTr("Account") }
 | 
					            HTabButton { text: qsTr("Account") }
 | 
				
			||||||
            HTabButton { text: qsTr("Encryption") }
 | 
					            HTabButton { text: qsTr("Security") }
 | 
				
			||||||
            HTabButton { text: qsTr("Sessions") }
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Account { userId: page.userId }
 | 
					        Account { userId: page.userId }
 | 
				
			||||||
        Encryption { userId: page.userId }
 | 
					        Security { userId: page.userId }
 | 
				
			||||||
        Sessions { userId: page.userId }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -30,6 +30,7 @@ HTile {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        HCheckBox {
 | 
					        HCheckBox {
 | 
				
			||||||
            id: checkBox
 | 
					            id: checkBox
 | 
				
			||||||
 | 
					            activeFocusOnTab: false
 | 
				
			||||||
            checked: view.checked[model.id] || false
 | 
					            checked: view.checked[model.id] || false
 | 
				
			||||||
            onClicked: view.toggleCheck(model.index)
 | 
					            onClicked: view.toggleCheck(model.index)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -64,6 +65,7 @@ HTile {
 | 
				
			|||||||
            icon.name: "device-action-menu"
 | 
					            icon.name: "device-action-menu"
 | 
				
			||||||
            toolTip.text: qsTr("Rename, verify or sign out")
 | 
					            toolTip.text: qsTr("Rename, verify or sign out")
 | 
				
			||||||
            backgroundColor: "transparent"
 | 
					            backgroundColor: "transparent"
 | 
				
			||||||
 | 
					            activeFocusOnTab: false
 | 
				
			||||||
            onClicked: deviceTile.openMenu()
 | 
					            onClicked: deviceTile.openMenu()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            Layout.fillHeight: true
 | 
					            Layout.fillHeight: true
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,7 +19,6 @@ HRowLayout {
 | 
				
			|||||||
    HCheckBox {
 | 
					    HCheckBox {
 | 
				
			||||||
        id: checkBox
 | 
					        id: checkBox
 | 
				
			||||||
        padding: theme.spacing
 | 
					        padding: theme.spacing
 | 
				
			||||||
        topPadding: padding * (section === "current" ? 1 : 2)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        text:
 | 
					        text:
 | 
				
			||||||
            section === "current" ? qsTr("Current session") :
 | 
					            section === "current" ? qsTr("Current session") :
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,60 +0,0 @@
 | 
				
			|||||||
// Copyright Mirage authors & contributors <https://github.com/mirukana/mirage>
 | 
					 | 
				
			||||||
// SPDX-License-Identifier: LGPL-3.0-or-later
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import QtQuick 2.12
 | 
					 | 
				
			||||||
import QtQuick.Layouts 1.12
 | 
					 | 
				
			||||||
import "../../Base"
 | 
					 | 
				
			||||||
import "../../Base/Buttons"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
HFlickableColumnPage {
 | 
					 | 
				
			||||||
    id: page
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    property string userId
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function takeFocus() { exportButton.forceActiveFocus() }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    footer: AutoDirectionLayout {
 | 
					 | 
				
			||||||
        GroupButton {
 | 
					 | 
				
			||||||
            id: exportButton
 | 
					 | 
				
			||||||
            text: qsTr("Export")
 | 
					 | 
				
			||||||
            icon.name: "export-keys"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            onClicked: utils.makeObject(
 | 
					 | 
				
			||||||
                "Dialogs/ExportKeys.qml",
 | 
					 | 
				
			||||||
                page,
 | 
					 | 
				
			||||||
                { userId: page.userId },
 | 
					 | 
				
			||||||
                obj => {
 | 
					 | 
				
			||||||
                    loading = Qt.binding(() => obj.exporting)
 | 
					 | 
				
			||||||
                    obj.dialog.open()
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        GroupButton {
 | 
					 | 
				
			||||||
            text: qsTr("Import")
 | 
					 | 
				
			||||||
            icon.name: "import-keys"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            onClicked: utils.makeObject(
 | 
					 | 
				
			||||||
                "Dialogs/ImportKeys.qml",
 | 
					 | 
				
			||||||
                page,
 | 
					 | 
				
			||||||
                { userId: page.userId },
 | 
					 | 
				
			||||||
                obj => { obj.dialog.open() }
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    HLabel {
 | 
					 | 
				
			||||||
        wrapMode: HLabel.Wrap
 | 
					 | 
				
			||||||
        text: qsTr(
 | 
					 | 
				
			||||||
            "The decryption keys for messages received in encrypted rooms " +
 | 
					 | 
				
			||||||
            "<b>until present time</b> can be saved " +
 | 
					 | 
				
			||||||
            "to a passphrase-protected file.<br><br>" +
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            "You can then import this file on any Matrix account or " +
 | 
					 | 
				
			||||||
            "client, to be able to decrypt these messages again."
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        textFormat: Text.StyledText
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Layout.fillWidth: true
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										327
									
								
								src/gui/Pages/AccountSettings/Security.qml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										327
									
								
								src/gui/Pages/AccountSettings/Security.qml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,327 @@
 | 
				
			|||||||
 | 
					// SPDX-License-Identifier: LGPL-3.0-or-later
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import QtQuick 2.12
 | 
				
			||||||
 | 
					import QtQuick.Controls 2.12
 | 
				
			||||||
 | 
					import QtQuick.Layouts 1.12
 | 
				
			||||||
 | 
					import "../.."
 | 
				
			||||||
 | 
					import "../../Base"
 | 
				
			||||||
 | 
					import "../../Base/Buttons"
 | 
				
			||||||
 | 
					import "../../PythonBridge"
 | 
				
			||||||
 | 
					import "../../ShortcutBundles"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					HColumnPage {
 | 
				
			||||||
 | 
					    id: page
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    property string userId
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    property bool enableFlickShortcuts:
 | 
				
			||||||
 | 
					        SwipeView ? SwipeView.isCurrentItem : true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    property Future loadFuture: null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function takeFocus() {
 | 
				
			||||||
 | 
					        deviceList.headerItem.exportButton.forceActiveFocus()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function loadDevices() {
 | 
				
			||||||
 | 
					        loadFuture = py.callClientCoro(userId, "devices_info", [], devices => {
 | 
				
			||||||
 | 
					            deviceList.uncheckAll()
 | 
				
			||||||
 | 
					            deviceList.model.clear()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            for (const device of devices)
 | 
				
			||||||
 | 
					                deviceList.model.append(device)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            loadFuture                   = null
 | 
				
			||||||
 | 
					            deviceList.sectionItemCounts = getSectionItemCounts()
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function renameDevice(index, name) {
 | 
				
			||||||
 | 
					        const device = deviceList.model.get(index)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        device.display_name = name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        py.callClientCoro(userId, "rename_device", [device.id, name], ok => {
 | 
				
			||||||
 | 
					            if (! ok) deviceList.model.remove(index)  // 404 happened
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function deleteDevices(...indice) {
 | 
				
			||||||
 | 
					        if (indice.length === 1 && indice[0] === 0) {
 | 
				
			||||||
 | 
					            window.makePopup("Popups/SignOutPopup.qml", {userId: page.userId})
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const deviceIds     = []
 | 
				
			||||||
 | 
					        let deleteOwnDevice = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for (const i of indice.sort()) {
 | 
				
			||||||
 | 
					            i === 0 ?
 | 
				
			||||||
 | 
					            deleteOwnDevice = true :
 | 
				
			||||||
 | 
					            deviceIds.push(deviceList.model.get(i).id)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        window.makePopup(
 | 
				
			||||||
 | 
					            "Popups/DeleteDevicesPopup.qml",
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                userId: page.userId,
 | 
				
			||||||
 | 
					                deviceIds,
 | 
				
			||||||
 | 
					                deletedCallback: () => {
 | 
				
			||||||
 | 
					                    deleteOwnDevice ?
 | 
				
			||||||
 | 
					                    window.makePopup(
 | 
				
			||||||
 | 
					                        "Popups/SignOutPopup.qml", { userId: page.userId },
 | 
				
			||||||
 | 
					                    ) :
 | 
				
			||||||
 | 
					                    page.loadDevices()
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function getSectionItemCounts() {
 | 
				
			||||||
 | 
					        const counts = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for (let i = 0; i < deviceList.model.count; i++) {
 | 
				
			||||||
 | 
					            const section = deviceList.model.get(i).type
 | 
				
			||||||
 | 
					            section in counts ? counts[section] += 1 : counts[section] = 1
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return counts
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    enabled: ModelStore.get("accounts").find(userId).presence !== "offline"
 | 
				
			||||||
 | 
					    contentHeight: Math.min(
 | 
				
			||||||
 | 
					        window.height,
 | 
				
			||||||
 | 
					        Math.max(
 | 
				
			||||||
 | 
					            deviceList.contentHeight + deviceList.bottomMargin,
 | 
				
			||||||
 | 
					            busyIndicatorLoader.height + theme.spacing * 2,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Keys.forwardTo: [deviceList]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    HListView {
 | 
				
			||||||
 | 
					        id: deviceList
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Don't bind directly to getSectionItemCounts(), laggy with big list
 | 
				
			||||||
 | 
					        property var sectionItemCounts: ({})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        bottomMargin: theme.spacing
 | 
				
			||||||
 | 
					        clip: true
 | 
				
			||||||
 | 
					        keyNavigationEnabled: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        model: ListModel {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        header: HColumnLayout {
 | 
				
			||||||
 | 
					            readonly property alias exportButton: exportButton
 | 
				
			||||||
 | 
					            readonly property alias signOutCheckedButton: signOutCheckedButton
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            spacing: theme.spacing
 | 
				
			||||||
 | 
					            x: spacing
 | 
				
			||||||
 | 
					            width: deviceList.width - x * 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            HLabel {
 | 
				
			||||||
 | 
					                text: qsTr("Decryption keys")
 | 
				
			||||||
 | 
					                font.pixelSize: theme.fontSize.big
 | 
				
			||||||
 | 
					                wrapMode: HLabel.Wrap
 | 
				
			||||||
 | 
					                topPadding: parent.spacing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                Layout.fillWidth: true
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            HLabel {
 | 
				
			||||||
 | 
					                text: qsTr(
 | 
				
			||||||
 | 
					                    "The decryption keys for messages received in encrypted " +
 | 
				
			||||||
 | 
					                    "rooms <b>until present time</b> can be exported " +
 | 
				
			||||||
 | 
					                    "to a passphrase-protected file.<br><br>" +
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    "You can then import this file on any Matrix account or " +
 | 
				
			||||||
 | 
					                    "application, in order to decrypt these messages again."
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                textFormat: Text.StyledText
 | 
				
			||||||
 | 
					                wrapMode: HLabel.Wrap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                Layout.fillWidth: true
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            AutoDirectionLayout {
 | 
				
			||||||
 | 
					                GroupButton {
 | 
				
			||||||
 | 
					                    id: exportButton
 | 
				
			||||||
 | 
					                    text: qsTr("Export")
 | 
				
			||||||
 | 
					                    icon.name: "export-keys"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    onClicked: utils.makeObject(
 | 
				
			||||||
 | 
					                        "Dialogs/ExportKeys.qml",
 | 
				
			||||||
 | 
					                        page,
 | 
				
			||||||
 | 
					                        { userId: page.userId },
 | 
				
			||||||
 | 
					                        obj => {
 | 
				
			||||||
 | 
					                            loading = Qt.binding(() => obj.exporting)
 | 
				
			||||||
 | 
					                            obj.dialog.open()
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    Keys.onBacktabPressed: {
 | 
				
			||||||
 | 
					                        deviceList.currentIndex = deviceList.count - 1
 | 
				
			||||||
 | 
					                        listController.forceActiveFocus()
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                GroupButton {
 | 
				
			||||||
 | 
					                    text: qsTr("Import")
 | 
				
			||||||
 | 
					                    icon.name: "import-keys"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    onClicked: utils.makeObject(
 | 
				
			||||||
 | 
					                        "Dialogs/ImportKeys.qml",
 | 
				
			||||||
 | 
					                        page,
 | 
				
			||||||
 | 
					                        { userId: page.userId },
 | 
				
			||||||
 | 
					                        obj => { obj.dialog.open() }
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            HLabel {
 | 
				
			||||||
 | 
					                text: qsTr("Sessions")
 | 
				
			||||||
 | 
					                font.pixelSize: theme.fontSize.big
 | 
				
			||||||
 | 
					                wrapMode: HLabel.Wrap
 | 
				
			||||||
 | 
					                topPadding: parent.spacing / 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                Layout.fillWidth: true
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            HLabel {
 | 
				
			||||||
 | 
					                text: qsTr(
 | 
				
			||||||
 | 
					                    "New sessions are created the first time you sign in " +
 | 
				
			||||||
 | 
					                    "from a different device or application."
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                wrapMode: HLabel.Wrap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                Layout.fillWidth: true
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            AutoDirectionLayout {
 | 
				
			||||||
 | 
					                GroupButton {
 | 
				
			||||||
 | 
					                    id: refreshButton
 | 
				
			||||||
 | 
					                    text: qsTr("Refresh")
 | 
				
			||||||
 | 
					                    icon.name: "device-refresh-list"
 | 
				
			||||||
 | 
					                    onClicked: page.loadDevices()
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                NegativeButton {
 | 
				
			||||||
 | 
					                    id: signOutCheckedButton
 | 
				
			||||||
 | 
					                    enabled: deviceList.model.count > 0
 | 
				
			||||||
 | 
					                    text:
 | 
				
			||||||
 | 
					                        deviceList.selectedCount === 0 ?
 | 
				
			||||||
 | 
					                        qsTr("Sign out others") :
 | 
				
			||||||
 | 
					                        qsTr("Sign out checked")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    icon.name: "device-delete-checked"
 | 
				
			||||||
 | 
					                    onClicked:
 | 
				
			||||||
 | 
					                        deviceList.selectedCount ?
 | 
				
			||||||
 | 
					                        page.deleteDevices(...deviceList.checkedIndice) :
 | 
				
			||||||
 | 
					                        page.deleteDevices(
 | 
				
			||||||
 | 
					                            ...utils.range(1, deviceList.count - 1),
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    Keys.onTabPressed: {
 | 
				
			||||||
 | 
					                        deviceList.currentIndex = 0
 | 
				
			||||||
 | 
					                        listController.forceActiveFocus()
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        delegate: DeviceDelegate {
 | 
				
			||||||
 | 
					            width: deviceList.width
 | 
				
			||||||
 | 
					            view: deviceList
 | 
				
			||||||
 | 
					            userId: page.userId
 | 
				
			||||||
 | 
					            onVerified: page.loadDevices()
 | 
				
			||||||
 | 
					            onBlacklisted: page.loadDevices()
 | 
				
			||||||
 | 
					            onRenameRequest: name => page.renameDevice(model.index, name)
 | 
				
			||||||
 | 
					            onDeleteRequest: page.deleteDevices(model.index)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        section.property: "type"
 | 
				
			||||||
 | 
					        section.delegate: DeviceSection {
 | 
				
			||||||
 | 
					            width: deviceList.width
 | 
				
			||||||
 | 
					            view: deviceList
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Component.onCompleted: page.loadDevices()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Layout.fillWidth: true
 | 
				
			||||||
 | 
					        Layout.fillHeight: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Keys.onEscapePressed: uncheckAll()
 | 
				
			||||||
 | 
					        Keys.onSpacePressed: if (currentItem) toggleCheck(currentIndex)
 | 
				
			||||||
 | 
					        Keys.onEnterPressed: if (currentItem) currentItem.openMenu(false)
 | 
				
			||||||
 | 
					        Keys.onReturnPressed: Keys.onEnterPressed(event)
 | 
				
			||||||
 | 
					        Keys.onMenuPressed: Keys.onEnterPressed(event)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Keys.onUpPressed: {
 | 
				
			||||||
 | 
					            deviceList.currentIndex = deviceList.count - 1
 | 
				
			||||||
 | 
					            listController.forceActiveFocus()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Keys.onDownPressed: {
 | 
				
			||||||
 | 
					            deviceList.currentIndex = 0
 | 
				
			||||||
 | 
					            listController.forceActiveFocus()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Item {
 | 
				
			||||||
 | 
					            id: listController
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Keys.onBacktabPressed: {
 | 
				
			||||||
 | 
					                if (parent.currentIndex === 0) {
 | 
				
			||||||
 | 
					                    parent.currentIndex = -1
 | 
				
			||||||
 | 
					                    parent.headerItem.signOutCheckedButton.forceActiveFocus()
 | 
				
			||||||
 | 
					                    return
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                parent.decrementCurrentIndex()
 | 
				
			||||||
 | 
					                forceActiveFocus()
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Keys.onTabPressed: {
 | 
				
			||||||
 | 
					                if (parent.currentIndex === parent.count - 1) {
 | 
				
			||||||
 | 
					                    utils.flickToTop(deviceList)
 | 
				
			||||||
 | 
					                    parent.currentIndex = -1
 | 
				
			||||||
 | 
					                    parent.headerItem.exportButton.forceActiveFocus()
 | 
				
			||||||
 | 
					                    return
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                parent.incrementCurrentIndex()
 | 
				
			||||||
 | 
					                forceActiveFocus()
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Keys.onUpPressed: ev => Keys.onBacktabPressed(ev)
 | 
				
			||||||
 | 
					            Keys.onDownPressed: ev => Keys.onTabPressed(ev)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        HShortcut {
 | 
				
			||||||
 | 
					            sequences: window.settings.keys.refreshDevices
 | 
				
			||||||
 | 
					            onActivated: refreshButton.clicked()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        HShortcut {
 | 
				
			||||||
 | 
					            sequences: window.settings.keys.signOutCheckedOrAllDevices
 | 
				
			||||||
 | 
					            onActivated: signOutCheckedButton.clicked()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        FlickShortcuts {
 | 
				
			||||||
 | 
					            flickable: deviceList
 | 
				
			||||||
 | 
					            active:
 | 
				
			||||||
 | 
					                ! mainUI.debugConsole.visible && page.enableFlickShortcuts
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        HLoader {
 | 
				
			||||||
 | 
					            id: busyIndicatorLoader
 | 
				
			||||||
 | 
					            anchors.centerIn: parent
 | 
				
			||||||
 | 
					            width: 96 * theme.uiScale
 | 
				
			||||||
 | 
					            height: width
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            source: "../../Base/HBusyIndicator.qml"
 | 
				
			||||||
 | 
					            active: page.loadFuture
 | 
				
			||||||
 | 
					            opacity: active ? 1 : 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Behavior on opacity { HNumberAnimation { factor: 2 } }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,194 +0,0 @@
 | 
				
			|||||||
// Copyright Mirage authors & contributors <https://github.com/mirukana/mirage>
 | 
					 | 
				
			||||||
// SPDX-License-Identifier: LGPL-3.0-or-later
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import QtQuick 2.12
 | 
					 | 
				
			||||||
import QtQuick.Controls 2.12
 | 
					 | 
				
			||||||
import QtQuick.Layouts 1.12
 | 
					 | 
				
			||||||
import "../.."
 | 
					 | 
				
			||||||
import "../../Base"
 | 
					 | 
				
			||||||
import "../../Base/Buttons"
 | 
					 | 
				
			||||||
import "../../ShortcutBundles"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
HColumnPage {
 | 
					 | 
				
			||||||
    id: page
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    property string userId
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    property bool enableFlickShortcuts:
 | 
					 | 
				
			||||||
        SwipeView ? SwipeView.isCurrentItem : true
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    property string loadFutureId: ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function takeFocus() {} // TODO
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function loadDevices() {
 | 
					 | 
				
			||||||
        loadFutureId = py.callClientCoro(userId, "devices_info",[],devices => {
 | 
					 | 
				
			||||||
            deviceList.uncheckAll()
 | 
					 | 
				
			||||||
            deviceList.model.clear()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            for (const device of devices)
 | 
					 | 
				
			||||||
                deviceList.model.append(device)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            loadFutureId                 = ""
 | 
					 | 
				
			||||||
            deviceList.sectionItemCounts = getSectionItemCounts()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (page.enabled && ! deviceList.currentItem)
 | 
					 | 
				
			||||||
                deviceList.currentIndex = 0
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function renameDevice(index, name) {
 | 
					 | 
				
			||||||
        const device = deviceList.model.get(index)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        device.display_name = name
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        py.callClientCoro(userId, "rename_device", [device.id, name], ok => {
 | 
					 | 
				
			||||||
            if (! ok) deviceList.model.remove(index)  // 404 happened
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function deleteDevices(...indice) {
 | 
					 | 
				
			||||||
        if (indice.length === 1 && indice[0] === 0) {
 | 
					 | 
				
			||||||
            window.makePopup("Popups/SignOutPopup.qml", {userId: page.userId})
 | 
					 | 
				
			||||||
            return
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const deviceIds     = []
 | 
					 | 
				
			||||||
        let deleteOwnDevice = false
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        for (const i of indice.sort()) {
 | 
					 | 
				
			||||||
            i === 0 ?
 | 
					 | 
				
			||||||
            deleteOwnDevice = true :
 | 
					 | 
				
			||||||
            deviceIds.push(deviceList.model.get(i).id)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        window.makePopup(
 | 
					 | 
				
			||||||
            "Popups/DeleteDevicesPopup.qml",
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                userId: page.userId,
 | 
					 | 
				
			||||||
                deviceIds,
 | 
					 | 
				
			||||||
                deletedCallback: () => {
 | 
					 | 
				
			||||||
                    deleteOwnDevice ?
 | 
					 | 
				
			||||||
                    window.makePopup(
 | 
					 | 
				
			||||||
                        "Popups/SignOutPopup.qml", { userId: page.userId },
 | 
					 | 
				
			||||||
                    ) :
 | 
					 | 
				
			||||||
                    page.loadDevices()
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function getSectionItemCounts() {
 | 
					 | 
				
			||||||
        const counts = {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        for (let i = 0; i < deviceList.model.count; i++) {
 | 
					 | 
				
			||||||
            const section = deviceList.model.get(i).type
 | 
					 | 
				
			||||||
            section in counts ? counts[section] += 1 : counts[section] = 1
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return counts
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    enabled: ModelStore.get("accounts").find(userId).presence !== "offline"
 | 
					 | 
				
			||||||
    contentHeight: Math.min(
 | 
					 | 
				
			||||||
        window.height,
 | 
					 | 
				
			||||||
        Math.max(
 | 
					 | 
				
			||||||
            deviceList.contentHeight + deviceList.bottomMargin,
 | 
					 | 
				
			||||||
            busyIndicatorLoader.height + theme.spacing * 2,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    footer: AutoDirectionLayout {
 | 
					 | 
				
			||||||
        GroupButton {
 | 
					 | 
				
			||||||
            id: refreshButton
 | 
					 | 
				
			||||||
            text: qsTr("Refresh")
 | 
					 | 
				
			||||||
            icon.name: "device-refresh-list"
 | 
					 | 
				
			||||||
            onClicked: page.loadDevices()
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        NegativeButton {
 | 
					 | 
				
			||||||
            id: signOutCheckedButton
 | 
					 | 
				
			||||||
            enabled: deviceList.model.count > 0
 | 
					 | 
				
			||||||
            text:
 | 
					 | 
				
			||||||
                deviceList.selectedCount === 0 ?
 | 
					 | 
				
			||||||
                qsTr("Sign out all") :
 | 
					 | 
				
			||||||
                qsTr("Sign out checked")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            icon.name: "device-delete-checked"
 | 
					 | 
				
			||||||
            onClicked:
 | 
					 | 
				
			||||||
                deviceList.selectedCount ?
 | 
					 | 
				
			||||||
                page.deleteDevices(...deviceList.checkedIndice) :
 | 
					 | 
				
			||||||
                page.deleteDevices(...utils.range(1, deviceList.count - 1))
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Keys.forwardTo: [deviceList]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    HListView {
 | 
					 | 
				
			||||||
        id: deviceList
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Don't bind directly to getSectionItemCounts(), laggy with big list
 | 
					 | 
				
			||||||
        property var sectionItemCounts: ({})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        bottomMargin: theme.spacing
 | 
					 | 
				
			||||||
        clip: true
 | 
					 | 
				
			||||||
        model: ListModel {}
 | 
					 | 
				
			||||||
        delegate: DeviceDelegate {
 | 
					 | 
				
			||||||
            width: deviceList.width
 | 
					 | 
				
			||||||
            view: deviceList
 | 
					 | 
				
			||||||
            userId: page.userId
 | 
					 | 
				
			||||||
            onVerified: page.loadDevices()
 | 
					 | 
				
			||||||
            onBlacklisted: page.loadDevices()
 | 
					 | 
				
			||||||
            onRenameRequest: name => page.renameDevice(model.index, name)
 | 
					 | 
				
			||||||
            onDeleteRequest: page.deleteDevices(model.index)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        section.property: "type"
 | 
					 | 
				
			||||||
        section.delegate: DeviceSection {
 | 
					 | 
				
			||||||
            width: deviceList.width
 | 
					 | 
				
			||||||
            view: deviceList
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Component.onCompleted: page.loadDevices()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Layout.fillWidth: true
 | 
					 | 
				
			||||||
        Layout.fillHeight: true
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Keys.onEscapePressed: uncheckAll()
 | 
					 | 
				
			||||||
        Keys.onTabPressed: incrementCurrentIndex()
 | 
					 | 
				
			||||||
        Keys.onBacktabPressed: decrementCurrentIndex()
 | 
					 | 
				
			||||||
        Keys.onSpacePressed: if (currentItem) toggleCheck(currentIndex)
 | 
					 | 
				
			||||||
        Keys.onEnterPressed: if (currentItem) currentItem.openMenu(false)
 | 
					 | 
				
			||||||
        Keys.onReturnPressed: Keys.onEnterPressed(event)
 | 
					 | 
				
			||||||
        Keys.onMenuPressed: Keys.onEnterPressed(event)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        HShortcut {
 | 
					 | 
				
			||||||
            sequences: window.settings.Keys.Sessions.refresh
 | 
					 | 
				
			||||||
            onActivated: refreshButton.clicked()
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        HShortcut {
 | 
					 | 
				
			||||||
            sequences: window.settings.Keys.Sessions.sign_out_checked_or_all
 | 
					 | 
				
			||||||
            onActivated: signOutCheckedButton.clicked()
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        FlickShortcuts {
 | 
					 | 
				
			||||||
            flickable: deviceList
 | 
					 | 
				
			||||||
            active:
 | 
					 | 
				
			||||||
                ! mainUI.debugConsole.visible && page.enableFlickShortcuts
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        HLoader {
 | 
					 | 
				
			||||||
            id: busyIndicatorLoader
 | 
					 | 
				
			||||||
            anchors.centerIn: parent
 | 
					 | 
				
			||||||
            width: 96 * theme.uiScale
 | 
					 | 
				
			||||||
            height: width
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            source: "../../Base/HBusyIndicator.qml"
 | 
					 | 
				
			||||||
            active: page.loadFutureId
 | 
					 | 
				
			||||||
            opacity: active ? 1 : 0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            Behavior on opacity { HNumberAnimation { factor: 2 } }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
		Reference in New Issue
	
	Block a user