From 751a27157c75b4ec891ca490d4406c0835787db9 Mon Sep 17 00:00:00 2001 From: miruka Date: Sat, 13 Jul 2019 20:15:20 -0400 Subject: [PATCH] Add account settings page Display name change working --- TODO.md | 9 +- src/icons/cancel.svg | 51 ++++++++++ src/icons/save.svg | 51 ++++++++++ src/python/image_provider.py | 76 +++++++++------ src/qml/Base/HAvatar.qml | 42 ++++----- src/qml/Base/HBaseButton.qml | 5 +- src/qml/Base/HColumnLayout.qml | 1 - src/qml/Base/HGridLayout.qml | 14 +++ src/qml/Base/HLabeledTextField.qml | 23 +++++ src/qml/Base/HRichLabel.qml | 19 +--- src/qml/Base/HRoomAvatar.qml | 6 +- src/qml/Base/HTextField.qml | 4 + src/qml/Base/HUIButton.qml | 18 +++- src/qml/Base/HUserAvatar.qml | 7 +- src/qml/Chat/Banners/Banner.qml | 1 - src/qml/Chat/RoomHeader.qml | 1 - src/qml/Chat/SendBox.qml | 1 - src/qml/Chat/Timeline/EventContent.qml | 7 +- src/qml/Chat/Timeline/EventDelegate.qml | 6 +- src/qml/Chat/Timeline/EventList.qml | 12 +-- src/qml/Pages/EditAccount/ClientSettings.qml | 7 +- src/qml/Pages/EditAccount/Devices.qml | 7 +- src/qml/Pages/EditAccount/EditAccount.qml | 97 +++++++++++--------- src/qml/Pages/EditAccount/Profile.qml | 92 ++++++++++++++++++- src/qml/SidePane/AccountDelegate.qml | 12 +-- src/qml/Theme.qml | 7 +- src/qml/utils.js | 21 ++++- 27 files changed, 435 insertions(+), 162 deletions(-) create mode 100644 src/icons/cancel.svg create mode 100644 src/icons/save.svg create mode 100644 src/qml/Base/HGridLayout.qml create mode 100644 src/qml/Base/HLabeledTextField.qml diff --git a/TODO.md b/TODO.md index 5b7e510a..88e7aee9 100644 --- a/TODO.md +++ b/TODO.md @@ -1,4 +1,12 @@ +- ElidedLabel component +- Can set `Layout.fillWidth: true` to elide/wrap +- Use childrenRect stuff - Rename theme.bottomElementsHeight +- Account delegate name color +- If avatar is set, name color from average color? +- normalSpacing in Theme +- Qt.AlignCenter instead of V | H +- banner button repair - Qt 5.12 - New input handlers @@ -57,7 +65,6 @@ - Client improvements - [debug mode](https://docs.python.org/3/library/asyncio-dev.html) - - More intelligent thumbnails downloading for different sizes - Filtering rooms: search more than display names? - Initial sync filter and lazy load, see weechat-matrix `_handle_login()` - See also `handle_response()`'s `keys_query` request diff --git a/src/icons/cancel.svg b/src/icons/cancel.svg new file mode 100644 index 00000000..584a4121 --- /dev/null +++ b/src/icons/cancel.svg @@ -0,0 +1,51 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/src/icons/save.svg b/src/icons/save.svg new file mode 100644 index 00000000..c2bef64e --- /dev/null +++ b/src/icons/save.svg @@ -0,0 +1,51 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/src/python/image_provider.py b/src/python/image_provider.py index ebc54935..5e1afb51 100644 --- a/src/python/image_provider.py +++ b/src/python/image_provider.py @@ -17,35 +17,48 @@ import nio import pyotherside from nio.api import ResizingMethod -from .app import App - Size = Tuple[int, int] ImageData = Tuple[bytearray, Size, int] # last int: pyotherside format enum @dataclass class Thumbnail: - provider: "ImageProvider" = field() - id: str = field() - width: int = field() - height: int = field() + # pylint: disable=no-member + provider: "ImageProvider" = field() + mxc: str = field() + width: int = field() + height: int = field() def __post_init__(self) -> None: - self.id = re.sub(r"#auto$", "", self.id) + self.mxc = re.sub(r"#auto$", "", self.mxc) - if not re.match(r"^(crop|scale)/mxc://.+/.+", self.id): - raise ValueError(f"Invalid image ID: {self.id}") + if not re.match(r"^mxc://.+/.+", self.mxc): + raise ValueError(f"Invalid mxc URI: {self.mxc}") + + + @property + def server_size(self) -> Tuple[int, int]: + # https://matrix.org/docs/spec/client_server/latest#thumbnails + + if self.width > 640 or self.height > 480: + return (800, 600) + + if self.width > 320 or self.height > 240: + return (640, 480) + + if self.width > 96 or self.height > 96: + return (320, 240) + + if self.width > 32 or self.height > 32: + return (96, 96) + + return (32, 32) @property def resize_method(self) -> ResizingMethod: - return ResizingMethod.crop \ - if self.id.startswith("crop/") else ResizingMethod.scale - - - @property - def mxc(self) -> str: - return re.sub(r"^(crop|scale)/", "", self.id) + return ResizingMethod.scale \ + if self.width > 96 or self.height > 96 else ResizingMethod.crop @property @@ -55,11 +68,12 @@ class Thumbnail: @property def local_path(self) -> Path: + # pylint: disable=bad-string-format-type parsed = urlparse(self.mxc) - name = "%s.%d.%d.%s" % ( + name = "%s.%03d.%03d.%s" % ( parsed.path.lstrip("/"), - self.width, - self.height, + self.server_size[0], + self.server_size[1], self.resize_method.value, ) return self.provider.cache / parsed.netloc / name @@ -74,16 +88,16 @@ class Thumbnail: response = await client.thumbnail( server_name = parsed.netloc, media_id = parsed.path.lstrip("/"), - width = self.width, - height = self.height, + width = self.server_size[0], + height = self.server_size[1], method = self.resize_method, ) body = response.body if response.content_type not in ("image/jpeg", "image/png"): - with BytesIO(body) as in_, BytesIO() as out: - PILImage.open(in_).save(out, "PNG") - body = out.getvalue() + with BytesIO(body) as img_in, BytesIO() as img_out: + PILImage.open(img_in).save(img_out, "PNG") + body = img_out.getvalue() self.local_path.parent.mkdir(parents=True, exist_ok=True) @@ -99,8 +113,10 @@ class Thumbnail: except FileNotFoundError: body = await self.download() - size = (self.width, self.height) - return (bytearray(body), size , pyotherside.format_data) + with BytesIO(body) as img_in: + real_size = PILImage.open(img_in).size + + return (bytearray(body), real_size, pyotherside.format_data) class ImageProvider: @@ -112,10 +128,10 @@ class ImageProvider: def get(self, image_id: str, requested_size: Size) -> ImageData: - width = 128 if requested_size[0] < 1 else requested_size[0] - height = width if requested_size[1] < 1 else requested_size[1] - thumb = Thumbnail(self, image_id, width, height) + if requested_size[0] < 1 or requested_size[1] < 1: + raise ValueError(f"width or height < 1: {requested_size!r}") return asyncio.run_coroutine_threadsafe( - thumb.get_data(), self.app.loop + Thumbnail(self, image_id, *requested_size).get_data(), + self.app.loop ).result() diff --git a/src/qml/Base/HAvatar.qml b/src/qml/Base/HAvatar.qml index 27555e94..1a59bc83 100644 --- a/src/qml/Base/HAvatar.qml +++ b/src/qml/Base/HAvatar.qml @@ -7,11 +7,14 @@ import "../Base" import "../utils.js" as Utils HRectangle { + id: avatar + implicitWidth: theme.avatar.size + implicitHeight: theme.avatar.size + property string name: "" property var imageUrl: null property var toolTipImageUrl: imageUrl - property int dimension: theme.avatar.size - property bool hidden: false + property alias fillMode: avatarImage.fillMode onImageUrlChanged: if (imageUrl) { avatarImage.source = imageUrl } @@ -19,19 +22,16 @@ HRectangle { avatarToolTipImage.source = toolTipImageUrl } - width: dimension - height: hidden ? 1 : dimension - implicitWidth: dimension - implicitHeight: hidden ? 1 : dimension + readonly property var params: Utils.thumbnailParametersFor(width, height) - opacity: hidden ? 0 : 1 - - color: name ? Utils.avatarColor(name) : theme.avatar.background.unknown + color: imageUrl ? "transparent" : + name ? Utils.avatarColor(name) : + theme.avatar.background.unknown HLabel { z: 1 anchors.centerIn: parent - visible: ! hidden && ! imageUrl + visible: ! imageUrl text: name ? name.charAt(0) : "?" color: theme.avatar.letter @@ -39,14 +39,13 @@ HRectangle { } HImage { - z: 2 id: avatarImage anchors.fill: parent - visible: ! hidden && imageUrl - fillMode: Image.PreserveAspectCrop - - sourceSize.width: dimension - sourceSize.height: dimension + visible: imageUrl + z: 2 + sourceSize.width: params.width + sourceSize.height: params.height + fillMode: params.fillMode HoverHandler { id: hoverHandler @@ -54,16 +53,17 @@ HRectangle { HToolTip { id: avatarToolTip - visible: hoverHandler.hovered + visible: toolTipImageUrl && hoverHandler.hovered width: 128 height: 128 HImage { id: avatarToolTipImage - sourceSize.width: avatarToolTip.width - sourceSize.height: avatarToolTip.height - width: sourceSize.width - height: sourceSize.height + width: parent.width + height: parent.height + sourceSize.width: parent.width + sourceSize.height: parent.height + fillMode: Image.PreserveAspectCrop } } } diff --git a/src/qml/Base/HBaseButton.qml b/src/qml/Base/HBaseButton.qml index 202927e7..ee98adb8 100644 --- a/src/qml/Base/HBaseButton.qml +++ b/src/qml/Base/HBaseButton.qml @@ -26,7 +26,10 @@ Button { background: Rectangle { id: buttonBackground color: Qt.lighter( - backgroundColor, checked ? (checkedLightens ? 1.3 : 0.7) : 1.0 + backgroundColor, + ! enabled ? 0.7 : + checked ? (checkedLightens ? 1.3 : 0.7) : + 1.0 ) radius: circle ? height : 0 diff --git a/src/qml/Base/HColumnLayout.qml b/src/qml/Base/HColumnLayout.qml index 66ca242e..4c50edc4 100644 --- a/src/qml/Base/HColumnLayout.qml +++ b/src/qml/Base/HColumnLayout.qml @@ -2,7 +2,6 @@ // This file is part of harmonyqml, licensed under LGPLv3. import QtQuick 2.12 -import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 ColumnLayout { diff --git a/src/qml/Base/HGridLayout.qml b/src/qml/Base/HGridLayout.qml new file mode 100644 index 00000000..126c767f --- /dev/null +++ b/src/qml/Base/HGridLayout.qml @@ -0,0 +1,14 @@ +// Copyright 2019 miruka +// This file is part of harmonyqml, licensed under LGPLv3. + +import QtQuick 2.12 +import QtQuick.Layouts 1.12 + +GridLayout { + id: gridLayout + rowSpacing: 0 + columnSpacing: 0 + + property int totalSpacing: + spacing * Math.max(0, (gridLayout.visibleChildren.length - 1)) +} diff --git a/src/qml/Base/HLabeledTextField.qml b/src/qml/Base/HLabeledTextField.qml new file mode 100644 index 00000000..b11e0027 --- /dev/null +++ b/src/qml/Base/HLabeledTextField.qml @@ -0,0 +1,23 @@ +// Copyright 2019 miruka +// This file is part of harmonyqml, licensed under LGPLv3. + +import QtQuick 2.12 +import QtQuick.Controls 2.12 + +Column { + spacing: 4 + + property alias label: fieldLabel + property alias field: textField + + HLabel { + id: fieldLabel + } + + HTextField { + id: textField + bordered: true + radius: 2 + width: parent.width + } +} diff --git a/src/qml/Base/HRichLabel.qml b/src/qml/Base/HRichLabel.qml index b89b8757..c8923f82 100644 --- a/src/qml/Base/HRichLabel.qml +++ b/src/qml/Base/HRichLabel.qml @@ -4,25 +4,14 @@ import QtQuick 2.12 HLabel { + // https://blog.shantanu.io/2015/02/15/creating-working-hyperlinks-in-qtquick-text/ id: label textFormat: Text.RichText + onLinkActivated: Qt.openUrlExternally(link) MouseArea { - id: mouseArea anchors.fill: parent - hoverEnabled: true - propagateComposedEvents: true - - onPositionChanged: function (mouse) { - mouse.accepted = false - cursorShape = label.linkAt(mouse.x, mouse.y) ? - Qt.PointingHandCursor : Qt.ArrowCursor - } - - onClicked: function(mouse) { - var link = label.linkAt(mouse.x, mouse.y) - mouse.accepted = Boolean(link) - if (link) { Qt.openUrlExternally(link) } - } + acceptedButtons: Qt.NoButton + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor } } diff --git a/src/qml/Base/HRoomAvatar.qml b/src/qml/Base/HRoomAvatar.qml index cab58529..6968c84c 100644 --- a/src/qml/Base/HRoomAvatar.qml +++ b/src/qml/Base/HRoomAvatar.qml @@ -14,10 +14,8 @@ HAvatar { dname[0] == "#" && dname.length > 1 ? dname.substring(1) : dname imageUrl: - roomInfo.avatarUrl ? - ("image://python/crop/" + roomInfo.avatarUrl) : null + roomInfo.avatarUrl ? ("image://python/" + roomInfo.avatarUrl) : null toolTipImageUrl: - roomInfo.avatarUrl ? - ("image://python/scale/" + roomInfo.avatarUrl) : null + roomInfo.avatarUrl ? ("image://python/" + roomInfo.avatarUrl) : null } diff --git a/src/qml/Base/HTextField.qml b/src/qml/Base/HTextField.qml index 160e822d..b42db833 100644 --- a/src/qml/Base/HTextField.qml +++ b/src/qml/Base/HTextField.qml @@ -5,7 +5,9 @@ import QtQuick 2.12 import QtQuick.Controls 2.12 TextField { + property bool bordered: false property alias backgroundColor: textFieldBackground.color + property alias radius: textFieldBackground.radius font.family: theme.fontFamily.sans font.pixelSize: theme.fontSize.normal @@ -14,6 +16,8 @@ TextField { background: Rectangle { id: textFieldBackground color: theme.controls.textField.background + border.color: theme.controls.textField.borderColor + border.width: bordered ? theme.controls.textField.borderWidth : 0 } selectByMouse: true diff --git a/src/qml/Base/HUIButton.qml b/src/qml/Base/HUIButton.qml index e6c9cbe6..387eee4a 100644 --- a/src/qml/Base/HUIButton.qml +++ b/src/qml/Base/HUIButton.qml @@ -4,6 +4,7 @@ import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 +import QtGraphicalEffects 1.12 HBaseButton { property int horizontalMargin: 0 @@ -14,6 +15,7 @@ HBaseButton { property var iconTransform: null property int fontSize: theme.fontSize.normal + property bool centerText: Boolean(iconName) property bool loading: false @@ -29,7 +31,7 @@ HBaseButton { HRowLayout { id: contentLayout - spacing: button.text && iconName ? 5 : 0 + spacing: button.text && iconName ? 8 : 0 Component.onCompleted: contentWidth = implicitWidth HIcon { @@ -41,15 +43,25 @@ HBaseButton { Layout.bottomMargin: verticalMargin Layout.leftMargin: horizontalMargin Layout.rightMargin: horizontalMargin + + // Colorize { + // anchors.fill: parent + // source: parent + // visible: ! button.enabled + // saturation: 0 + // } } HLabel { text: button.text font.pixelSize: fontSize - horizontalAlignment: Text.AlignHCenter + horizontalAlignment: button.centerText ? + Text.AlignHCenter : Text.AlignLeft verticalAlignment: Text.AlignVCenter + color: enabled ? + theme.colors.foreground : theme.colors.foregroundDim2 - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + Layout.fillWidth: true } } } diff --git a/src/qml/Base/HUserAvatar.qml b/src/qml/Base/HUserAvatar.qml index 768c4a47..716d10b0 100644 --- a/src/qml/Base/HUserAvatar.qml +++ b/src/qml/Base/HUserAvatar.qml @@ -5,19 +5,16 @@ import QtQuick 2.12 HAvatar { property string userId: "" - readonly property var userInfo: userId ? users.find(userId) : ({}) name: userInfo.displayName || userId.substring(1) // no leading @ imageUrl: - userInfo.avatarUrl ? - ("image://python/crop/" + userInfo.avatarUrl) : null + userInfo.avatarUrl ? ("image://python/" + userInfo.avatarUrl) : null toolTipImageUrl: - userInfo.avatarUrl ? - ("image://python/scale/" + userInfo.avatarUrl) : null + userInfo.avatarUrl ? ("image://python/" + userInfo.avatarUrl) : null //HImage { //id: status diff --git a/src/qml/Chat/Banners/Banner.qml b/src/qml/Chat/Banners/Banner.qml index 0e14e4f2..5d134f38 100644 --- a/src/qml/Chat/Banners/Banner.qml +++ b/src/qml/Chat/Banners/Banner.qml @@ -22,7 +22,6 @@ HRectangle { HUserAvatar { id: bannerAvatar - dimension: banner.Layout.preferredHeight } HIcon { diff --git a/src/qml/Chat/RoomHeader.qml b/src/qml/Chat/RoomHeader.qml index 14efa1d3..a3ed2b0a 100644 --- a/src/qml/Chat/RoomHeader.qml +++ b/src/qml/Chat/RoomHeader.qml @@ -26,7 +26,6 @@ HRectangle { HRoomAvatar { id: avatar roomId: chatPage.roomId - dimension: roomHeader.height Layout.alignment: Qt.AlignTop } diff --git a/src/qml/Chat/SendBox.qml b/src/qml/Chat/SendBox.qml index 38be711c..fbaead21 100644 --- a/src/qml/Chat/SendBox.qml +++ b/src/qml/Chat/SendBox.qml @@ -22,7 +22,6 @@ HRectangle { HUserAvatar { id: avatar userId: chatPage.userId - dimension: sendBox.Layout.minimumHeight } HScrollableTextArea { diff --git a/src/qml/Chat/Timeline/EventContent.qml b/src/qml/Chat/Timeline/EventContent.qml index 5af4cbe5..87143e45 100644 --- a/src/qml/Chat/Timeline/EventContent.qml +++ b/src/qml/Chat/Timeline/EventContent.qml @@ -13,9 +13,10 @@ Row { HUserAvatar { id: avatar - hidden: combine userId: model.senderId - dimension: model.showNameLine ? 48 : 28 + width: model.showNameLine ? 48 : 28 + height: combine ? 1 : model.showNameLine ? 48 : 28 + opacity: combine ? 0 : 1 visible: ! isOwn } @@ -26,7 +27,7 @@ Row { //width: nameLabel.implicitWidth width: Math.min( - roomEventListView.width - avatar.width - messageContent.spacing, + eventList.width - avatar.width - messageContent.spacing, theme.fontSize.normal * 0.5 * 75, // 600 with 16px font Math.max( nameLabel.visible ? nameLabel.implicitWidth : 0, diff --git a/src/qml/Chat/Timeline/EventDelegate.qml b/src/qml/Chat/Timeline/EventDelegate.qml index 26be9440..0e61bd64 100644 --- a/src/qml/Chat/Timeline/EventDelegate.qml +++ b/src/qml/Chat/Timeline/EventDelegate.qml @@ -16,8 +16,8 @@ Column { function getPreviousItem(nth) { // Remember, index 0 = newest bottomest message nth = nth || 1 - return roomEventListView.model.count - 1 > model.index + nth ? - roomEventListView.model.get(model.index + nth) : null + return eventList.model.count - 1 > model.index + nth ? + eventList.model.get(model.index + nth) : null } property var previousItem: getPreviousItem() @@ -60,7 +60,7 @@ Column { property int verticalPadding: 4 ListView.onAdd: { - var nextDelegate = roomEventListView.contentItem.children[index] + var nextDelegate = eventList.contentItem.children[index] if (nextDelegate) { nextDelegate.reloadPreviousItem() } } diff --git a/src/qml/Chat/Timeline/EventList.qml b/src/qml/Chat/Timeline/EventList.qml index 74bb3973..67268059 100644 --- a/src/qml/Chat/Timeline/EventList.qml +++ b/src/qml/Chat/Timeline/EventList.qml @@ -6,14 +6,14 @@ import SortFilterProxyModel 0.2 import "../../Base" HRectangle { - property alias listView: roomEventListView + property alias listView: eventList property int space: 8 - color: theme.chat.roomEventList.background + color: theme.chat.eventList.background HListView { - id: roomEventListView + id: eventList clip: true model: HListModel { @@ -48,12 +48,12 @@ HRectangle { if (chatPage.category != "Invites" && canLoad && yPos <= 0.1) { zz += 1 print(canLoad, zz) - canLoad = false + eventList.canLoad = false py.callClientCoro( chatPage.userId, "load_past_events", [chatPage.roomId], - function(more_to_load) { canLoad = more_to_load } + function(more_to_load) { eventList.canLoad = more_to_load } ) } } @@ -62,7 +62,7 @@ HRectangle { HNoticePage { text: qsTr("Nothing to show here yet...") - visible: roomEventListView.model.count < 1 + visible: eventList.model.count < 1 anchors.fill: parent } } diff --git a/src/qml/Pages/EditAccount/ClientSettings.qml b/src/qml/Pages/EditAccount/ClientSettings.qml index 656d82bb..742ae0ab 100644 --- a/src/qml/Pages/EditAccount/ClientSettings.qml +++ b/src/qml/Pages/EditAccount/ClientSettings.qml @@ -7,9 +7,6 @@ import QtQuick.Layouts 1.12 import "../../Base" import "../../utils.js" as Utils -HRectangle { - HLabel { - anchors.centerIn: parent - text: "Client - TODO" - } +HLabel { + text: "Client - TODO" } diff --git a/src/qml/Pages/EditAccount/Devices.qml b/src/qml/Pages/EditAccount/Devices.qml index 4ac80c26..1e8cb0a6 100644 --- a/src/qml/Pages/EditAccount/Devices.qml +++ b/src/qml/Pages/EditAccount/Devices.qml @@ -7,9 +7,6 @@ import QtQuick.Layouts 1.12 import "../../Base" import "../../utils.js" as Utils -HRectangle { - HLabel { - anchors.centerIn: parent - text: "Devices - TODO" - } +HLabel { + text: "Devices - TODO" } diff --git a/src/qml/Pages/EditAccount/EditAccount.qml b/src/qml/Pages/EditAccount/EditAccount.qml index 39059258..2c81e45b 100644 --- a/src/qml/Pages/EditAccount/EditAccount.qml +++ b/src/qml/Pages/EditAccount/EditAccount.qml @@ -7,66 +7,75 @@ import QtQuick.Layouts 1.12 import "../../Base" import "../../utils.js" as Utils -HRectangle { +Page { + id: editAccount + padding: currentSpacing < 8 ? 0 : currentSpacing + Behavior on padding { HNumberAnimation {} } + + property bool wide: width > 414 + padding * 2 + property int thinMaxWidth: 240 + property int normalSpacing: 8 + property int currentSpacing: + Math.min(normalSpacing * width / 400, normalSpacing * 2) + property string userId: "" readonly property var userInfo: users.find(userId) - HColumnLayout { - anchors.fill: parent + header: HRectangle { + width: parent.width + height: theme.bottomElementsHeight + color: theme.pageHeadersBackground HRowLayout { - Layout.preferredHeight: theme.bottomElementsHeight + width: parent.width HLabel { - text: qsTr("Edit %1").arg( + text: qsTr("Account settings for %1").arg( Utils.coloredNameHtml(userInfo.displayName, userId) ) textFormat: Text.StyledText font.pixelSize: theme.fontSize.big elide: Text.ElideRight maximumLineCount: 1 - // visible: width > 50 - Layout.fillWidth: true - Layout.maximumWidth: parent.width - tabBar.width - Layout.leftMargin: 8 + Layout.leftMargin: currentSpacing Layout.rightMargin: Layout.leftMargin + Layout.fillWidth: true } - - TabBar { - id: tabBar - currentIndex: swipeView.currentIndex - spacing: 0 - contentHeight: parent.height - - TabButton { - text: qsTr("Profile") - width: implicitWidth * 1.25 - } - - TabButton { - text: qsTr("Devices") - width: implicitWidth * 1.25 - } - - TabButton { - text: qsTr("Harmony") - width: implicitWidth * 1.25 - } - } - } - - SwipeView { - id: swipeView - clip: true - currentIndex: tabBar.currentIndex - - Layout.fillHeight: true - Layout.fillWidth: true - - Profile {} - Devices {} - ClientSettings {} } } + + background: null + + HColumnLayout { + anchors.fill: parent + spacing: 16 + + HRectangle { + color: theme.box.background + // radius: theme.box.radius + + Layout.alignment: Qt.AlignCenter + + Layout.preferredWidth: wide ? parent.width : thinMaxWidth + Layout.maximumWidth: Math.min(parent.width, 640) + + Layout.preferredHeight: childrenRect.height + Layout.maximumHeight: parent.height + + Profile { width: parent.width } + } + + // HRectangle { + // color: theme.box.background + // radius: theme.box.radius + // ClientSettings { width: parent.width } + // } + + // HRectangle { + // color: theme.box.background + // radius: theme.box.radius + // Devices { width: parent.width } + // } + } } diff --git a/src/qml/Pages/EditAccount/Profile.qml b/src/qml/Pages/EditAccount/Profile.qml index 45369bc8..1eaf9748 100644 --- a/src/qml/Pages/EditAccount/Profile.qml +++ b/src/qml/Pages/EditAccount/Profile.qml @@ -7,9 +7,93 @@ import QtQuick.Layouts 1.12 import "../../Base" import "../../utils.js" as Utils -HRectangle { - HLabel { - anchors.centerIn: parent - text: "profile" +HGridLayout { + function applyChanges() { + saveButton.loading = true + + py.callClientCoro( + userId, "set_displayname", [nameField.field.text], + () => { saveButton.loading = false } + ) + } + + columns: 2 + flow: wide ? GridLayout.LeftToRight : GridLayout.TopToBottom + rowSpacing: currentSpacing + + Component.onCompleted: nameField.field.forceActiveFocus() + + HUserAvatar { + id: avatar + userId: editAccount.userId + toolTipImageUrl: null + + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: wide ? 0 : currentSpacing + + Layout.preferredWidth: thinMaxWidth + Layout.preferredHeight: Layout.preferredWidth + } + + HColumnLayout { + id: profileInfo + spacing: normalSpacing + + HColumnLayout { + spacing: normalSpacing + Layout.margins: currentSpacing + + HLabel { + text: qsTr("User ID:
%1") + .arg(Utils.coloredNameHtml(userId, userId)) + textFormat: Text.StyledText + wrapMode: Text.Wrap + + Layout.fillWidth: true + } + + HLabeledTextField { + id: nameField + label.text: qsTr("Display name:") + field.text: userInfo.displayName + field.onAccepted: applyChanges() + + Layout.fillWidth: true + Layout.maximumWidth: 480 + } + } + + HSpacer {} + + HRowLayout { + Layout.alignment: Qt.AlignBottom + + HUIButton { + id: saveButton + iconName: "save" + text: qsTr("Save") + centerText: false + enabled: nameField.field.text != userInfo.displayName + + Layout.fillWidth: true + Layout.alignment: Qt.AlignBottom + + onClicked: applyChanges() + } + + HUIButton { + iconName: "cancel" + text: qsTr("Cancel") + centerText: false + + Layout.fillWidth: true + Layout.alignment: Qt.AlignBottom + enabled: saveButton.enabled && ! saveButton.loading + + onClicked: { + nameField.field.text = userInfo.displayName + } + } + } } } diff --git a/src/qml/SidePane/AccountDelegate.qml b/src/qml/SidePane/AccountDelegate.qml index e830d13e..6691202f 100644 --- a/src/qml/SidePane/AccountDelegate.qml +++ b/src/qml/SidePane/AccountDelegate.qml @@ -12,18 +12,18 @@ Column { property var userInfo: users.find(model.userId) property bool expanded: true - TapHandler { - onTapped: pageStack.showPage( - "EditAccount/EditAccount", { "userId": model.userId } - ) - } - HHighlightRectangle { width: parent.width height: childrenRect.height normalColor: theme.sidePane.account.background + TapHandler { + onTapped: pageStack.showPage( + "EditAccount/EditAccount", { "userId": model.userId } + ) + } + HRowLayout { id: row width: parent.width diff --git a/src/qml/Theme.qml b/src/qml/Theme.qml index 579af502..d88b5414 100644 --- a/src/qml/Theme.qml +++ b/src/qml/Theme.qml @@ -32,6 +32,7 @@ QtObject { property color background2: Qt.hsla(0, 0, 0.9, 0.7) property color foreground: "black" property color foregroundDim: Qt.hsla(0, 0, 0.2, 1) + property color foregroundDim2: Qt.hsla(0, 0, 0.3, 1) property color foregroundError: Qt.hsla(0.95, 0.64, 0.32, 1) property color textBorder: Qt.hsla(0, 0, 0, 0.07) } @@ -50,6 +51,8 @@ QtObject { property QtObject textField: QtObject { property color background: colors.background2 + property color borderColor: "black" + property int borderWidth: 1 } property QtObject textArea: QtObject { @@ -82,7 +85,7 @@ QtObject { property color background: colors.background2 } - property QtObject roomEventList: QtObject { + property QtObject eventList: QtObject { property color background: "transparent" } @@ -120,6 +123,8 @@ QtObject { } } + property color pageHeadersBackground: colors.background2 + property QtObject box: QtObject { property color background: colors.background0 property int radius: theme.radius diff --git a/src/qml/utils.js b/src/qml/utils.js index df4c6a4d..5e3de386 100644 --- a/src/qml/utils.js +++ b/src/qml/utils.js @@ -47,7 +47,7 @@ function nameColor(name) { function coloredNameHtml(name, alt_id) { // substring: remove leading @ - return "" + + return "" + escapeHtml(name || alt_id) + "" } @@ -106,3 +106,22 @@ function filterMatches(filter, text) { } return true } + + +function thumbnailParametersFor(width, height) { + // https://matrix.org/docs/spec/client_server/latest#thumbnails + + if (width > 640 || height > 480) + return {width: 800, height: 600, fillMode: Image.PreserveAspectFit} + + if (width > 320 || height > 240) + return {width: 640, height: 480, fillMode: Image.PreserveAspectFit} + + if (width > 96 || height > 96) + return {width: 320, height: 240, fillMode: Image.PreserveAspectFit} + + if (width > 32 || height > 32) + return {width: 96, height: 96, fillMode: Image.PreserveAspectCrop} + + return {width: 32, height: 32, fillMode: Image.PreserveAspectCrop} +}