From 62056b6124d159044762549e04c99e05dc8bccca Mon Sep 17 00:00:00 2001 From: miruka Date: Mon, 15 Jul 2019 16:14:08 -0400 Subject: [PATCH] Avatar change working --- TODO.md | 14 ++++++++ harmonyqml.pro | 3 +- src/main.cpp | 4 +-- src/python/matrix_client.py | 44 ++++++++++++++++++++++++- src/qml/Base/HAvatar.qml | 20 ++++++++---- src/qml/Base/HFileDialogOpener.qml | 46 +++++++++++++++++++++++++++ src/qml/Base/HToolTip.qml | 2 +- src/qml/Base/HUIButton.qml | 8 +---- src/qml/Base/HUserAvatar.qml | 11 ++++--- src/qml/Pages/EditAccount/Profile.qml | 43 +++++++++++++++++++++---- src/qml/Window.qml | 9 +++--- 11 files changed, 171 insertions(+), 33 deletions(-) create mode 100644 src/qml/Base/HFileDialogOpener.qml diff --git a/TODO.md b/TODO.md index 88e7aee9..b00e0e21 100644 --- a/TODO.md +++ b/TODO.md @@ -7,6 +7,18 @@ - normalSpacing in Theme - Qt.AlignCenter instead of V | H - banner button repair +- Wrong avatar for group rooms +- Can assign "" to an Image source +- Make sure to not cache user images and that sourceSize is set everywhere +- Reduce messages ListView cacheBuffer height once http thumbnails + downloading is implemented +- HTextField focus effect +- Button can get "hoverEnabled: false" to let HoverHandlers work +- Center account box buttons +- Handle TimeoutError for all kind of async requests (nio) +- Handle thumbnail response status 400 +- "Loading..." if going to edit account page while it's loading +- Improve avatar tooltips position, add stuff to room tooltips (last msg?) - Qt 5.12 - New input handlers @@ -50,6 +62,8 @@ - Multiaccount aliases - Message/text selection + - Custom file picker for Linux... + - Major features - E2E - Device verification diff --git a/harmonyqml.pro b/harmonyqml.pro index ca66526b..1d40708d 100644 --- a/harmonyqml.pro +++ b/harmonyqml.pro @@ -1,5 +1,6 @@ TEMPLATE = app -QT = quick +# widgets: Make native file dialogs available to QML (must use QApplication) +QT = quick widgets DEFINES += QT_DEPRECATED_WARNINGS CONFIG += warn_off c++11 release dev { diff --git a/src/main.cpp b/src/main.cpp index e483dd81..846389a0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,7 +1,7 @@ // Copyright 2019 miruka // This file is part of harmonyqml, licensed under LGPLv3. -#include +#include #include #include #include @@ -10,7 +10,7 @@ int main(int argc, char *argv[]) { - QGuiApplication app(argc, argv); + QApplication app(argc, argv); QQmlEngine engine; QQmlContext *objectContext = new QQmlContext(engine.rootContext()); diff --git a/src/python/matrix_client.py b/src/python/matrix_client.py index 278e603e..e6f79642 100644 --- a/src/python/matrix_client.py +++ b/src/python/matrix_client.py @@ -9,10 +9,14 @@ import logging as log import platform from contextlib import suppress from datetime import datetime +from enum import Enum +from pathlib import Path from types import ModuleType -from typing import DefaultDict, Dict, Optional, Type +from typing import DefaultDict, Dict, Optional, Type, Union from uuid import uuid4 +import filetype + import nio from nio.rooms import MatrixRoom @@ -22,6 +26,12 @@ from .events.rooms import TimelineEventReceived, TimelineMessageReceived from .html_filter import HTML_FILTER +class UploadError(Enum): + forbidden = "M_FORBIDDEN" + too_large = "M_TOO_LARGE" + unknown = "UNKNOWN" + + class MatrixClient(nio.AsyncClient): def __init__(self, backend, @@ -207,6 +217,38 @@ class MatrixClient(nio.AsyncClient): rooms.RoomForgotten(user_id=self.user_id, room_id=room_id) + async def upload_file(self, path: Union[Path, str]) -> str: + path = Path(path) + + with open(path, "rb") as file: + mime = filetype.guess_mime(file) + file.seek(0, 0) + + resp = await self.upload(file, mime, path.name) + + if not isinstance(resp, nio.ErrorResponse): + return resp.content_uri + + if resp.status_code == 403: + return UploadError.forbidden.value + + if resp.status_code == 413: + return UploadError.too_large.value + + return UploadError.unknown.value + + + async def set_avatar_from_file(self, path: Union[Path, str] + ) -> Union[bool, str]: + resp = await self.upload_file(path) + + if resp in (i.value for i in UploadError): + return resp + + await self.set_avatar(resp) + return True + + # Callbacks for nio responses async def onSyncResponse(self, resp: nio.SyncResponse) -> None: diff --git a/src/qml/Base/HAvatar.qml b/src/qml/Base/HAvatar.qml index 1a59bc83..341d5a6f 100644 --- a/src/qml/Base/HAvatar.qml +++ b/src/qml/Base/HAvatar.qml @@ -54,15 +54,23 @@ HRectangle { HToolTip { id: avatarToolTip visible: toolTipImageUrl && hoverHandler.hovered - width: 128 - height: 128 + width: 192 + background.border.width * 2 + height: width + delay: 1000 + + background: HRectangle { + id: background + border.color: "black" + border.width: 2 + } HImage { id: avatarToolTipImage - width: parent.width - height: parent.height - sourceSize.width: parent.width - sourceSize.height: parent.height + anchors.centerIn: parent + sourceSize.width: parent.width - background.border.width * 2 + sourceSize.height: parent.height - background.border.width * 2 + width: sourceSize.width + height: sourceSize.width fillMode: Image.PreserveAspectCrop } } diff --git a/src/qml/Base/HFileDialogOpener.qml b/src/qml/Base/HFileDialogOpener.qml new file mode 100644 index 00000000..d51a5029 --- /dev/null +++ b/src/qml/Base/HFileDialogOpener.qml @@ -0,0 +1,46 @@ +// Copyright 2019 miruka +// This file is part of harmonyqml, licensed under LGPLv3. + +import QtQuick 2.12 +import Qt.labs.platform 1.1 + +Item { + anchors.fill: parent + + property alias dialog: fileDialog + property var selectedFile: null + + enum FileType { All, Images } + property int fileType: FileType.All + + TapHandler { onTapped: fileDialog.open() } + + FileDialog { + id: fileDialog + + property var filters: ({ + all: qsTr("All files") + " (*)", + images: qsTr("Image files") + + " (*.jpg *.jpeg *.png *.gif *.bmp *.webp)" + }) + + nameFilters: + fileType == HFileDialogOpener.FileType.Images ? + [filters.images, filters.all] : + [filters.all] + + folder: StandardPaths.writableLocation( + fileType == HFileDialogOpener.FileType.Images ? + StandardPaths.PicturesLocation : + StandardPaths.HomeLocation + ) + + title: "Select file" + modality: Qt.WindowModal + + onVisibleChanged: if (visible) { + selectedFile = Qt.binding(() => Qt.resolvedUrl(currentFile)) + } + onRejected: selectedFile = null + } +} diff --git a/src/qml/Base/HToolTip.qml b/src/qml/Base/HToolTip.qml index c37eaa83..8bf8745a 100644 --- a/src/qml/Base/HToolTip.qml +++ b/src/qml/Base/HToolTip.qml @@ -6,7 +6,7 @@ ToolTip { // going out of the window's boundaries id: toolTip - delay: Qt.styleHints.mousePressAndHoldInterval + delay: 150 padding: 0 enter: Transition { diff --git a/src/qml/Base/HUIButton.qml b/src/qml/Base/HUIButton.qml index 387eee4a..91e3a501 100644 --- a/src/qml/Base/HUIButton.qml +++ b/src/qml/Base/HUIButton.qml @@ -38,18 +38,12 @@ HBaseButton { svgName: loading ? "hourglass" : iconName dimension: iconDimension || contentLayout.height transform: iconTransform + opacity: button.enabled ? 1 : 0.7 Layout.topMargin: verticalMargin Layout.bottomMargin: verticalMargin Layout.leftMargin: horizontalMargin Layout.rightMargin: horizontalMargin - - // Colorize { - // anchors.fill: parent - // source: parent - // visible: ! button.enabled - // saturation: 0 - // } } HLabel { diff --git a/src/qml/Base/HUserAvatar.qml b/src/qml/Base/HUserAvatar.qml index 716d10b0..2e807899 100644 --- a/src/qml/Base/HUserAvatar.qml +++ b/src/qml/Base/HUserAvatar.qml @@ -7,15 +7,16 @@ HAvatar { property string userId: "" readonly property var userInfo: userId ? users.find(userId) : ({}) - name: - userInfo.displayName || userId.substring(1) // no leading @ - - imageUrl: + readonly property var defaultImageUrl: userInfo.avatarUrl ? ("image://python/" + userInfo.avatarUrl) : null - toolTipImageUrl: + readonly property var defaultToolTipImageUrl: userInfo.avatarUrl ? ("image://python/" + userInfo.avatarUrl) : null + name: userInfo.displayName || userId.substring(1) // no leading @ + imageUrl: defaultImageUrl + toolTipImageUrl:defaultToolTipImageUrl + //HImage { //id: status //anchors.right: parent.right diff --git a/src/qml/Pages/EditAccount/Profile.qml b/src/qml/Pages/EditAccount/Profile.qml index 1eaf9748..2b6d0bf2 100644 --- a/src/qml/Pages/EditAccount/Profile.qml +++ b/src/qml/Pages/EditAccount/Profile.qml @@ -9,12 +9,27 @@ import "../../utils.js" as Utils HGridLayout { function applyChanges() { - saveButton.loading = true + if (nameField.changed) { + saveButton.nameChangeRunning = true - py.callClientCoro( - userId, "set_displayname", [nameField.field.text], - () => { saveButton.loading = false } - ) + py.callClientCoro( + userId, "set_displayname", [nameField.field.text], () => { + saveButton.nameChangeRunning = false + } + ) + } + + if (avatar.changed) { + saveButton.avatarChangeRunning = true + var path = Qt.resolvedUrl(avatar.imageUrl).replace(/^file:/, "") + + py.callClientCoro( + userId, "set_avatar_from_file", [path], response => { + saveButton.avatarChangeRunning = false + if (response != true) { print(response) } + } + ) + } } columns: 2 @@ -24,8 +39,11 @@ HGridLayout { Component.onCompleted: nameField.field.forceActiveFocus() HUserAvatar { + property bool changed: avatar.imageUrl != avatar.defaultImageUrl + id: avatar userId: editAccount.userId + imageUrl: fileDialog.selectedFile || defaultImageUrl toolTipImageUrl: null Layout.alignment: Qt.AlignHCenter @@ -33,6 +51,13 @@ HGridLayout { Layout.preferredWidth: thinMaxWidth Layout.preferredHeight: Layout.preferredWidth + + HFileDialogOpener { + id: fileDialog + fileType: HFileDialogOpener.FileType.Images + dialog.title: qsTr("Select profile picture for %1") + .arg(userInfo.displayName) + } } HColumnLayout { @@ -53,6 +78,8 @@ HGridLayout { } HLabeledTextField { + property bool changed: field.text != userInfo.displayName + id: nameField label.text: qsTr("Display name:") field.text: userInfo.displayName @@ -69,11 +96,15 @@ HGridLayout { Layout.alignment: Qt.AlignBottom HUIButton { + property bool nameChangeRunning: false + property bool avatarChangeRunning: false + id: saveButton iconName: "save" text: qsTr("Save") centerText: false - enabled: nameField.field.text != userInfo.displayName + loading: nameChangeRunning || avatarChangeRunning + enabled: nameField.changed || avatar.changed Layout.fillWidth: true Layout.alignment: Qt.AlignBottom diff --git a/src/qml/Window.qml b/src/qml/Window.qml index eac8b2b4..d9506701 100644 --- a/src/qml/Window.qml +++ b/src/qml/Window.qml @@ -18,10 +18,11 @@ ApplicationWindow { property bool ready: false Component.onCompleted: { - Qt.application.name = "harmonyqml" - Qt.application.displayName = "Harmony QML" - Qt.application.version = "0.1.0" - window.ready = true + Qt.application.organization = "harmonyqml" + Qt.application.name = "harmonyqml" + Qt.application.displayName = "Harmony QML" + Qt.application.version = "0.1.0" + window.ready = true } Theme { id: theme }