Add account settings page

Display name change working
This commit is contained in:
miruka 2019-07-13 20:15:20 -04:00
parent eeea0af4cd
commit 751a27157c
27 changed files with 435 additions and 162 deletions

View File

@ -1,4 +1,12 @@
- ElidedLabel component
- Can set `Layout.fillWidth: true` to elide/wrap
- Use childrenRect stuff
- Rename theme.bottomElementsHeight - 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 - Qt 5.12
- New input handlers - New input handlers
@ -57,7 +65,6 @@
- Client improvements - Client improvements
- [debug mode](https://docs.python.org/3/library/asyncio-dev.html) - [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? - Filtering rooms: search more than display names?
- Initial sync filter and lazy load, see weechat-matrix `_handle_login()` - Initial sync filter and lazy load, see weechat-matrix `_handle_login()`
- See also `handle_response()`'s `keys_query` request - See also `handle_response()`'s `keys_query` request

51
src/icons/cancel.svg Normal file
View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="24"
height="24"
viewBox="0 0 24 24"
version="1.1"
id="svg14"
sodipodi:docname="invite_decline.svg"
inkscape:version="">
<metadata
id="metadata20">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs18" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="640"
inkscape:window-height="480"
id="namedview16"
showgrid="false"
inkscape:zoom="9.8333333"
inkscape:cx="6.9152542"
inkscape:cy="17.084746"
inkscape:current-layer="svg14" />
<path
d="M23 20.168l-8.185-8.187 8.185-8.174-2.832-2.807-8.182 8.179-8.176-8.179-2.81 2.81 8.186 8.196-8.186 8.184 2.81 2.81 8.203-8.192 8.18 8.192z"
id="path12"
style="fill:#ab0938;fill-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

51
src/icons/save.svg Normal file
View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="24"
height="24"
viewBox="0 0 24 24"
version="1.1"
id="svg4"
sodipodi:docname="invite_accept.svg"
inkscape:version="">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="640"
inkscape:window-height="480"
id="namedview6"
showgrid="false"
inkscape:zoom="9.8333333"
inkscape:cx="-28.271186"
inkscape:cy="12"
inkscape:current-layer="svg4" />
<path
d="M9 21.035l-9-8.638 2.791-2.87 6.156 5.874 12.21-12.436 2.843 2.817z"
id="path2"
style="fill:#0d8967;fill-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -17,35 +17,48 @@ import nio
import pyotherside import pyotherside
from nio.api import ResizingMethod from nio.api import ResizingMethod
from .app import App
Size = Tuple[int, int] Size = Tuple[int, int]
ImageData = Tuple[bytearray, Size, int] # last int: pyotherside format enum ImageData = Tuple[bytearray, Size, int] # last int: pyotherside format enum
@dataclass @dataclass
class Thumbnail: class Thumbnail:
provider: "ImageProvider" = field() # pylint: disable=no-member
id: str = field() provider: "ImageProvider" = field()
width: int = field() mxc: str = field()
height: int = field() width: int = field()
height: int = field()
def __post_init__(self) -> None: 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): if not re.match(r"^mxc://.+/.+", self.mxc):
raise ValueError(f"Invalid image ID: {self.id}") 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 @property
def resize_method(self) -> ResizingMethod: def resize_method(self) -> ResizingMethod:
return ResizingMethod.crop \ return ResizingMethod.scale \
if self.id.startswith("crop/") else ResizingMethod.scale if self.width > 96 or self.height > 96 else ResizingMethod.crop
@property
def mxc(self) -> str:
return re.sub(r"^(crop|scale)/", "", self.id)
@property @property
@ -55,11 +68,12 @@ class Thumbnail:
@property @property
def local_path(self) -> Path: def local_path(self) -> Path:
# pylint: disable=bad-string-format-type
parsed = urlparse(self.mxc) parsed = urlparse(self.mxc)
name = "%s.%d.%d.%s" % ( name = "%s.%03d.%03d.%s" % (
parsed.path.lstrip("/"), parsed.path.lstrip("/"),
self.width, self.server_size[0],
self.height, self.server_size[1],
self.resize_method.value, self.resize_method.value,
) )
return self.provider.cache / parsed.netloc / name return self.provider.cache / parsed.netloc / name
@ -74,16 +88,16 @@ class Thumbnail:
response = await client.thumbnail( response = await client.thumbnail(
server_name = parsed.netloc, server_name = parsed.netloc,
media_id = parsed.path.lstrip("/"), media_id = parsed.path.lstrip("/"),
width = self.width, width = self.server_size[0],
height = self.height, height = self.server_size[1],
method = self.resize_method, method = self.resize_method,
) )
body = response.body body = response.body
if response.content_type not in ("image/jpeg", "image/png"): if response.content_type not in ("image/jpeg", "image/png"):
with BytesIO(body) as in_, BytesIO() as out: with BytesIO(body) as img_in, BytesIO() as img_out:
PILImage.open(in_).save(out, "PNG") PILImage.open(img_in).save(img_out, "PNG")
body = out.getvalue() body = img_out.getvalue()
self.local_path.parent.mkdir(parents=True, exist_ok=True) self.local_path.parent.mkdir(parents=True, exist_ok=True)
@ -99,8 +113,10 @@ class Thumbnail:
except FileNotFoundError: except FileNotFoundError:
body = await self.download() body = await self.download()
size = (self.width, self.height) with BytesIO(body) as img_in:
return (bytearray(body), size , pyotherside.format_data) real_size = PILImage.open(img_in).size
return (bytearray(body), real_size, pyotherside.format_data)
class ImageProvider: class ImageProvider:
@ -112,10 +128,10 @@ class ImageProvider:
def get(self, image_id: str, requested_size: Size) -> ImageData: def get(self, image_id: str, requested_size: Size) -> ImageData:
width = 128 if requested_size[0] < 1 else requested_size[0] if requested_size[0] < 1 or requested_size[1] < 1:
height = width if requested_size[1] < 1 else requested_size[1] raise ValueError(f"width or height < 1: {requested_size!r}")
thumb = Thumbnail(self, image_id, width, height)
return asyncio.run_coroutine_threadsafe( return asyncio.run_coroutine_threadsafe(
thumb.get_data(), self.app.loop Thumbnail(self, image_id, *requested_size).get_data(),
self.app.loop
).result() ).result()

