Avatar change working

This commit is contained in:
miruka 2019-07-15 16:14:08 -04:00
parent 751a27157c
commit 62056b6124
11 changed files with 171 additions and 33 deletions

14
TODO.md
View File

@ -7,6 +7,18 @@
- normalSpacing in Theme - normalSpacing in Theme
- Qt.AlignCenter instead of V | H - Qt.AlignCenter instead of V | H
- banner button repair - 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 - Qt 5.12
- New input handlers - New input handlers
@ -50,6 +62,8 @@
- Multiaccount aliases - Multiaccount aliases
- Message/text selection - Message/text selection
- Custom file picker for Linux...
- Major features - Major features
- E2E - E2E
- Device verification - Device verification

View File

@ -1,5 +1,6 @@
TEMPLATE = app TEMPLATE = app
QT = quick # widgets: Make native file dialogs available to QML (must use QApplication)
QT = quick widgets
DEFINES += QT_DEPRECATED_WARNINGS DEFINES += QT_DEPRECATED_WARNINGS
CONFIG += warn_off c++11 release CONFIG += warn_off c++11 release
dev { dev {

View File

@ -1,7 +1,7 @@
// Copyright 2019 miruka // Copyright 2019 miruka
// This file is part of harmonyqml, licensed under LGPLv3. // This file is part of harmonyqml, licensed under LGPLv3.
#include <QGuiApplication> #include <QApplication>
#include <QQmlEngine> #include <QQmlEngine>
#include <QQmlContext> #include <QQmlContext>
#include <QQmlComponent> #include <QQmlComponent>
@ -10,7 +10,7 @@
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
QGuiApplication app(argc, argv); QApplication app(argc, argv);
QQmlEngine engine; QQmlEngine engine;
QQmlContext *objectContext = new QQmlContext(engine.rootContext()); QQmlContext *objectContext = new QQmlContext(engine.rootContext());

View File

@ -9,10 +9,14 @@ import logging as log
import platform import platform
from contextlib import suppress from contextlib import suppress
from datetime import datetime from datetime import datetime
from enum import Enum
from pathlib import Path
from types import ModuleType from types import ModuleType
from typing import DefaultDict, Dict, Optional, Type from typing import DefaultDict, Dict, Optional, Type, Union
from uuid import uuid4 from uuid import uuid4
import filetype
import nio import nio
from nio.rooms import MatrixRoom from nio.rooms import MatrixRoom
@ -22,6 +26,12 @@ from .events.rooms import TimelineEventReceived, TimelineMessageReceived
from .html_filter import HTML_FILTER from .html_filter import HTML_FILTER
class UploadError(Enum):
forbidden = "M_FORBIDDEN"
too_large = "M_TOO_LARGE"
unknown = "UNKNOWN"
class MatrixClient(nio.AsyncClient): class MatrixClient(nio.AsyncClient):
def __init__(self, def __init__(self,
backend, backend,
@ -207,6 +217,38 @@ class MatrixClient(nio.AsyncClient):
rooms.RoomForgotten(user_id=self.user_id, room_id=room_id) 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 # Callbacks for nio responses
async def onSyncResponse(self, resp: nio.SyncResponse) -> None: async def onSyncResponse(self, resp: nio.SyncResponse) -> None:

View File

@ -54,15 +54,23 @@ HRectangle {
HToolTip { HToolTip {
id: avatarToolTip id: avatarToolTip
visible: toolTipImageUrl && hoverHandler.hovered visible: toolTipImageUrl && hoverHandler.hovered
width: 128 width: 192 + background.border.width * 2
height: 128 height: width
delay: 1000
background: HRectangle {
id: background
border.color: "black"
border.width: 2
}
HImage { HImage {
id: avatarToolTipImage id: avatarToolTipImage
width: parent.width anchors.centerIn: parent
height: parent.height sourceSize.width: parent.width - background.border.width * 2
sourceSize.width: parent.width sourceSize.height: parent.height - background.border.width * 2
sourceSize.height: parent.height width: sourceSize.width
height: sourceSize.width
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
} }
} }

View File

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

View File

@ -6,7 +6,7 @@ ToolTip {
// going out of the window's boundaries // going out of the window's boundaries
id: toolTip id: toolTip
delay: Qt.styleHints.mousePressAndHoldInterval delay: 150
padding: 0 padding: 0
enter: Transition { enter: Transition {

View File

@ -38,18 +38,12 @@ HBaseButton {
svgName: loading ? "hourglass" : iconName svgName: loading ? "hourglass" : iconName
dimension: iconDimension || contentLayout.height dimension: iconDimension || contentLayout.height
transform: iconTransform transform: iconTransform
opacity: button.enabled ? 1 : 0.7
Layout.topMargin: verticalMargin Layout.topMargin: verticalMargin
Layout.bottomMargin: verticalMargin Layout.bottomMargin: verticalMargin
Layout.leftMargin: horizontalMargin Layout.leftMargin: horizontalMargin
Layout.rightMargin: horizontalMargin Layout.rightMargin: horizontalMargin
// Colorize {
// anchors.fill: parent
// source: parent
// visible: ! button.enabled
// saturation: 0
// }
} }
HLabel { HLabel {

View File

@ -7,15 +7,16 @@ HAvatar {
property string userId: "" property string userId: ""
readonly property var userInfo: userId ? users.find(userId) : ({}) readonly property var userInfo: userId ? users.find(userId) : ({})
name: readonly property var defaultImageUrl:
userInfo.displayName || userId.substring(1) // no leading @
imageUrl:
userInfo.avatarUrl ? ("image://python/" + userInfo.avatarUrl) : null userInfo.avatarUrl ? ("image://python/" + userInfo.avatarUrl) : null
toolTipImageUrl: readonly property var defaultToolTipImageUrl:
userInfo.avatarUrl ? ("image://python/" + userInfo.avatarUrl) : null userInfo.avatarUrl ? ("image://python/" + userInfo.avatarUrl) : null
name: userInfo.displayName || userId.substring(1) // no leading @
imageUrl: defaultImageUrl
toolTipImageUrl:defaultToolTipImageUrl
//HImage { //HImage {
//id: status //id: status
//anchors.right: parent.right //anchors.right: parent.right

View File

@ -9,12 +9,27 @@ import "../../utils.js" as Utils
HGridLayout { HGridLayout {
function applyChanges() { function applyChanges() {
saveButton.loading = true if (nameField.changed) {
saveButton.nameChangeRunning = true
py.callClientCoro( py.callClientCoro(
userId, "set_displayname", [nameField.field.text], userId, "set_displayname", [nameField.field.text], () => {
() => { saveButton.loading = false } 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 columns: 2
@ -24,8 +39,11 @@ HGridLayout {
Component.onCompleted: nameField.field.forceActiveFocus() Component.onCompleted: nameField.field.forceActiveFocus()
HUserAvatar { HUserAvatar {
property bool changed: avatar.imageUrl != avatar.defaultImageUrl
id: avatar id: avatar
userId: editAccount.userId userId: editAccount.userId
imageUrl: fileDialog.selectedFile || defaultImageUrl
toolTipImageUrl: null toolTipImageUrl: null
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
@ -33,6 +51,13 @@ HGridLayout {
Layout.preferredWidth: thinMaxWidth Layout.preferredWidth: thinMaxWidth
Layout.preferredHeight: Layout.preferredWidth Layout.preferredHeight: Layout.preferredWidth
HFileDialogOpener {
id: fileDialog
fileType: HFileDialogOpener.FileType.Images
dialog.title: qsTr("Select profile picture for %1")
.arg(userInfo.displayName)
}
} }
HColumnLayout { HColumnLayout {
@ -53,6 +78,8 @@ HGridLayout {
} }
HLabeledTextField { HLabeledTextField {
property bool changed: field.text != userInfo.displayName
id: nameField id: nameField
label.text: qsTr("Display name:") label.text: qsTr("Display name:")
field.text: userInfo.displayName field.text: userInfo.displayName
@ -69,11 +96,15 @@ HGridLayout {
Layout.alignment: Qt.AlignBottom Layout.alignment: Qt.AlignBottom
HUIButton { HUIButton {
property bool nameChangeRunning: false
property bool avatarChangeRunning: false
id: saveButton id: saveButton
iconName: "save" iconName: "save"
text: qsTr("Save") text: qsTr("Save")
centerText: false centerText: false
enabled: nameField.field.text != userInfo.displayName loading: nameChangeRunning || avatarChangeRunning
enabled: nameField.changed || avatar.changed
Layout.fillWidth: true Layout.fillWidth: true
Layout.alignment: Qt.AlignBottom Layout.alignment: Qt.AlignBottom

View File

@ -18,10 +18,11 @@ ApplicationWindow {
property bool ready: false property bool ready: false
Component.onCompleted: { Component.onCompleted: {
Qt.application.name = "harmonyqml" Qt.application.organization = "harmonyqml"
Qt.application.displayName = "Harmony QML" Qt.application.name = "harmonyqml"
Qt.application.version = "0.1.0" Qt.application.displayName = "Harmony QML"
window.ready = true Qt.application.version = "0.1.0"
window.ready = true
} }
Theme { id: theme } Theme { id: theme }