View File

@ -7,11 +7,14 @@ import "../Base"
import "../utils.js" as Utils import "../utils.js" as Utils
HRectangle { HRectangle {
id: avatar
implicitWidth: theme.avatar.size
implicitHeight: theme.avatar.size
property string name: "" property string name: ""
property var imageUrl: null property var imageUrl: null
property var toolTipImageUrl: imageUrl property var toolTipImageUrl: imageUrl
property int dimension: theme.avatar.size property alias fillMode: avatarImage.fillMode
property bool hidden: false
onImageUrlChanged: if (imageUrl) { avatarImage.source = imageUrl } onImageUrlChanged: if (imageUrl) { avatarImage.source = imageUrl }
@ -19,19 +22,16 @@ HRectangle {
avatarToolTipImage.source = toolTipImageUrl avatarToolTipImage.source = toolTipImageUrl
} }
width: dimension readonly property var params: Utils.thumbnailParametersFor(width, height)
height: hidden ? 1 : dimension
implicitWidth: dimension
implicitHeight: hidden ? 1 : dimension
opacity: hidden ? 0 : 1 color: imageUrl ? "transparent" :
name ? Utils.avatarColor(name) :
color: name ? Utils.avatarColor(name) : theme.avatar.background.unknown theme.avatar.background.unknown
HLabel { HLabel {
z: 1 z: 1
anchors.centerIn: parent anchors.centerIn: parent
visible: ! hidden && ! imageUrl visible: ! imageUrl
text: name ? name.charAt(0) : "?" text: name ? name.charAt(0) : "?"
color: theme.avatar.letter color: theme.avatar.letter
@ -39,14 +39,13 @@ HRectangle {
} }
HImage { HImage {
z: 2
id: avatarImage id: avatarImage
anchors.fill: parent anchors.fill: parent
visible: ! hidden && imageUrl visible: imageUrl
fillMode: Image.PreserveAspectCrop z: 2
sourceSize.width: params.width
sourceSize.width: dimension sourceSize.height: params.height
sourceSize.height: dimension fillMode: params.fillMode
HoverHandler { HoverHandler {
id: hoverHandler id: hoverHandler
@ -54,16 +53,17 @@ HRectangle {
HToolTip { HToolTip {
id: avatarToolTip id: avatarToolTip
visible: hoverHandler.hovered visible: toolTipImageUrl && hoverHandler.hovered
width: 128 width: 128
height: 128 height: 128
HImage { HImage {
id: avatarToolTipImage id: avatarToolTipImage
sourceSize.width: avatarToolTip.width width: parent.width
sourceSize.height: avatarToolTip.height height: parent.height
width: sourceSize.width sourceSize.width: parent.width
height: sourceSize.height sourceSize.height: parent.height
fillMode: Image.PreserveAspectCrop
} }
} }
} }

View File

@ -26,7 +26,10 @@ Button {
background: Rectangle { background: Rectangle {
id: buttonBackground id: buttonBackground
color: Qt.lighter( 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 radius: circle ? height : 0

View File

@ -2,7 +2,6 @@
// This file is part of harmonyqml, licensed under LGPLv3. // This file is part of harmonyqml, licensed under LGPLv3.
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
ColumnLayout { ColumnLayout {

View File

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

View File

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

View File

@ -4,25 +4,14 @@
import QtQuick 2.12 import QtQuick 2.12
HLabel { HLabel {
// https://blog.shantanu.io/2015/02/15/creating-working-hyperlinks-in-qtquick-text/
id: label id: label
textFormat: Text.RichText textFormat: Text.RichText
onLinkActivated: Qt.openUrlExternally(link)
MouseArea { MouseArea {
id: mouseArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true acceptedButtons: Qt.NoButton
propagateComposedEvents: true cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
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) }
}
} }
} }

View File

@ -14,10 +14,8 @@ HAvatar {
dname[0] == "#" && dname.length > 1 ? dname.substring(1) : dname dname[0] == "#" && dname.length > 1 ? dname.substring(1) : dname
imageUrl: imageUrl:
roomInfo.avatarUrl ? roomInfo.avatarUrl ? ("image://python/" + roomInfo.avatarUrl) : null
("image://python/crop/" + roomInfo.avatarUrl) : null
toolTipImageUrl: toolTipImageUrl:
roomInfo.avatarUrl ? roomInfo.avatarUrl ? ("image://python/" + roomInfo.avatarUrl) : null
("image://python/scale/" + roomInfo.avatarUrl) : null
} }

View File

@ -5,7 +5,9 @@ import QtQuick 2.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12
TextField { TextField {
property bool bordered: false
property alias backgroundColor: textFieldBackground.color property alias backgroundColor: textFieldBackground.color
property alias radius: textFieldBackground.radius
font.family: theme.fontFamily.sans font.family: theme.fontFamily.sans
font.pixelSize: theme.fontSize.normal font.pixelSize: theme.fontSize.normal
@ -14,6 +16,8 @@ TextField {
background: Rectangle { background: Rectangle {
id: textFieldBackground id: textFieldBackground
color: theme.controls.textField.background color: theme.controls.textField.background
border.color: theme.controls.textField.borderColor
border.width: bordered ? theme.controls.textField.borderWidth : 0
} }
selectByMouse: true selectByMouse: true

View File

@ -4,6 +4,7 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import QtGraphicalEffects 1.12
HBaseButton { HBaseButton {
property int horizontalMargin: 0 property int horizontalMargin: 0
@ -14,6 +15,7 @@ HBaseButton {
property var iconTransform: null property var iconTransform: null
property int fontSize: theme.fontSize.normal property int fontSize: theme.fontSize.normal
property bool centerText: Boolean(iconName)
property bool loading: false property bool loading: false
@ -29,7 +31,7 @@ HBaseButton {
HRowLayout { HRowLayout {
id: contentLayout id: contentLayout
spacing: button.text && iconName ? 5 : 0 spacing: button.text && iconName ? 8 : 0
Component.onCompleted: contentWidth = implicitWidth Component.onCompleted: contentWidth = implicitWidth
HIcon { HIcon {
@ -41,15 +43,25 @@ HBaseButton {
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 {
text: button.text text: button.text
font.pixelSize: fontSize font.pixelSize: fontSize
horizontalAlignment: Text.AlignHCenter horizontalAlignment: button.centerText ?
Text.AlignHCenter : Text.AlignLeft
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
color: enabled ?
theme.colors.foreground : theme.colors.foregroundDim2
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter Layout.fillWidth: true
} }
} }
} }

View File

@ -5,19 +5,16 @@ import QtQuick 2.12
HAvatar { HAvatar {
property string userId: "" property string userId: ""
readonly property var userInfo: userId ? users.find(userId) : ({}) readonly property var userInfo: userId ? users.find(userId) : ({})
name: name:
userInfo.displayName || userId.substring(1) // no leading @ userInfo.displayName || userId.substring(1) // no leading @
imageUrl: imageUrl:
userInfo.avatarUrl ? userInfo.avatarUrl ? ("image://python/" + userInfo.avatarUrl) : null
("image://python/crop/" + userInfo.avatarUrl) : null
toolTipImageUrl: toolTipImageUrl:
userInfo.avatarUrl ? userInfo.avatarUrl ? ("image://python/" + userInfo.avatarUrl) : null
("image://python/scale/" + userInfo.avatarUrl) : null
//HImage { //HImage {
//id: status //id: status

View File

@ -22,7 +22,6 @@ HRectangle {
HUserAvatar { HUserAvatar {
id: bannerAvatar id: bannerAvatar
dimension: banner.Layout.preferredHeight
} }
HIcon { HIcon {

View File

@ -26,7 +26,6 @@ HRectangle {
HRoomAvatar { HRoomAvatar {
id: avatar id: avatar
roomId: chatPage.roomId roomId: chatPage.roomId
dimension: roomHeader.height
Layout.alignment: Qt.AlignTop Layout.alignment: Qt.AlignTop
} }

View File

@ -22,7 +22,6 @@ HRectangle {
HUserAvatar { HUserAvatar {
id: avatar id: avatar
userId: chatPage.userId userId: chatPage.userId
dimension: sendBox.Layout.minimumHeight
} }
HScrollableTextArea { HScrollableTextArea {

View File

@ -13,9 +13,10 @@ Row {
HUserAvatar { HUserAvatar {
id: avatar id: avatar
hidden: combine
userId: model.senderId 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 visible: ! isOwn
} }
@ -26,7 +27,7 @@ Row {
//width: nameLabel.implicitWidth //width: nameLabel.implicitWidth
width: Math.min( 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 theme.fontSize.normal * 0.5 * 75, // 600 with 16px font
Math.max( Math.max(
nameLabel.visible ? nameLabel.implicitWidth : 0, nameLabel.visible ? nameLabel.implicitWidth : 0,

View File

@ -16,8 +16,8 @@ Column {
function getPreviousItem(nth) { function getPreviousItem(nth) {
// Remember, index 0 = newest bottomest message // Remember, index 0 = newest bottomest message
nth = nth || 1 nth = nth || 1
return roomEventListView.model.count - 1 > model.index + nth ? return eventList.model.count - 1 > model.index + nth ?
roomEventListView.model.get(model.index + nth) : null eventList.model.get(model.index + nth) : null
} }
property var previousItem: getPreviousItem() property var previousItem: getPreviousItem()
@ -60,7 +60,7 @@ Column {
property int verticalPadding: 4 property int verticalPadding: 4
ListView.onAdd: { ListView.onAdd: {
var nextDelegate = roomEventListView.contentItem.children[index] var nextDelegate = eventList.contentItem.children[index]
if (nextDelegate) { nextDelegate.reloadPreviousItem() } if (nextDelegate) { nextDelegate.reloadPreviousItem() }
} }

View File

@ -6,14 +6,14 @@ import SortFilterProxyModel 0.2
import "../../Base" import "../../Base"
HRectangle { HRectangle {
property alias listView: roomEventListView property alias listView: eventList
property int space: 8 property int space: 8
color: theme.chat.roomEventList.background color: theme.chat.eventList.background
HListView { HListView {
id: roomEventListView id: eventList
clip: true clip: true
model: HListModel { model: HListModel {
@ -48,12 +48,12 @@ HRectangle {
if (chatPage.category != "Invites" && canLoad && yPos <= 0.1) { if (chatPage.category != "Invites" && canLoad && yPos <= 0.1) {
zz += 1 zz += 1
print(canLoad, zz) print(canLoad, zz)
canLoad = false eventList.canLoad = false
py.callClientCoro( py.callClientCoro(
chatPage.userId, chatPage.userId,
"load_past_events", "load_past_events",
[chatPage.roomId], [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 { HNoticePage {
text: qsTr("Nothing to show here yet...") text: qsTr("Nothing to show here yet...")
visible: roomEventListView.model.count < 1 visible: eventList.model.count < 1
anchors.fill: parent anchors.fill: parent
} }
} }

View File

@ -7,9 +7,6 @@ import QtQuick.Layouts 1.12
import "../../Base" import "../../Base"
import "../../utils.js" as Utils import "../../utils.js" as Utils
HRectangle { HLabel {
HLabel { text: "Client - TODO"
anchors.centerIn: parent
text: "Client - TODO"
}
} }

View File

@ -7,9 +7,6 @@ import QtQuick.Layouts 1.12
import "../../Base" import "../../Base"
import "../../utils.js" as Utils import "../../utils.js" as Utils
HRectangle { HLabel {
HLabel { text: "Devices - TODO"
anchors.centerIn: parent
text: "Devices - TODO"
}
} }

View File

@ -7,66 +7,75 @@ import QtQuick.Layouts 1.12
import "../../Base" import "../../Base"
import "../../utils.js" as Utils 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: "" property string userId: ""
readonly property var userInfo: users.find(userId) readonly property var userInfo: users.find(userId)
HColumnLayout { header: HRectangle {
anchors.fill: parent width: parent.width
height: theme.bottomElementsHeight
color: theme.pageHeadersBackground
HRowLayout { HRowLayout {
Layout.preferredHeight: theme.bottomElementsHeight width: parent.width
HLabel { HLabel {
text: qsTr("Edit %1").arg( text: qsTr("Account settings for %1").arg(
Utils.coloredNameHtml(userInfo.displayName, userId) Utils.coloredNameHtml(userInfo.displayName, userId)
) )
textFormat: Text.StyledText textFormat: Text.StyledText
font.pixelSize: theme.fontSize.big font.pixelSize: theme.fontSize.big
elide: Text.ElideRight elide: Text.ElideRight
maximumLineCount: 1 maximumLineCount: 1
// visible: width > 50
Layout.fillWidth: true Layout.leftMargin: currentSpacing
Layout.maximumWidth: parent.width - tabBar.width
Layout.leftMargin: 8
Layout.rightMargin: Layout.leftMargin 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 }
// }
}
} }

View File

@ -7,9 +7,93 @@ import QtQuick.Layouts 1.12
import "../../Base" import "../../Base"
import "../../utils.js" as Utils import "../../utils.js" as Utils
HRectangle { HGridLayout {
HLabel { function applyChanges() {
anchors.centerIn: parent saveButton.loading = true
text: "profile"
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:<br>%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
}
}
}
} }
} }

View File

@ -12,18 +12,18 @@ Column {
property var userInfo: users.find(model.userId) property var userInfo: users.find(model.userId)
property bool expanded: true property bool expanded: true
TapHandler {
onTapped: pageStack.showPage(
"EditAccount/EditAccount", { "userId": model.userId }
)
}
HHighlightRectangle { HHighlightRectangle {
width: parent.width width: parent.width
height: childrenRect.height height: childrenRect.height
normalColor: theme.sidePane.account.background normalColor: theme.sidePane.account.background
TapHandler {
onTapped: pageStack.showPage(
"EditAccount/EditAccount", { "userId": model.userId }
)
}
HRowLayout { HRowLayout {
id: row id: row
width: parent.width width: parent.width

View File

@ -32,6 +32,7 @@ QtObject {
property color background2: Qt.hsla(0, 0, 0.9, 0.7) property color background2: Qt.hsla(0, 0, 0.9, 0.7)
property color foreground: "black" property color foreground: "black"
property color foregroundDim: Qt.hsla(0, 0, 0.2, 1) 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 foregroundError: Qt.hsla(0.95, 0.64, 0.32, 1)
property color textBorder: Qt.hsla(0, 0, 0, 0.07) property color textBorder: Qt.hsla(0, 0, 0, 0.07)
} }
@ -50,6 +51,8 @@ QtObject {
property QtObject textField: QtObject { property QtObject textField: QtObject {
property color background: colors.background2 property color background: colors.background2
property color borderColor: "black"
property int borderWidth: 1
} }
property QtObject textArea: QtObject { property QtObject textArea: QtObject {
@ -82,7 +85,7 @@ QtObject {
property color background: colors.background2 property color background: colors.background2
} }
property QtObject roomEventList: QtObject { property QtObject eventList: QtObject {
property color background: "transparent" property color background: "transparent"
} }
@ -120,6 +123,8 @@ QtObject {
} }
} }
property color pageHeadersBackground: colors.background2
property QtObject box: QtObject { property QtObject box: QtObject {
property color background: colors.background0 property color background: colors.background0
property int radius: theme.radius property int radius: theme.radius

View File

@ -47,7 +47,7 @@ function nameColor(name) {
function coloredNameHtml(name, alt_id) { function coloredNameHtml(name, alt_id) {
// substring: remove leading @ // substring: remove leading @
return "<font color='" + nameColor(name || alt_id.substring(1)) + "'>" + return "<font color='" + nameColor(alt_id.substring(1)) + "'>" +
escapeHtml(name || alt_id) + escapeHtml(name || alt_id) +
"</font>" "</font>"
} }
@ -106,3 +106,22 @@ function filterMatches(filter, text) {
} }
return true 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}
}