Rename some filers and folder for clarity
This commit is contained in:
86
src/gui/Base/HAvatar.qml
Normal file
86
src/gui/Base/HAvatar.qml
Normal file
@@ -0,0 +1,86 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import "../Base"
|
||||
|
||||
Rectangle {
|
||||
id: avatar
|
||||
implicitWidth: theme.controls.avatar.size
|
||||
implicitHeight: theme.controls.avatar.size
|
||||
|
||||
color: avatarImage.visible ? "transparent" : utils.hsluv(
|
||||
name ? utils.hueFrom(name) : 0,
|
||||
name ? theme.controls.avatar.background.saturation : 0,
|
||||
theme.controls.avatar.background.lightness,
|
||||
theme.controls.avatar.background.opacity
|
||||
)
|
||||
|
||||
property string name
|
||||
property alias mxc: avatarImage.mxc
|
||||
|
||||
property alias toolTipMxc: avatarToolTipImage.mxc
|
||||
property alias sourceOverride: avatarImage.sourceOverride
|
||||
property alias toolTipSourceOverride: avatarToolTipImage.sourceOverride
|
||||
property alias fillMode: avatarImage.fillMode
|
||||
property alias animate: avatarImage.animate
|
||||
|
||||
readonly property alias hovered: hoverHandler.hovered
|
||||
|
||||
HLabel {
|
||||
z: 1
|
||||
anchors.centerIn: parent
|
||||
visible: ! avatarImage.visible
|
||||
|
||||
text: name ? name.charAt(0) : "?"
|
||||
font.pixelSize: parent.height / 1.4
|
||||
|
||||
color: utils.hsluv(
|
||||
name ? utils.hueFrom(name) : 0,
|
||||
name ? theme.controls.avatar.letter.saturation : 0,
|
||||
theme.controls.avatar.letter.lightness,
|
||||
theme.controls.avatar.letter.opacity
|
||||
)
|
||||
}
|
||||
|
||||
HMxcImage {
|
||||
id: avatarImage
|
||||
anchors.fill: parent
|
||||
showProgressBar: false
|
||||
visible: Boolean(sourceOverride || mxc)
|
||||
z: 2
|
||||
sourceSize.width: parent.width
|
||||
sourceSize.height: parent.height
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
animate: false
|
||||
|
||||
HoverHandler { id: hoverHandler }
|
||||
|
||||
HToolTip {
|
||||
id: avatarToolTip
|
||||
visible: ! avatarImage.broken &&
|
||||
avatarImage.status !== Image.Error &&
|
||||
avatarImage.width < dimension * 0.75 &&
|
||||
(toolTipSourceOverride || toolTipMxc) &&
|
||||
hoverHandler.hovered
|
||||
delay: 1000
|
||||
backgroundColor: theme.controls.avatar.hoveredImage.background
|
||||
|
||||
readonly property int dimension: Math.min(
|
||||
mainUI.width / 1.25,
|
||||
mainUI.height / 1.25,
|
||||
theme.controls.avatar.hoveredImage.size +
|
||||
background.border.width * 2,
|
||||
)
|
||||
|
||||
contentItem: HMxcImage {
|
||||
id: avatarToolTipImage
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
mxc: avatarImage.mxc
|
||||
|
||||
sourceSize.width: avatarToolTip.dimension
|
||||
sourceSize.height: avatarToolTip.dimension
|
||||
width: avatarToolTip.dimension
|
||||
height: avatarToolTip.dimension
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
125
src/gui/Base/HBox.qml
Normal file
125
src/gui/Base/HBox.qml
Normal file
@@ -0,0 +1,125 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
|
||||
Rectangle {
|
||||
id: box
|
||||
color: theme.controls.box.background
|
||||
implicitWidth: theme.controls.box.defaultWidth
|
||||
implicitHeight: childrenRect.height
|
||||
|
||||
Keys.onReturnPressed: if (clickButtonOnEnter) enterClickButton()
|
||||
Keys.onEnterPressed: Keys.onReturnPressed(event)
|
||||
|
||||
|
||||
property alias buttonModel: buttonRepeater.model
|
||||
property var buttonCallbacks: []
|
||||
property string focusButton: ""
|
||||
property string clickButtonOnEnter: ""
|
||||
|
||||
property bool fillAvailableHeight: false
|
||||
|
||||
property HButton firstButton: null
|
||||
|
||||
default property alias body: interfaceBody.data
|
||||
|
||||
|
||||
function enterClickButton() {
|
||||
for (let i = 0; i < buttonModel.length; i++) {
|
||||
let btn = buttonRepeater.itemAt(i)
|
||||
if (btn.enabled && btn.name === clickButtonOnEnter) btn.clicked()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
HNumberAnimation on scale {
|
||||
running: true
|
||||
from: 0
|
||||
to: 1
|
||||
overshoot: 3
|
||||
}
|
||||
|
||||
HColumnLayout {
|
||||
id: mainColumn
|
||||
width: parent.width
|
||||
|
||||
Binding on height {
|
||||
value: box.height
|
||||
when: box.fillAvailableHeight
|
||||
}
|
||||
|
||||
HColumnLayout {
|
||||
id: interfaceBody
|
||||
spacing: theme.spacing * 1.5
|
||||
|
||||
Layout.margins: spacing
|
||||
}
|
||||
|
||||
HGridLayout {
|
||||
id: buttonGrid
|
||||
visible: buttonModel.length > 0
|
||||
flow: width >= buttonRepeater.childrenImplicitWidth ?
|
||||
GridLayout.LeftToRight : GridLayout.TopToBottom
|
||||
|
||||
HRepeater {
|
||||
id: buttonRepeater
|
||||
model: []
|
||||
|
||||
onItemAdded:
|
||||
if (index === 0) firstButton = buttonRepeater.itemAt(0)
|
||||
|
||||
onItemRemoved:
|
||||
if (index === 0) firstButton = null
|
||||
|
||||
HButton {
|
||||
id: button
|
||||
text: modelData.text
|
||||
icon.name: modelData.iconName || ""
|
||||
icon.color: modelData.iconColor || (
|
||||
name === "ok" || name === "apply" || name === "retry" ?
|
||||
theme.colors.positiveBackground :
|
||||
|
||||
name === "cancel" ?
|
||||
theme.colors.negativeBackground :
|
||||
|
||||
theme.icons.colorize
|
||||
)
|
||||
|
||||
enabled:
|
||||
modelData.enabled === undefined ?
|
||||
true : modelData.enabled
|
||||
|
||||
loading: modelData.loading || false
|
||||
|
||||
disableWhileLoading:
|
||||
modelData.disableWhileLoading === undefined ?
|
||||
true : modelData.disableWhileLoading
|
||||
|
||||
onClicked: buttonCallbacks[name](button)
|
||||
|
||||
Keys.onLeftPressed: previous.forceActiveFocus()
|
||||
Keys.onUpPressed: previous.forceActiveFocus()
|
||||
Keys.onRightPressed: next.forceActiveFocus()
|
||||
Keys.onDownPressed: next.forceActiveFocus()
|
||||
Keys.onReturnPressed: if (button.enabled) button.clicked()
|
||||
Keys.onEnterPressed: Keys.onReturnPressed(event)
|
||||
|
||||
Component.onCompleted:
|
||||
if (name === focusButton) forceActiveFocus()
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: theme.baseElementsHeight
|
||||
|
||||
|
||||
property string name: modelData.name
|
||||
|
||||
property Item next: buttonRepeater.itemAt(
|
||||
utils.numberWrapAt(index + 1, buttonRepeater.count),
|
||||
)
|
||||
property Item previous: buttonRepeater.itemAt(
|
||||
utils.numberWrapAt(index - 1, buttonRepeater.count),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
8
src/gui/Base/HBusyIndicator.qml
Normal file
8
src/gui/Base/HBusyIndicator.qml
Normal file
@@ -0,0 +1,8 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Shapes 1.12
|
||||
|
||||
HCircleProgressBar {
|
||||
indeterminate: true
|
||||
dialWidth: 2
|
||||
}
|
63
src/gui/Base/HButton.qml
Normal file
63
src/gui/Base/HButton.qml
Normal file
@@ -0,0 +1,63 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
|
||||
Button {
|
||||
id: button
|
||||
enabled: ! button.loading
|
||||
spacing: theme.spacing
|
||||
topPadding: padded ? spacing / (circle ? 1.75 : 1.5) : 0
|
||||
bottomPadding: topPadding
|
||||
leftPadding: padded ? spacing / (circle ? 1.5 : 1) : 0
|
||||
rightPadding: leftPadding
|
||||
|
||||
iconItem.svgName: loading ? "hourglass" : icon.name
|
||||
icon.color: theme.icons.colorize
|
||||
|
||||
// Must be explicitely set to display correctly on KDE
|
||||
implicitWidth: Math.max(
|
||||
implicitBackgroundWidth + leftInset + rightInset,
|
||||
implicitContentWidth + leftPadding + rightPadding
|
||||
)
|
||||
implicitHeight: Math.max(
|
||||
implicitBackgroundHeight + topInset + bottomInset,
|
||||
implicitContentHeight + topPadding + bottomPadding
|
||||
)
|
||||
|
||||
// Prevent button from gaining focus and being highlighted on click
|
||||
focusPolicy: Qt.TabFocus
|
||||
|
||||
background: HButtonBackground {
|
||||
button: button
|
||||
buttonTheme: theme.controls.button
|
||||
radius: circle ? height : 0
|
||||
color: backgroundColor
|
||||
}
|
||||
|
||||
contentItem: HButtonContent {
|
||||
id: contentItem
|
||||
button: button
|
||||
buttonTheme: theme.controls.button
|
||||
}
|
||||
|
||||
|
||||
readonly property alias iconItem: contentItem.icon
|
||||
readonly property alias label: contentItem.label
|
||||
|
||||
property color backgroundColor: theme.controls.button.background
|
||||
property bool disableWhileLoading: true
|
||||
property bool loading: false
|
||||
property bool circle: false
|
||||
property bool padded: true
|
||||
|
||||
property HToolTip toolTip: HToolTip {
|
||||
id: toolTip
|
||||
visible: text && hovered
|
||||
}
|
||||
|
||||
|
||||
Binding on enabled {
|
||||
when: disableWhileLoading && button.loading
|
||||
value: false
|
||||
}
|
||||
}
|
31
src/gui/Base/HButtonBackground.qml
Normal file
31
src/gui/Base/HButtonBackground.qml
Normal file
@@ -0,0 +1,31 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
|
||||
Rectangle {
|
||||
color: buttonTheme.background
|
||||
opacity: loading ? theme.loadingElementsOpacity :
|
||||
enabled ? 1 : theme.disabledElementsOpacity
|
||||
|
||||
|
||||
property AbstractButton button
|
||||
property QtObject buttonTheme
|
||||
|
||||
|
||||
Behavior on opacity { HNumberAnimation {} }
|
||||
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: button.checked ? buttonTheme.checkedOverlay :
|
||||
|
||||
button.enabled && button.pressed ? buttonTheme.pressedOverlay :
|
||||
|
||||
(button.enabled && button.hovered) || button.activeFocus ?
|
||||
buttonTheme.hoveredOverlay :
|
||||
|
||||
"transparent"
|
||||
|
||||
Behavior on color { HColorAnimation { factor: 0.5 } }
|
||||
}
|
||||
}
|
91
src/gui/Base/HButtonContent.qml
Normal file
91
src/gui/Base/HButtonContent.qml
Normal file
@@ -0,0 +1,91 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
|
||||
HRowLayout {
|
||||
id: buttonContent
|
||||
spacing: button.spacing
|
||||
opacity: loading ? theme.loadingElementsOpacity :
|
||||
enabled ? 1 : theme.disabledElementsOpacity
|
||||
|
||||
|
||||
property AbstractButton button
|
||||
property QtObject buttonTheme
|
||||
|
||||
readonly property alias icon: icon
|
||||
readonly property alias label: label
|
||||
|
||||
|
||||
Behavior on opacity { HNumberAnimation {} }
|
||||
|
||||
|
||||
HIcon {
|
||||
property bool loading: button.loading || false
|
||||
|
||||
id: icon
|
||||
svgName: button.icon.name
|
||||
colorize: button.icon.color
|
||||
cache: button.icon.cache
|
||||
|
||||
onLoadingChanged: if (! loading) resetAnimations.start()
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
|
||||
|
||||
ParallelAnimation {
|
||||
id: resetAnimations
|
||||
HNumberAnimation { target: icon; property: "opacity"; to: 1 }
|
||||
HNumberAnimation { target: icon; property: "rotation"; to: 0 }
|
||||
}
|
||||
|
||||
|
||||
HNumberAnimation on opacity {
|
||||
id: blink
|
||||
from: 1
|
||||
to: 0.5
|
||||
factor: 2
|
||||
running: button.loading || false
|
||||
onFinished: { [from, to] = [to, from]; start() }
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
running: button.loading || false
|
||||
loops: Animation.Infinite
|
||||
|
||||
HPauseAnimation { factor: blink.factor * 8 }
|
||||
|
||||
HNumberAnimation {
|
||||
id: rotation1
|
||||
target: icon
|
||||
property: "rotation"
|
||||
from: 0
|
||||
to: 180
|
||||
factor: blink.factor
|
||||
}
|
||||
|
||||
HPauseAnimation { factor: blink.factor * 8 }
|
||||
|
||||
HNumberAnimation {
|
||||
target: rotation1.target
|
||||
property: rotation1.property
|
||||
from: rotation1.to
|
||||
to: 360
|
||||
factor: rotation1.factor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HLabel {
|
||||
id: label
|
||||
text: button.text
|
||||
visible: Boolean(text)
|
||||
color: buttonTheme.text
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
90
src/gui/Base/HCheckBox.qml
Normal file
90
src/gui/Base/HCheckBox.qml
Normal file
@@ -0,0 +1,90 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
|
||||
CheckBox {
|
||||
id: box
|
||||
checked: defaultChecked
|
||||
spacing: theme.spacing
|
||||
padding: 0
|
||||
|
||||
indicator: Rectangle {
|
||||
opacity: box.enabled ? 1 : theme.disabledElementsOpacity + 0.2
|
||||
implicitWidth: theme.controls.checkBox.boxSize
|
||||
implicitHeight: implicitWidth
|
||||
x: box.leftPadding
|
||||
y: box.topPadding + box.availableHeight / 2 - height / 2
|
||||
radius: theme.radius / 1.5
|
||||
|
||||
color: theme.controls.checkBox.boxBackground
|
||||
border.color:
|
||||
box.enabled && box.pressed ?
|
||||
theme.controls.checkBox.boxPressedBorder :
|
||||
|
||||
(box.enabled && box.hovered) || box.activeFocus ?
|
||||
theme.controls.checkBox.boxHoveredBorder :
|
||||
|
||||
theme.controls.checkBox.boxBorder
|
||||
|
||||
Behavior on border.color { HColorAnimation { factor: 0.5 } }
|
||||
|
||||
HIcon {
|
||||
anchors.centerIn: parent
|
||||
dimension: parent.width - 2
|
||||
svgName: "check-mark"
|
||||
colorize: theme.controls.checkBox.checkIconColorize
|
||||
|
||||
scale: box.checked ? 1 : 0
|
||||
|
||||
Behavior on scale {
|
||||
HNumberAnimation {
|
||||
overshoot: 4
|
||||
easing.type: Easing.InOutBack
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: HColumnLayout {
|
||||
opacity: box.enabled ? 1 : theme.disabledElementsOpacity
|
||||
|
||||
HLabel {
|
||||
id: mainText
|
||||
text: box.text
|
||||
color: theme.controls.checkBox.text
|
||||
|
||||
// Set a width on CheckBox for wrapping to work,
|
||||
// e.g. by using Layout.fillWidth
|
||||
wrapMode: Text.Wrap
|
||||
leftPadding: box.indicator.width + box.spacing
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
HLabel {
|
||||
id: subtitleText
|
||||
visible: Boolean(text)
|
||||
color: theme.controls.checkBox.subtitle
|
||||
font.pixelSize: theme.fontSize.small
|
||||
|
||||
wrapMode: mainText.wrapMode
|
||||
leftPadding: mainText.leftPadding
|
||||
verticalAlignment: mainText.verticalAlignment
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
property alias mainText: mainText
|
||||
property alias subtitle: subtitleText
|
||||
property bool defaultChecked: false
|
||||
readonly property bool changed: checked !== defaultChecked
|
||||
|
||||
|
||||
function reset() { checked = defaultChecked }
|
||||
|
||||
|
||||
Behavior on opacity { HNumberAnimation { factor: 2 } }
|
||||
}
|
48
src/gui/Base/HCircleProgressBar.qml
Normal file
48
src/gui/Base/HCircleProgressBar.qml
Normal file
@@ -0,0 +1,48 @@
|
||||
import QtQuick 2.12
|
||||
import RadialBar 1.0
|
||||
|
||||
RadialBar {
|
||||
id: bar
|
||||
implicitWidth: 96 * (theme ? theme.uiScale : 1)
|
||||
implicitHeight: implicitWidth
|
||||
foregroundColor: theme.controls.circleProgressBar.background
|
||||
progressColor: theme.controls.circleProgressBar.foreground
|
||||
dialWidth: theme.controls.circleProgressBar.thickness
|
||||
startAngle: 0
|
||||
spanAngle: 360
|
||||
|
||||
from: 0
|
||||
to: 1
|
||||
value: 0
|
||||
|
||||
showText: true
|
||||
textFont.pixelSize: theme ? theme.fontSize.big : 22
|
||||
textColor: theme ? theme.controls.circleProgressBar.text : "white"
|
||||
|
||||
|
||||
property alias from: bar.minValue
|
||||
property alias to: bar.maxValue
|
||||
property bool indeterminate: false
|
||||
|
||||
property real indeterminateSpan:
|
||||
theme.controls.circleProgressBar.indeterminateSpan
|
||||
|
||||
|
||||
Binding on value {
|
||||
value: bar.to * bar.indeterminateSpan
|
||||
when: bar.indeterminate
|
||||
}
|
||||
|
||||
Binding on showText {
|
||||
value: false
|
||||
when: bar.indeterminate
|
||||
}
|
||||
|
||||
HNumberAnimation on rotation {
|
||||
running: bar.indeterminate
|
||||
from: 0
|
||||
to: 360
|
||||
loops: Animation.Infinite
|
||||
duration: theme ? (theme.animationDuration * 6) : 600
|
||||
}
|
||||
}
|
6
src/gui/Base/HColorAnimation.qml
Normal file
6
src/gui/Base/HColorAnimation.qml
Normal file
@@ -0,0 +1,6 @@
|
||||
import QtQuick 2.12
|
||||
|
||||
ColorAnimation {
|
||||
property real factor: 1.0
|
||||
duration: theme.animationDuration * factor
|
||||
}
|
6
src/gui/Base/HColumnLayout.qml
Normal file
6
src/gui/Base/HColumnLayout.qml
Normal file
@@ -0,0 +1,6 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
}
|
123
src/gui/Base/HDrawer.qml
Normal file
123
src/gui/Base/HDrawer.qml
Normal file
@@ -0,0 +1,123 @@
|
||||
import QtQuick 2.13
|
||||
import QtQuick.Controls 2.13
|
||||
|
||||
Drawer {
|
||||
id: drawer
|
||||
implicitWidth: horizontal ? calculatedSize * theme.uiScale : parent.width
|
||||
implicitHeight: vertical ? calculatedSize * theme.uiScale : parent.height
|
||||
|
||||
// Prevents this: open a popup, make the window small enough for the
|
||||
// drawer to collapse, then make it big again → popup is now behind drawer
|
||||
z: -1
|
||||
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
|
||||
// FIXME: https://bugreports.qt.io/browse/QTBUG-59141
|
||||
// dragMargin: parent.width / 2
|
||||
|
||||
interactive: collapse
|
||||
position: 1
|
||||
visible: ! collapse
|
||||
modal: false
|
||||
closePolicy: Popup.CloseOnEscape
|
||||
|
||||
background: Rectangle { id: bg; color: theme.colors.strongBackground }
|
||||
|
||||
|
||||
property string saveName: ""
|
||||
property var saveId: "ALL"
|
||||
property var saveProperties: ["preferredSize"]
|
||||
|
||||
//
|
||||
|
||||
property alias color: bg.color
|
||||
|
||||
property int defaultSize: 300
|
||||
|
||||
property int preferredSize:
|
||||
window.getState(this, "preferredSize", defaultSize)
|
||||
|
||||
property int minimumSize: resizeAreaSize
|
||||
property int maximumSize:
|
||||
horizontal ?
|
||||
referenceSizeParent.width - theme.minimumSupportedWidth :
|
||||
referenceSizeParent.height - theme.minimumSupportedHeight
|
||||
|
||||
//
|
||||
|
||||
property Item referenceSizeParent: parent
|
||||
|
||||
property bool collapse:
|
||||
(horizontal ? window.width : window.height) < 400 * theme.uiScale
|
||||
|
||||
property int peekSizeWhileCollapsed:
|
||||
horizontal ? referenceSizeParent.width : referenceSizeParent.height
|
||||
|
||||
property int resizeAreaSize: theme.spacing / 2
|
||||
|
||||
readonly property int calculatedSize:
|
||||
collapse ?
|
||||
peekSizeWhileCollapsed :
|
||||
Math.max(minimumSize, Math.min(preferredSize, maximumSize))
|
||||
|
||||
//
|
||||
|
||||
readonly property int visibleSize: visible ? width * position : 0
|
||||
|
||||
readonly property bool horizontal:
|
||||
edge === Qt.LeftEdge || edge === Qt.RightEdge
|
||||
|
||||
readonly property bool vertical: ! horizontal
|
||||
|
||||
|
||||
Behavior on width {
|
||||
enabled: horizontal && ! resizeMouseHandler.drag.active
|
||||
NumberAnimation { duration: 100 }
|
||||
}
|
||||
|
||||
Behavior on height {
|
||||
enabled: vertical && ! resizeMouseHandler.drag.active
|
||||
NumberAnimation { duration: 100 }
|
||||
}
|
||||
|
||||
Item {
|
||||
id: resizeArea
|
||||
x: vertical || drawer.edge === Qt.RightEdge ? 0 : drawer.width-width
|
||||
y: horizontal || drawer.edge !== Qt.TopEdge ? 0 : drawer.height-height
|
||||
width: horizontal ? resizeAreaSize * theme.uiScale : parent.width
|
||||
height: vertical ? resizeAreaSize * theme.uiScale : parent.height
|
||||
z: 999
|
||||
|
||||
MouseArea {
|
||||
id: resizeMouseHandler
|
||||
anchors.fill: parent
|
||||
enabled: ! drawer.collapse
|
||||
acceptedButtons: Qt.LeftButton
|
||||
preventStealing: true
|
||||
hoverEnabled: true
|
||||
cursorShape:
|
||||
containsMouse || drag.active ?
|
||||
(horizontal ? Qt.SizeHorCursor : Qt.SizeVerCursor) :
|
||||
Qt.ArrowCursor
|
||||
|
||||
onMouseXChanged:
|
||||
if (horizontal && pressed) {
|
||||
drawer.preferredSize =
|
||||
drawer.calculatedSize +
|
||||
(drawer.edge === Qt.RightEdge ? -mouseX : mouseX)
|
||||
}
|
||||
|
||||
onMouseYChanged:
|
||||
if (vertical && pressed) {
|
||||
drawer.preferredSize =
|
||||
drawer.calculatedSize +
|
||||
(drawer.edge === Qt.BottomEdge ? -mouseY : mouseY)
|
||||
}
|
||||
|
||||
onReleased: window.saveState(drawer)
|
||||
}
|
||||
}
|
||||
}
|
7
src/gui/Base/HFlickable.qml
Normal file
7
src/gui/Base/HFlickable.qml
Normal file
@@ -0,0 +1,7 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
|
||||
Flickable {
|
||||
interactive: contentWidth > width || contentHeight > height
|
||||
ScrollBar.vertical: ScrollBar {}
|
||||
}
|
36
src/gui/Base/HFlow.qml
Normal file
36
src/gui/Base/HFlow.qml
Normal file
@@ -0,0 +1,36 @@
|
||||
import QtQuick 2.12
|
||||
|
||||
Flow {
|
||||
populate: Transition {
|
||||
id: addTrans
|
||||
|
||||
SequentialAnimation {
|
||||
PropertyAction { property: "opacity"; value: 0 }
|
||||
|
||||
PauseAnimation {
|
||||
duration:
|
||||
addTrans.ViewTransition.index * theme.animationDuration / 2
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
HNumberAnimation { property: "opacity"; to: 1 }
|
||||
HNumberAnimation { properties: "x,y"; from: 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
add: Transition {
|
||||
ParallelAnimation {
|
||||
HNumberAnimation { property: "opacity"; to: 1 }
|
||||
HNumberAnimation { properties: "x,y"; from: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
move: Transition {
|
||||
ParallelAnimation {
|
||||
// Ensure opacity goes to 1 if add transition is interrupted
|
||||
HNumberAnimation { property: "opacity"; to: 1 }
|
||||
HNumberAnimation { properties: "x,y" }
|
||||
}
|
||||
}
|
||||
}
|
7
src/gui/Base/HGridLayout.qml
Normal file
7
src/gui/Base/HGridLayout.qml
Normal file
@@ -0,0 +1,7 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
|
||||
GridLayout {
|
||||
rowSpacing: 0
|
||||
columnSpacing: 0
|
||||
}
|
35
src/gui/Base/HIcon.qml
Normal file
35
src/gui/Base/HIcon.qml
Normal file
@@ -0,0 +1,35 @@
|
||||
import QtQuick 2.12
|
||||
import QtGraphicalEffects 1.12
|
||||
|
||||
Image {
|
||||
id: icon
|
||||
cache: true
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectFit
|
||||
visible: Boolean(svgName)
|
||||
|
||||
source: svgName ? `../../icons/${iconPack}/${svgName}.svg` : ""
|
||||
sourceSize.width: svgName ? dimension : 0
|
||||
sourceSize.height: svgName ? dimension : 0
|
||||
|
||||
|
||||
property string svgName: ""
|
||||
|
||||
property bool small: false
|
||||
property int dimension:
|
||||
theme ?
|
||||
(small ? theme.icons.smallDimension : theme.icons.dimension) :
|
||||
(small ? 16 : 22)
|
||||
|
||||
property color colorize: theme.icons.colorize
|
||||
property string iconPack: theme ? theme.icons.preferredPack : "thin"
|
||||
|
||||
|
||||
layer.enabled: ! Qt.colorEqual(colorize, "transparent")
|
||||
layer.effect: ColorOverlay {
|
||||
color: icon.colorize
|
||||
cached: icon.cache
|
||||
|
||||
Behavior on color { HColorAnimation {} }
|
||||
}
|
||||
}
|
90
src/gui/Base/HImage.qml
Normal file
90
src/gui/Base/HImage.qml
Normal file
@@ -0,0 +1,90 @@
|
||||
import QtQuick 2.12
|
||||
|
||||
Image {
|
||||
id: image
|
||||
autoTransform: true
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectFit
|
||||
|
||||
cache: ! (animate && animated) &&
|
||||
(sourceSize.width + sourceSize.height) <= 512
|
||||
|
||||
|
||||
property bool broken: false
|
||||
property bool animate: true
|
||||
property bool animated: utils.urlExtension(image.source) === "gif"
|
||||
|
||||
property alias showProgressBar: progressBarLoader.active
|
||||
property bool inderterminateProgressBar: false
|
||||
|
||||
|
||||
Component {
|
||||
id: animatedImageComponent
|
||||
|
||||
AnimatedImage {
|
||||
id: animatedImage
|
||||
source: image.source
|
||||
autoTransform: image.autoTransform
|
||||
asynchronous: image.asynchronous
|
||||
fillMode: image.fillMode
|
||||
mirror: image.mirror
|
||||
mipmap: image.mipmap
|
||||
smooth: image.smooth
|
||||
horizontalAlignment: image.horizontalAlignment
|
||||
verticalAlignment: image.verticalAlignment
|
||||
|
||||
// Online GIFs won't be able to loop if cache is set to false,
|
||||
// but caching GIFs is expansive.
|
||||
cache: ! Qt.resolvedUrl(source).startsWith("file://")
|
||||
paused: ! visible || window.hidden || userPaused
|
||||
|
||||
property bool userPaused: ! window.settings.media.autoPlayGIF
|
||||
|
||||
TapHandler {
|
||||
onTapped: parent.userPaused = ! parent.userPaused
|
||||
}
|
||||
|
||||
HIcon {
|
||||
anchors.centerIn: parent
|
||||
svgName: "play-overlay"
|
||||
colorize: "transparent"
|
||||
dimension: Math.min(
|
||||
parent.width - theme.spacing * 2,
|
||||
parent.height - theme.spacing * 2,
|
||||
theme.controls.image.maxPauseIndicatorSize,
|
||||
)
|
||||
scale: parent.status === Image.Ready && parent.paused ? 1 : 0
|
||||
|
||||
Behavior on scale { HNumberAnimation { overshoot: 4 } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HLoader {
|
||||
anchors.fill: parent
|
||||
sourceComponent: animate && animated ? animatedImageComponent : null
|
||||
}
|
||||
|
||||
HLoader {
|
||||
id: progressBarLoader
|
||||
anchors.centerIn: parent
|
||||
width: Math.min(parent.width, parent.height) * 0.5
|
||||
height: width
|
||||
active: image.status === Image.Loading
|
||||
|
||||
sourceComponent: HCircleProgressBar {
|
||||
id: progressBar
|
||||
value: image.progress
|
||||
text: Math.round(value * 100) + "%"
|
||||
|
||||
Behavior on value { HNumberAnimation { factor: 2 } }
|
||||
}
|
||||
}
|
||||
|
||||
HIcon {
|
||||
anchors.centerIn: parent
|
||||
visible: broken || image.status === Image.Error
|
||||
svgName: "broken-image"
|
||||
colorize: theme.colors.negativeBackground
|
||||
}
|
||||
}
|
14
src/gui/Base/HLabel.qml
Normal file
14
src/gui/Base/HLabel.qml
Normal file
@@ -0,0 +1,14 @@
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick 2.12
|
||||
|
||||
Label {
|
||||
font.family: theme.fontFamily.sans
|
||||
font.pixelSize: theme.fontSize.normal
|
||||
font.pointSize: -1
|
||||
textFormat: Label.PlainText
|
||||
|
||||
color: theme.colors.text
|
||||
linkColor: theme.colors.link
|
||||
|
||||
maximumLineCount: elide === Label.ElideNone ? Number.MAX_VALUE : 1
|
||||
}
|
19
src/gui/Base/HLabeledTextField.qml
Normal file
19
src/gui/Base/HLabeledTextField.qml
Normal file
@@ -0,0 +1,19 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
|
||||
Column {
|
||||
spacing: theme.spacing / 2
|
||||
|
||||
property alias label: fieldLabel
|
||||
property alias field: textField
|
||||
|
||||
HLabel {
|
||||
id: fieldLabel
|
||||
}
|
||||
|
||||
HTextField {
|
||||
id: textField
|
||||
radius: 2
|
||||
width: parent.width
|
||||
}
|
||||
}
|
28
src/gui/Base/HListModel.qml
Normal file
28
src/gui/Base/HListModel.qml
Normal file
@@ -0,0 +1,28 @@
|
||||
import QtQuick 2.12
|
||||
import QSyncable 1.0
|
||||
|
||||
JsonListModel {
|
||||
id: model
|
||||
source: []
|
||||
Component.onCompleted: if (! keyField) { throw "keyField not set" }
|
||||
|
||||
function toObject(itemList=listModel) {
|
||||
let objList = []
|
||||
|
||||
for (let item of itemList) {
|
||||
let obj = JSON.parse(JSON.stringify(item))
|
||||
|
||||
for (let role in obj) {
|
||||
if (obj[role]["objectName"] !== undefined) {
|
||||
obj[role] = toObject(item[role])
|
||||
}
|
||||
}
|
||||
objList.push(obj)
|
||||
}
|
||||
return objList
|
||||
}
|
||||
|
||||
function toJson() {
|
||||
return JSON.stringify(toObject(), null, 4)
|
||||
}
|
||||
}
|
90
src/gui/Base/HListView.qml
Normal file
90
src/gui/Base/HListView.qml
Normal file
@@ -0,0 +1,90 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
|
||||
ListView {
|
||||
id: listView
|
||||
interactive: allowDragging
|
||||
currentIndex: -1
|
||||
keyNavigationWraps: true
|
||||
highlightMoveDuration: theme.animationDuration
|
||||
|
||||
// Keep highlighted delegate at the center
|
||||
highlightRangeMode: ListView.ApplyRange
|
||||
preferredHighlightBegin: height / 2 - currentItemHeight
|
||||
preferredHighlightEnd: height / 2 + currentItemHeight
|
||||
|
||||
maximumFlickVelocity: 4000
|
||||
|
||||
|
||||
highlight: Rectangle {
|
||||
color: theme.controls.listView.highlight
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
visible: listView.interactive || ! listView.allowDragging
|
||||
}
|
||||
|
||||
// Make sure to handle when a previous transition gets interrupted
|
||||
add: Transition {
|
||||
ParallelAnimation {
|
||||
// ScriptAction { script: print("add") }
|
||||
HNumberAnimation { property: "opacity"; from: 0; to: 1 }
|
||||
HNumberAnimation { property: "scale"; from: 0; to: 1 }
|
||||
}
|
||||
}
|
||||
|
||||
move: Transition {
|
||||
ParallelAnimation {
|
||||
// ScriptAction { script: print("move") }
|
||||
HNumberAnimation { property: "opacity"; to: 1 }
|
||||
HNumberAnimation { property: "scale"; to: 1 }
|
||||
HNumberAnimation { properties: "x,y" }
|
||||
}
|
||||
}
|
||||
|
||||
remove: Transition {
|
||||
ParallelAnimation {
|
||||
// ScriptAction { script: print("remove") }
|
||||
HNumberAnimation { property: "opacity"; to: 0 }
|
||||
HNumberAnimation { property: "scale"; to: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
// displaced: move
|
||||
displaced: Transition {
|
||||
ParallelAnimation {
|
||||
// ScriptAction { script: print("displaced") }
|
||||
HNumberAnimation { property: "opacity"; to: 1 }
|
||||
HNumberAnimation { property: "scale"; to: 1 }
|
||||
HNumberAnimation { properties: "x,y" }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
property bool allowDragging: true
|
||||
|
||||
property alias cursorShape: mouseArea.cursorShape
|
||||
|
||||
readonly property int currentItemHeight:
|
||||
currentItem ? currentItem.height : 0
|
||||
|
||||
|
||||
Connections {
|
||||
target: listView
|
||||
enabled: ! listView.allowDragging
|
||||
// interactive gets temporarily set to true below to allow wheel scroll
|
||||
onDraggingChanged: listView.interactive = false
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
enabled: ! parent.allowDragging || cursorShape !== Qt.ArrowCursor
|
||||
acceptedButtons: Qt.NoButton
|
||||
onWheel: {
|
||||
// Allow wheel usage, will be back to false on any drag attempt
|
||||
parent.interactive = true
|
||||
wheel.accepted = false
|
||||
}
|
||||
}
|
||||
}
|
7
src/gui/Base/HLoader.qml
Normal file
7
src/gui/Base/HLoader.qml
Normal file
@@ -0,0 +1,7 @@
|
||||
import QtQuick 2.12
|
||||
|
||||
Loader {
|
||||
id: loader
|
||||
asynchronous: true
|
||||
visible: status === Loader.Ready
|
||||
}
|
31
src/gui/Base/HMenu.qml
Normal file
31
src/gui/Base/HMenu.qml
Normal file
@@ -0,0 +1,31 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
|
||||
Menu {
|
||||
id: menu
|
||||
padding: theme.controls.menu.borderWidth
|
||||
|
||||
implicitWidth: {
|
||||
let result = 0
|
||||
|
||||
for (let i = 0; i < count; ++i) {
|
||||
let item = itemAt(i)
|
||||
if (! item.visible) continue
|
||||
|
||||
result = Math.max(item.implicitWidth, result)
|
||||
}
|
||||
return Math.min(result + menu.padding * 2, window.width)
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: theme.controls.menu.background
|
||||
border.color: theme.controls.menu.border
|
||||
border.width: theme.controls.menu.borderWidth
|
||||
}
|
||||
|
||||
onAboutToShow: previouslyFocused = window.activeFocusItem
|
||||
onClosed: if (previouslyFocused) previouslyFocused.forceActiveFocus()
|
||||
|
||||
|
||||
property var previouslyFocused: null
|
||||
}
|
29
src/gui/Base/HMenuItem.qml
Normal file
29
src/gui/Base/HMenuItem.qml
Normal file
@@ -0,0 +1,29 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
|
||||
MenuItem {
|
||||
id: menuItem
|
||||
spacing: theme.spacing
|
||||
leftPadding: spacing
|
||||
rightPadding: leftPadding
|
||||
topPadding: spacing / 1.75
|
||||
bottomPadding: topPadding
|
||||
height: visible ? implicitHeight : 0
|
||||
|
||||
|
||||
readonly property alias iconItem: contentItem.icon
|
||||
readonly property alias label: contentItem.label
|
||||
|
||||
|
||||
background: HButtonBackground {
|
||||
button: menuItem
|
||||
buttonTheme: theme.controls.menuItem
|
||||
}
|
||||
|
||||
contentItem: HButtonContent {
|
||||
id: contentItem
|
||||
button: menuItem
|
||||
buttonTheme: theme.controls.menuItem
|
||||
label.horizontalAlignment: Label.AlignLeft
|
||||
}
|
||||
}
|
56
src/gui/Base/HMxcImage.qml
Normal file
56
src/gui/Base/HMxcImage.qml
Normal file
@@ -0,0 +1,56 @@
|
||||
import QtQuick 2.12
|
||||
|
||||
HImage {
|
||||
id: image
|
||||
inderterminateProgressBar: isMxc
|
||||
source: sourceOverride || (show ? cachedPath : "")
|
||||
onWidthChanged: Qt.callLater(update)
|
||||
onHeightChanged: Qt.callLater(update)
|
||||
onVisibleChanged: Qt.callLater(update)
|
||||
onMxcChanged: Qt.callLater(update)
|
||||
|
||||
|
||||
property string mxc
|
||||
property string sourceOverride: ""
|
||||
property bool thumbnail: true
|
||||
property var cryptDict: ({})
|
||||
|
||||
property bool show: false
|
||||
property string cachedPath: ""
|
||||
readonly property bool isMxc: mxc.startsWith("mxc://")
|
||||
|
||||
|
||||
function update() {
|
||||
let w = sourceSize.width || width
|
||||
let h = sourceSize.height || height
|
||||
|
||||
if (! image.mxc || w < 1 || h < 1 ) {
|
||||
show = false
|
||||
return
|
||||
}
|
||||
|
||||
if (! image) return // if it was destroyed
|
||||
|
||||
if (! isMxc) {
|
||||
if (source !== mxc) source = mxc
|
||||
show = image.visible
|
||||
return
|
||||
}
|
||||
|
||||
let method = image.thumbnail ? "get_thumbnail" : "get_media"
|
||||
let args = image.thumbnail ?
|
||||
[image.mxc, w, h, cryptDict] : [image.mxc, cryptDict]
|
||||
|
||||
py.callCoro("media_cache." + method, args, path => {
|
||||
if (! image) return
|
||||
if (image.cachedPath !== path) image.cachedPath = path
|
||||
|
||||
image.broken = false
|
||||
image.show = image.visible
|
||||
|
||||
}, () => {
|
||||
image.broken = true
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
33
src/gui/Base/HNoticePage.qml
Normal file
33
src/gui/Base/HNoticePage.qml
Normal file
@@ -0,0 +1,33 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../Base"
|
||||
|
||||
HRowLayout {
|
||||
property alias label: noticeLabel
|
||||
property alias text: noticeLabel.text
|
||||
property alias color: noticeLabel.color
|
||||
property alias font: noticeLabel.font
|
||||
property alias backgroundColor: noticeLabelBackground.color
|
||||
property alias radius: noticeLabelBackground.radius
|
||||
|
||||
HLabel {
|
||||
id: noticeLabel
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.Wrap
|
||||
padding: theme.spacing / 2
|
||||
leftPadding: theme.spacing
|
||||
rightPadding: leftPadding
|
||||
|
||||
opacity: width > 16 * theme.uiScale ? 1 : 0
|
||||
|
||||
background: Rectangle {
|
||||
id: noticeLabelBackground
|
||||
color: theme.controls.box.background
|
||||
radius: theme.controls.box.radius
|
||||
}
|
||||
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.preferredWidth: implicitWidth
|
||||
Layout.maximumWidth: parent.width
|
||||
}
|
||||
}
|
10
src/gui/Base/HNumberAnimation.qml
Normal file
10
src/gui/Base/HNumberAnimation.qml
Normal file
@@ -0,0 +1,10 @@
|
||||
import QtQuick 2.12
|
||||
|
||||
NumberAnimation {
|
||||
property real factor: 1.0
|
||||
property real overshoot: 1.0
|
||||
|
||||
duration: theme.animationDuration * Math.max(overshoot / 1.7, 1.0) * factor
|
||||
easing.type: overshoot > 1 ? Easing.OutBack : Easing.Linear
|
||||
easing.overshoot: overshoot
|
||||
}
|
73
src/gui/Base/HPage.qml
Normal file
73
src/gui/Base/HPage.qml
Normal file
@@ -0,0 +1,73 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../MainPane"
|
||||
|
||||
Page {
|
||||
id: innerPage
|
||||
|
||||
|
||||
default property alias columnData: contentColumn.data
|
||||
|
||||
property alias flickable: innerFlickable
|
||||
property alias headerLabel: innerHeaderLabel
|
||||
property var hideHeaderUnderHeight: null
|
||||
|
||||
property int currentSpacing:
|
||||
Math.min(theme.spacing * width / 400, theme.spacing)
|
||||
|
||||
property bool becomeKeyboardFlickableTarget: true
|
||||
|
||||
|
||||
background: null
|
||||
|
||||
header: Rectangle {
|
||||
implicitWidth: parent ? parent.width : 0
|
||||
color: theme.controls.header.background
|
||||
|
||||
height: innerHeaderLabel.text && (
|
||||
! hideHeaderUnderHeight ||
|
||||
window.height >=
|
||||
hideHeaderUnderHeight +
|
||||
theme.baseElementsHeight +
|
||||
currentSpacing * 2
|
||||
) ? theme.baseElementsHeight : 0
|
||||
|
||||
Behavior on height { HNumberAnimation {} }
|
||||
visible: height > 0
|
||||
|
||||
HLabel {
|
||||
id: innerHeaderLabel
|
||||
anchors.fill: parent
|
||||
textFormat: Text.StyledText
|
||||
font.pixelSize: theme.fontSize.big
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
leftPadding: currentSpacing
|
||||
rightPadding: leftPadding
|
||||
}
|
||||
}
|
||||
|
||||
leftPadding: currentSpacing < theme.spacing ? 0 : currentSpacing
|
||||
rightPadding: leftPadding
|
||||
Behavior on leftPadding { HNumberAnimation {} }
|
||||
|
||||
HFlickable {
|
||||
id: innerFlickable
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
contentWidth: parent.width
|
||||
contentHeight: contentColumn.childrenRect.height
|
||||
|
||||
Component.onCompleted:
|
||||
if (becomeKeyboardFlickableTarget) shortcuts.flickTarget = this
|
||||
|
||||
HColumnLayout {
|
||||
id: contentColumn
|
||||
width: innerFlickable.width
|
||||
height: innerFlickable.height
|
||||
}
|
||||
}
|
||||
}
|
7
src/gui/Base/HPauseAnimation.qml
Normal file
7
src/gui/Base/HPauseAnimation.qml
Normal file
@@ -0,0 +1,7 @@
|
||||
import QtQuick 2.12
|
||||
|
||||
PauseAnimation {
|
||||
property real factor: 1.0
|
||||
|
||||
duration: theme.animationDuration * factor
|
||||
}
|
35
src/gui/Base/HPopup.qml
Normal file
35
src/gui/Base/HPopup.qml
Normal file
@@ -0,0 +1,35 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
|
||||
Popup {
|
||||
id: popup
|
||||
anchors.centerIn: Overlay.overlay
|
||||
modal: true
|
||||
focus: true
|
||||
padding: 0
|
||||
margins: theme.spacing
|
||||
|
||||
enter: Transition {
|
||||
HNumberAnimation { property: "scale"; from: 0; to: 1; overshoot: 4 }
|
||||
}
|
||||
|
||||
exit: Transition {
|
||||
HNumberAnimation { property: "scale"; to: 0 }
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: theme.controls.popup.background
|
||||
}
|
||||
|
||||
onAboutToShow: previouslyFocused = window.activeFocusItem
|
||||
onClosed: if (previouslyFocused) previouslyFocused.forceActiveFocus()
|
||||
|
||||
|
||||
property var previouslyFocused: null
|
||||
|
||||
readonly property int maximumPreferredWidth:
|
||||
window.width - leftMargin - rightMargin - leftInset - rightInset
|
||||
|
||||
readonly property int maximumPreferredHeight:
|
||||
window.height - topMargin - bottomMargin - topInset - bottomInset
|
||||
}
|
44
src/gui/Base/HProgressBar.qml
Normal file
44
src/gui/Base/HProgressBar.qml
Normal file
@@ -0,0 +1,44 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
|
||||
ProgressBar {
|
||||
id: bar
|
||||
|
||||
property color backgroundColor: theme.controls.progressBar.background
|
||||
property color foregroundColor: theme.controls.progressBar.foreground
|
||||
|
||||
background: Rectangle {
|
||||
implicitWidth: 200
|
||||
implicitHeight: theme.controls.progressBar.height
|
||||
color: backgroundColor
|
||||
}
|
||||
|
||||
contentItem: Item {
|
||||
implicitWidth: 200
|
||||
implicitHeight: theme.controls.progressBar.height
|
||||
|
||||
Rectangle {
|
||||
id: indicator
|
||||
width: bar.indeterminate ?
|
||||
parent.width / 8 : bar.visualPosition * parent.width
|
||||
height: parent.height
|
||||
color: foregroundColor
|
||||
|
||||
Behavior on color { HColorAnimation {} }
|
||||
|
||||
HNumberAnimation on x {
|
||||
running: bar.visible && bar.indeterminate
|
||||
duration: theme.animationDuration * 8
|
||||
from: 0
|
||||
to: bar.width - indicator.width
|
||||
|
||||
onStopped: if (bar.indeterminate) {
|
||||
[from, to] = [to, from];
|
||||
start()
|
||||
} else {
|
||||
indicator.x = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
29
src/gui/Base/HRepeater.qml
Normal file
29
src/gui/Base/HRepeater.qml
Normal file
@@ -0,0 +1,29 @@
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick 2.12
|
||||
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
|
||||
readonly property int childrenImplicitWidth: {
|
||||
let total = 0
|
||||
|
||||
for (let i = 0; i < repeater.count; i++) {
|
||||
let item = repeater.itemAt(i)
|
||||
if (item && item.implicitWidth) total += item.implicitWidth
|
||||
}
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
readonly property int childrenWidth: {
|
||||
let total = 0
|
||||
|
||||
for (let i = 0; i < repeater.count; i++) {
|
||||
let item = repeater.itemAt(i)
|
||||
if (item && item.width) total += item.width
|
||||
}
|
||||
|
||||
return total
|
||||
}
|
||||
}
|
14
src/gui/Base/HRichLabel.qml
Normal file
14
src/gui/Base/HRichLabel.qml
Normal file
@@ -0,0 +1,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 {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
}
|
||||
}
|
10
src/gui/Base/HRoomAvatar.qml
Normal file
10
src/gui/Base/HRoomAvatar.qml
Normal file
@@ -0,0 +1,10 @@
|
||||
import QtQuick 2.12
|
||||
|
||||
HAvatar {
|
||||
name: displayName[0] === "#" && displayName.length > 1 ?
|
||||
displayName.substring(1) :
|
||||
displayName
|
||||
|
||||
|
||||
property string displayName
|
||||
}
|
6
src/gui/Base/HRowLayout.qml
Normal file
6
src/gui/Base/HRowLayout.qml
Normal file
@@ -0,0 +1,6 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
|
||||
RowLayout {
|
||||
spacing: 0
|
||||
}
|
108
src/gui/Base/HScrollableTextArea.qml
Normal file
108
src/gui/Base/HScrollableTextArea.qml
Normal file
@@ -0,0 +1,108 @@
|
||||
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
|
||||
ScrollView {
|
||||
id: scrollView
|
||||
opacity: enabled ? 1 : theme.disabledElementsOpacity
|
||||
clip: true
|
||||
ScrollBar.vertical.visible: contentHeight > height
|
||||
|
||||
// Set it only on component creation to avoid binding loops
|
||||
Component.onCompleted: if (! text) {
|
||||
text = window.getState(this, "text", "")
|
||||
textArea.cursorPosition = text.length
|
||||
}
|
||||
|
||||
onTextChanged: window.saveState(this)
|
||||
|
||||
|
||||
default property alias textAreaData: textArea.data
|
||||
|
||||
property string saveName: ""
|
||||
property var saveId: "ALL"
|
||||
property var saveProperties: ["text"]
|
||||
|
||||
property alias backgroundColor: textAreaBackground.color
|
||||
property alias placeholderText: textArea.placeholderText
|
||||
property alias placeholderTextColor: textArea.placeholderTextColor
|
||||
property alias area: textArea
|
||||
property alias text: textArea.text
|
||||
|
||||
property var focusItemOnTab: null
|
||||
property var disabledText: null
|
||||
property string defaultText: ""
|
||||
readonly property bool changed: text !== defaultText
|
||||
|
||||
|
||||
function reset() { area.clear(); text = defaultText }
|
||||
|
||||
|
||||
Behavior on opacity { HNumberAnimation {} }
|
||||
|
||||
TextArea {
|
||||
id: textArea
|
||||
text: defaultText
|
||||
enabled: parent.enabled
|
||||
leftPadding: theme.spacing
|
||||
rightPadding: leftPadding
|
||||
topPadding: theme.spacing / 1.5
|
||||
bottomPadding: topPadding
|
||||
|
||||
readOnly: ! visible
|
||||
selectByMouse: true
|
||||
|
||||
wrapMode: TextEdit.Wrap
|
||||
font.family: theme.fontFamily.sans
|
||||
font.pixelSize: theme.fontSize.normal
|
||||
font.pointSize: -1
|
||||
|
||||
placeholderTextColor: theme.controls.textArea.placeholderText
|
||||
color: theme.controls.textArea.text
|
||||
|
||||
background: Rectangle {
|
||||
id: textAreaBackground
|
||||
color: theme.controls.textArea.background
|
||||
}
|
||||
|
||||
Keys.onPressed: if (
|
||||
event.modifiers & Qt.AltModifier ||
|
||||
event.modifiers & Qt.MetaModifier
|
||||
) event.accepted = true
|
||||
|
||||
KeyNavigation.priority: KeyNavigation.BeforeItem
|
||||
KeyNavigation.tab: focusItemOnTab
|
||||
|
||||
|
||||
Binding on color {
|
||||
value: "transparent"
|
||||
when: disabledText !== null && ! textArea.enabled
|
||||
}
|
||||
|
||||
Binding on placeholderTextColor {
|
||||
value: "transparent"
|
||||
when: disabledText !== null && ! textArea.enabled
|
||||
}
|
||||
|
||||
Behavior on color { HColorAnimation {} }
|
||||
Behavior on placeholderTextColor { HColorAnimation {} }
|
||||
|
||||
HLabel {
|
||||
anchors.fill: parent
|
||||
visible: opacity > 0
|
||||
opacity: disabledText !== null && parent.enabled ? 0 : 1
|
||||
text: disabledText || ""
|
||||
|
||||
leftPadding: parent.leftPadding
|
||||
rightPadding: parent.rightPadding
|
||||
topPadding: parent.topPadding
|
||||
bottomPadding: parent.bottomPadding
|
||||
|
||||
wrapMode: parent.wrapMode
|
||||
font.family: parent.font.family
|
||||
font.pixelSize: parent.font.pixelSize
|
||||
|
||||
Behavior on opacity { HNumberAnimation {} }
|
||||
}
|
||||
}
|
||||
}
|
140
src/gui/Base/HSelectableLabel.qml
Normal file
140
src/gui/Base/HSelectableLabel.qml
Normal file
@@ -0,0 +1,140 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
|
||||
TextEdit {
|
||||
id: label
|
||||
font.family: theme.fontFamily.sans
|
||||
font.pixelSize: theme.fontSize.normal
|
||||
color: theme.colors.text
|
||||
|
||||
textFormat: Label.PlainText
|
||||
tabStopDistance: 4 * 4 // 4 spaces
|
||||
|
||||
readOnly: true
|
||||
persistentSelection: true
|
||||
activeFocusOnPress: false
|
||||
focus: false
|
||||
|
||||
onLinkActivated: Qt.openUrlExternally(link)
|
||||
|
||||
Component.onCompleted: updateSelection()
|
||||
|
||||
|
||||
// If index is a whole number, the label will get two \n before itself
|
||||
// in container.joinedSelection. If it's a decimal number, if gets one \n.
|
||||
property real index
|
||||
property HSelectableLabelContainer container
|
||||
property bool selectable: true
|
||||
|
||||
|
||||
function updateSelection() {
|
||||
if (! selectable && label.selectedText) {
|
||||
label.deselect()
|
||||
updateContainerSelectedTexts()
|
||||
return
|
||||
}
|
||||
|
||||
if (! selectable) return
|
||||
|
||||
if (! container.reversed &&
|
||||
container.selectionStart <= container.selectionEnd ||
|
||||
|
||||
container.reversed &&
|
||||
container.selectionStart > container.selectionEnd)
|
||||
{
|
||||
var first = container.selectionStart
|
||||
var firstPos = container.selectionStartPosition
|
||||
var last = container.selectionEnd
|
||||
var lastPos = container.selectionEndPosition
|
||||
} else {
|
||||
var first = container.selectionEnd
|
||||
var firstPos = container.selectionEndPosition
|
||||
var last = container.selectionStart
|
||||
var lastPos = container.selectionStartPosition
|
||||
}
|
||||
|
||||
if (first === index && last === index) {
|
||||
select(
|
||||
label.positionAt(firstPos.x, firstPos.y),
|
||||
label.positionAt(lastPos.x, lastPos.y),
|
||||
)
|
||||
|
||||
} else if ((! container.reversed && first < index && index < last) ||
|
||||
(container.reversed && first > index && index > last))
|
||||
{
|
||||
label.selectAll()
|
||||
|
||||
} else if (first === index) {
|
||||
label.select(positionAt(firstPos.x, firstPos.y), length)
|
||||
|
||||
} else if (last === index) {
|
||||
label.select(0, positionAt(lastPos.x, lastPos.y))
|
||||
|
||||
} else {
|
||||
label.deselect()
|
||||
}
|
||||
|
||||
updateContainerSelectedTexts()
|
||||
}
|
||||
|
||||
function updateContainerSelectedTexts() {
|
||||
container.selectedTexts[index] = selectedText
|
||||
container.selectedTextsChanged()
|
||||
}
|
||||
|
||||
function selectWordAt(position) {
|
||||
container.clearSelection()
|
||||
label.cursorPosition = positionAt(position.x, position.y)
|
||||
label.selectWord()
|
||||
updateContainerSelectedTexts()
|
||||
}
|
||||
|
||||
function selectAllText() {
|
||||
container.clearSelection()
|
||||
label.selectAll()
|
||||
updateContainerSelectedTexts()
|
||||
}
|
||||
|
||||
|
||||
Connections {
|
||||
target: container
|
||||
onSelectionInfoChanged: updateSelection()
|
||||
onDeselectAll: deselect()
|
||||
}
|
||||
|
||||
DropArea {
|
||||
anchors.fill: parent
|
||||
onPositionChanged: {
|
||||
if (! container.selecting) {
|
||||
container.clearSelection()
|
||||
container.selectionStart = index
|
||||
container.selectionStartPosition = Qt.point(drag.x, drag.y)
|
||||
container.selecting = true
|
||||
} else {
|
||||
container.selectionEnd = index
|
||||
container.selectionEndPosition = Qt.point(drag.x, drag.y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onTapped: {
|
||||
tapCount === 2 ? selectWordAt(eventPoint.position) :
|
||||
tapCount === 3 ? selectAllText() :
|
||||
container.clearSelection()
|
||||
}
|
||||
}
|
||||
|
||||
PointHandler {
|
||||
onActiveChanged:
|
||||
active ? container.dragStarted() : container.dragStopped()
|
||||
onPointChanged: container.dragPointChanged(point)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: label
|
||||
acceptedButtons: Qt.NoButton
|
||||
cursorShape: label.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
|
||||
}
|
||||
}
|
89
src/gui/Base/HSelectableLabelContainer.qml
Normal file
89
src/gui/Base/HSelectableLabelContainer.qml
Normal file
@@ -0,0 +1,89 @@
|
||||
import QtQuick 2.12
|
||||
|
||||
FocusScope {
|
||||
signal deselectAll()
|
||||
signal dragStarted()
|
||||
signal dragStopped()
|
||||
signal dragPointChanged(var eventPoint)
|
||||
|
||||
|
||||
property bool reversed: false
|
||||
|
||||
property bool selecting: false
|
||||
property real selectionStart: -1
|
||||
property real selectionEnd: -1
|
||||
property point selectionStartPosition: Qt.point(-1, -1)
|
||||
property point selectionEndPosition: Qt.point(-1, -1)
|
||||
property var selectedTexts: ({})
|
||||
|
||||
readonly property var selectionInfo: [
|
||||
selectionStart, selectionStartPosition,
|
||||
selectionEnd, selectionEndPosition,
|
||||
]
|
||||
|
||||
readonly property string joinedSelection: {
|
||||
let toCopy = []
|
||||
|
||||
for (let key of Object.keys(selectedTexts).sort()) {
|
||||
if (! selectedTexts[key]) continue
|
||||
|
||||
// For some dumb reason, Object.keys convert the floats to strings
|
||||
toCopy.push(Number.isInteger(parseFloat(key)) ? "\n\n" : "\n")
|
||||
toCopy.push(selectedTexts[key])
|
||||
}
|
||||
|
||||
if (reversed) toCopy.reverse()
|
||||
|
||||
return toCopy.join("").trim()
|
||||
}
|
||||
|
||||
|
||||
onJoinedSelectionChanged:
|
||||
if (joinedSelection) Clipboard.selection = joinedSelection
|
||||
|
||||
onDragStarted: {
|
||||
draggedItem.Drag.active = true
|
||||
}
|
||||
onDragStopped: {
|
||||
draggedItem.Drag.drop()
|
||||
draggedItem.Drag.active = false
|
||||
selecting = false
|
||||
}
|
||||
onDragPointChanged: {
|
||||
let pos = mapFromItem(
|
||||
mainUI, eventPoint.scenePosition.x, eventPoint.scenePosition.y,
|
||||
)
|
||||
draggedItem.x = pos.x
|
||||
draggedItem.y = pos.y
|
||||
}
|
||||
|
||||
|
||||
function clearSelection() {
|
||||
selecting = false
|
||||
selectionStart = -1
|
||||
selectionEnd = -1
|
||||
selectionStartPosition = Qt.point(-1, -1)
|
||||
selectionEndPosition = Qt.point(-1, -1)
|
||||
deselectAll()
|
||||
}
|
||||
|
||||
|
||||
// PointHandler and TapHandler won't activate if the press occurs inside
|
||||
// a label child, so we need a Point/TapHandler inside them too.
|
||||
|
||||
PointHandler {
|
||||
// We don't use a DragHandler because they have an unchangable minimum
|
||||
// drag distance before they activate.
|
||||
id: pointHandler
|
||||
onActiveChanged: active ? dragStarted() : dragStopped()
|
||||
onPointChanged: dragPointChanged(point)
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onTapped: clearSelection()
|
||||
}
|
||||
|
||||
// This item will trigger the children labels's DropAreas
|
||||
Item { id: draggedItem }
|
||||
}
|
5
src/gui/Base/HShortcut.qml
Normal file
5
src/gui/Base/HShortcut.qml
Normal file
@@ -0,0 +1,5 @@
|
||||
import QtQuick 2.12
|
||||
|
||||
Shortcut {
|
||||
context: Qt.ApplicationShortcut
|
||||
}
|
72
src/gui/Base/HSlider.qml
Normal file
72
src/gui/Base/HSlider.qml
Normal file
@@ -0,0 +1,72 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
|
||||
Slider {
|
||||
id: slider
|
||||
leftPadding: 0
|
||||
rightPadding: leftPadding
|
||||
topPadding: 0
|
||||
bottomPadding: topPadding
|
||||
|
||||
property bool enableRadius: true
|
||||
property bool fullHeight: false
|
||||
property color backgroundColor: theme.controls.slider.background
|
||||
property color foregroundColor: theme.controls.slider.foreground
|
||||
|
||||
property alias toolTip: toolTip
|
||||
property alias mouseArea: mouseArea
|
||||
|
||||
background: Rectangle {
|
||||
color: backgroundColor
|
||||
x: slider.leftPadding
|
||||
y: slider.topPadding + slider.availableHeight / 2 - height / 2
|
||||
|
||||
implicitWidth: 200
|
||||
implicitHeight: theme.controls.slider.height
|
||||
width: slider.availableWidth
|
||||
height: fullHeight ? slider.height : implicitHeight
|
||||
radius: enableRadius ? theme.controls.slider.radius : 0
|
||||
|
||||
Rectangle {
|
||||
width: slider.visualPosition * parent.width
|
||||
height: parent.height
|
||||
color: foregroundColor
|
||||
radius: parent.radius
|
||||
}
|
||||
}
|
||||
|
||||
handle: Rectangle {
|
||||
x: slider.leftPadding + slider.visualPosition *
|
||||
(slider.availableWidth - width)
|
||||
y: slider.topPadding + slider.availableHeight / 2 - height / 2
|
||||
|
||||
implicitWidth: theme.controls.slider.handle.size
|
||||
implicitHeight: implicitWidth
|
||||
radius: implicitWidth / 2
|
||||
|
||||
color: slider.pressed ?
|
||||
theme.controls.slider.handle.pressedInside :
|
||||
theme.controls.slider.handle.inside
|
||||
|
||||
border.color: slider.pressed ?
|
||||
theme.controls.slider.handle.pressedBorder :
|
||||
theme.controls.slider.handle.border
|
||||
|
||||
Behavior on color { HColorAnimation {} }
|
||||
Behavior on border.color { HColorAnimation {} }
|
||||
}
|
||||
|
||||
HToolTip {
|
||||
id: toolTip
|
||||
parent: slider.handle
|
||||
visible: slider.pressed && text
|
||||
delay: 0
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
cursorShape: slider.hovered ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
}
|
||||
}
|
7
src/gui/Base/HSpacer.qml
Normal file
7
src/gui/Base/HSpacer.qml
Normal file
@@ -0,0 +1,7 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
}
|
27
src/gui/Base/HSwipeView.qml
Normal file
27
src/gui/Base/HSwipeView.qml
Normal file
@@ -0,0 +1,27 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
|
||||
SwipeView {
|
||||
Component.onCompleted: if (! changed) {
|
||||
if (becomeKeyboardTabsTarget) shortcuts.tabsTarget = this
|
||||
setCurrentIndex(window.getState(this, "currentIndex", defaultIndex))
|
||||
saveEnabled = true
|
||||
}
|
||||
|
||||
onCurrentIndexChanged: if (saveEnabled) window.saveState(this)
|
||||
|
||||
|
||||
property string saveName: ""
|
||||
property var saveId: "ALL"
|
||||
property var saveProperties: ["currentIndex"]
|
||||
|
||||
// Prevent onCurrentIndexChanged from running before Component.onCompleted
|
||||
property bool saveEnabled: false
|
||||
|
||||
property bool becomeKeyboardTabsTarget: true
|
||||
property int defaultIndex: 0
|
||||
property bool changed: currentIndex !== defaultIndex
|
||||
|
||||
|
||||
function reset() { setCurrentIndex(defaultIndex) }
|
||||
}
|
7
src/gui/Base/HTabBar.qml
Normal file
7
src/gui/Base/HTabBar.qml
Normal file
@@ -0,0 +1,7 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
|
||||
TabBar {
|
||||
spacing: 0
|
||||
position: TabBar.Header
|
||||
}
|
56
src/gui/Base/HTabButton.qml
Normal file
56
src/gui/Base/HTabButton.qml
Normal file
@@ -0,0 +1,56 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
|
||||
TabButton {
|
||||
id: button
|
||||
spacing: theme.spacing
|
||||
topPadding: spacing / 1.5
|
||||
bottomPadding: topPadding
|
||||
leftPadding: spacing
|
||||
rightPadding: leftPadding
|
||||
|
||||
iconItem.svgName: loading ? "hourglass" : icon.name
|
||||
icon.color: theme.icons.colorize
|
||||
|
||||
implicitWidth: Math.max(
|
||||
implicitBackgroundWidth + leftInset + rightInset,
|
||||
// FIXME: why is *2 needed to not get ellided text in AddAccount page?
|
||||
implicitContentWidth + leftPadding * 2 + rightPadding * 2,
|
||||
)
|
||||
implicitHeight: Math.max(
|
||||
implicitBackgroundHeight + topInset + bottomInset,
|
||||
implicitContentHeight + topPadding + bottomPadding,
|
||||
)
|
||||
|
||||
// Prevent button from gaining focus and being highlighted on click
|
||||
focusPolicy: Qt.TabFocus
|
||||
|
||||
|
||||
readonly property alias iconItem: contentItem.icon
|
||||
readonly property alias label: contentItem.label
|
||||
|
||||
property color backgroundColor:
|
||||
TabBar.index % 2 === 0 ?
|
||||
theme.controls.tab.background : theme.controls.tab.alternateBackground
|
||||
|
||||
property bool loading: false
|
||||
|
||||
property HToolTip toolTip: HToolTip {
|
||||
id: toolTip
|
||||
visible: text && hovered
|
||||
}
|
||||
|
||||
|
||||
background: HButtonBackground {
|
||||
button: button
|
||||
buttonTheme: theme.controls.tab
|
||||
color: backgroundColor
|
||||
}
|
||||
|
||||
contentItem: HButtonContent {
|
||||
id: contentItem
|
||||
button: button
|
||||
buttonTheme: theme.controls.tab
|
||||
}
|
||||
}
|
38
src/gui/Base/HTabContainer.qml
Normal file
38
src/gui/Base/HTabContainer.qml
Normal file
@@ -0,0 +1,38 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
|
||||
HColumnLayout {
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.fillWidth: false
|
||||
Layout.fillHeight: false
|
||||
Layout.maximumWidth: parent.width
|
||||
|
||||
property alias tabIndex: tabBar.currentIndex
|
||||
property alias tabModel: tabRepeater.model
|
||||
default property alias data: swipeView.contentData
|
||||
|
||||
HTabBar {
|
||||
id: tabBar
|
||||
Component.onCompleted: shortcuts.tabsTarget = this
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
Repeater {
|
||||
id: tabRepeater
|
||||
HTabButton { text: modelData }
|
||||
}
|
||||
}
|
||||
|
||||
SwipeView {
|
||||
id: swipeView
|
||||
clip: true
|
||||
currentIndex: tabBar.currentIndex
|
||||
interactive: false
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
Behavior on implicitWidth { HNumberAnimation {} }
|
||||
Behavior on implicitHeight { HNumberAnimation {} }
|
||||
}
|
||||
}
|
104
src/gui/Base/HTextField.qml
Normal file
104
src/gui/Base/HTextField.qml
Normal file
@@ -0,0 +1,104 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
|
||||
TextField {
|
||||
id: field
|
||||
text: defaultText
|
||||
opacity: enabled ? 1 : theme.disabledElementsOpacity
|
||||
selectByMouse: true
|
||||
leftPadding: theme.spacing
|
||||
rightPadding: leftPadding
|
||||
topPadding: theme.spacing / 1.5
|
||||
bottomPadding: topPadding
|
||||
|
||||
font.family: theme.fontFamily.sans
|
||||
font.pixelSize: theme.fontSize.normal
|
||||
font.pointSize: -1
|
||||
|
||||
placeholderTextColor: theme.controls.textField.placeholderText
|
||||
color: activeFocus ?
|
||||
theme.controls.textField.focusedText :
|
||||
theme.controls.textField.text
|
||||
|
||||
background: Rectangle {
|
||||
id: textFieldBackground
|
||||
color: field.activeFocus ? focusedBackgroundColor : backgroundColor
|
||||
border.color: error ? errorBorder :
|
||||
field.activeFocus ? focusedBorderColor : borderColor
|
||||
border.width: bordered ? theme.controls.textField.borderWidth : 0
|
||||
|
||||
Behavior on color { HColorAnimation { factor: 0.25 } }
|
||||
Behavior on border.color { HColorAnimation { factor: 0.25 } }
|
||||
}
|
||||
|
||||
// Set it only on component creation to avoid binding loops
|
||||
Component.onCompleted: if (! text) {
|
||||
text = window.getState(this, "text", "")
|
||||
cursorPosition = text.length
|
||||
}
|
||||
|
||||
onTextChanged: window.saveState(this)
|
||||
|
||||
Keys.onPressed: if (
|
||||
event.modifiers & Qt.AltModifier ||
|
||||
event.modifiers & Qt.MetaModifier
|
||||
) event.accepted = true // XXX Still needed?
|
||||
|
||||
|
||||
property string saveName: ""
|
||||
property var saveId: "ALL"
|
||||
property var saveProperties: ["text"]
|
||||
|
||||
property bool error: false
|
||||
|
||||
property alias radius: textFieldBackground.radius
|
||||
property bool bordered: true
|
||||
|
||||
property color backgroundColor: theme.controls.textField.background
|
||||
property color borderColor: theme.controls.textField.border
|
||||
property color errorBorder: theme.controls.textField.errorBorder
|
||||
|
||||
property color focusedBackgroundColor:
|
||||
theme.controls.textField.focusedBackground
|
||||
property color focusedBorderColor: theme.controls.textField.focusedBorder
|
||||
|
||||
property var disabledText: null
|
||||
property string defaultText: ""
|
||||
readonly property bool changed: text !== defaultText
|
||||
|
||||
|
||||
function reset() { clear(); text = defaultText }
|
||||
|
||||
|
||||
Binding on color {
|
||||
value: "transparent"
|
||||
when: disabledText !== null && ! field.enabled
|
||||
}
|
||||
|
||||
Binding on placeholderTextColor {
|
||||
value: "transparent"
|
||||
when: disabledText !== null && ! field.enabled
|
||||
}
|
||||
|
||||
Behavior on opacity { HNumberAnimation {} }
|
||||
Behavior on color { HColorAnimation {} }
|
||||
Behavior on placeholderTextColor { HColorAnimation {} }
|
||||
|
||||
HLabel {
|
||||
anchors.fill: parent
|
||||
visible: opacity > 0
|
||||
opacity: disabledText !== null && parent.enabled ? 0 : 1
|
||||
text: disabledText || ""
|
||||
|
||||
leftPadding: parent.leftPadding
|
||||
rightPadding: parent.rightPadding
|
||||
topPadding: parent.topPadding
|
||||
bottomPadding: parent.bottomPadding
|
||||
|
||||
wrapMode: parent.wrapMode
|
||||
font.family: parent.font.family
|
||||
font.pixelSize: parent.font.pixelSize
|
||||
|
||||
Behavior on opacity { HNumberAnimation {} }
|
||||
}
|
||||
}
|
110
src/gui/Base/HTile.qml
Normal file
110
src/gui/Base/HTile.qml
Normal file
@@ -0,0 +1,110 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
|
||||
HButton {
|
||||
id: tile
|
||||
|
||||
signal leftClicked()
|
||||
signal rightClicked()
|
||||
|
||||
default property alias additionalData: contentItem.data
|
||||
|
||||
property real contentOpacity: 1
|
||||
|
||||
readonly property alias title: title
|
||||
readonly property alias additionalInfo: additionalInfo
|
||||
readonly property alias rightInfo: rightInfo
|
||||
readonly property alias subtitle: subtitle
|
||||
|
||||
property alias contextMenu: contextMenuLoader.sourceComponent
|
||||
|
||||
property Component image
|
||||
|
||||
contentItem: HRowLayout {
|
||||
id: contentItem
|
||||
spacing: tile.spacing
|
||||
opacity: tile.contentOpacity
|
||||
|
||||
HLoader {
|
||||
sourceComponent: image
|
||||
}
|
||||
|
||||
HColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
HRowLayout {
|
||||
spacing: tile.spacing
|
||||
|
||||
HLabel {
|
||||
id: title
|
||||
text: "Missing title"
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Qt.AlignVCenter
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
HRowLayout {
|
||||
id: additionalInfo
|
||||
visible: visibleChildren.length > 0
|
||||
}
|
||||
|
||||
HLabel {
|
||||
id: rightInfo
|
||||
font.pixelSize: theme.fontSize.small
|
||||
color: theme.colors.halfDimText
|
||||
|
||||
visible: Layout.maximumWidth > 0
|
||||
Layout.fillHeight: true
|
||||
Layout.maximumWidth:
|
||||
text && tile.width >= 160 * theme.uiScale ?
|
||||
implicitWidth : 0
|
||||
|
||||
Behavior on Layout.maximumWidth { HNumberAnimation {} }
|
||||
}
|
||||
}
|
||||
|
||||
HRichLabel {
|
||||
id: subtitle
|
||||
textFormat: Text.StyledText
|
||||
font.pixelSize: theme.fontSize.small
|
||||
elide: Text.ElideRight
|
||||
color: theme.colors.dimText
|
||||
|
||||
visible: Layout.maximumHeight > 0
|
||||
Layout.maximumHeight: text ? implicitWidth : 0
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
Behavior on Layout.maximumHeight { HNumberAnimation {} }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onTapped: leftClicked()
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
onTapped: {
|
||||
rightClicked()
|
||||
if (contextMenu) contextMenuLoader.active = true
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
enabled: contextMenuLoader.status === Loader.Ready
|
||||
target: contextMenuLoader.item
|
||||
onClosed: contextMenuLoader.active = false
|
||||
}
|
||||
|
||||
HLoader {
|
||||
id: contextMenuLoader
|
||||
active: false
|
||||
onLoaded: item.popup()
|
||||
}
|
||||
}
|
34
src/gui/Base/HTileDelegate.qml
Normal file
34
src/gui/Base/HTileDelegate.qml
Normal file
@@ -0,0 +1,34 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
|
||||
HTile {
|
||||
id: tile
|
||||
onActivated: view.currentIndex = model.index
|
||||
onLeftClicked: {
|
||||
view.highlightRangeMode = ListView.NoHighlightRange
|
||||
view.highlightMoveDuration = 0
|
||||
activated()
|
||||
view.highlightRangeMode = ListView.ApplyRange
|
||||
view.highlightMoveDuration = theme.animationDuration
|
||||
}
|
||||
|
||||
|
||||
signal activated()
|
||||
|
||||
property HListView view: ListView.view
|
||||
property bool shouldBeCurrent: false
|
||||
|
||||
readonly property QtObject delegateModel: model
|
||||
|
||||
readonly property alias setCurrentTimer: setCurrentTimer
|
||||
|
||||
|
||||
Timer {
|
||||
id: setCurrentTimer
|
||||
interval: 100
|
||||
repeat: true
|
||||
running: true
|
||||
// Component.onCompleted won't work for this
|
||||
onTriggered: if (shouldBeCurrent) view.currentIndex = model.index
|
||||
}
|
||||
}
|
58
src/gui/Base/HToolTip.qml
Normal file
58
src/gui/Base/HToolTip.qml
Normal file
@@ -0,0 +1,58 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
|
||||
ToolTip {
|
||||
id: toolTip
|
||||
delay: theme.controls.toolTip.delay
|
||||
padding: background.border.width
|
||||
|
||||
|
||||
property alias label: label
|
||||
property alias backgroundColor: background.color
|
||||
|
||||
readonly property bool hideNow: ! window.hovered
|
||||
|
||||
onHideNowChanged: if (visible && hideNow) toolTip.hide()
|
||||
|
||||
|
||||
background: Rectangle {
|
||||
id: background
|
||||
color: theme.controls.toolTip.background
|
||||
border.color: theme.controls.toolTip.border
|
||||
border.width: theme.controls.toolTip.borderWidth
|
||||
}
|
||||
|
||||
contentItem: HRowLayout {
|
||||
HLabel {
|
||||
id: label
|
||||
color: theme.controls.toolTip.text
|
||||
text: toolTip.text
|
||||
wrapMode: Text.Wrap
|
||||
|
||||
leftPadding: theme.spacing / 1.5
|
||||
rightPadding: leftPadding
|
||||
topPadding: theme.spacing / 2
|
||||
bottomPadding: topPadding
|
||||
|
||||
Layout.maximumWidth: Math.min(
|
||||
window.width / 1.25, theme.fontSize.normal * 0.5 * 75,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
enter: Transition {
|
||||
HNumberAnimation { property: "opacity"; from: 0.0; to: 1.0 }
|
||||
}
|
||||
exit: Transition {
|
||||
HNumberAnimation { property: "opacity"; to: 0.0 }
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
onTapped: toolTip.hide()
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
onHoveredChanged: if (! hovered) toolTip.hide()
|
||||
}
|
||||
}
|
51
src/gui/Base/HUserAvatar.qml
Normal file
51
src/gui/Base/HUserAvatar.qml
Normal file
@@ -0,0 +1,51 @@
|
||||
import QtQuick 2.12
|
||||
|
||||
HAvatar {
|
||||
name: displayName || userId.substring(1) // no leading @
|
||||
|
||||
|
||||
property string userId
|
||||
property string displayName
|
||||
property int powerLevel: 0
|
||||
property bool shiftMembershipIconPosition: true
|
||||
property bool invited: false
|
||||
|
||||
readonly property bool admin: powerLevel >= 100
|
||||
readonly property bool moderator: powerLevel >= 50 && ! admin
|
||||
|
||||
|
||||
HLoader {
|
||||
active: admin || moderator || invited
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.topMargin: shiftMembershipIconPosition ? -16 / 2 : 0
|
||||
anchors.leftMargin: anchors.topMargin
|
||||
z: 100
|
||||
|
||||
Behavior on anchors.topMargin { HNumberAnimation {} }
|
||||
|
||||
sourceComponent: HIcon {
|
||||
small: true
|
||||
svgName:
|
||||
invited ? "user-invited" :
|
||||
admin ? "user-power-100" :
|
||||
"user-power-50"
|
||||
|
||||
colorize:
|
||||
invited ? theme.chat.roomPane.member.invitedIcon :
|
||||
admin ? theme.chat.roomPane.member.adminIcon :
|
||||
theme.chat.roomPane.member.moderatorIcon
|
||||
|
||||
HoverHandler { id: membershipIcon }
|
||||
|
||||
HToolTip {
|
||||
visible: membershipIcon.hovered
|
||||
text:
|
||||
invited ? qsTr("Invited") :
|
||||
admin ? qsTr("Admin (%1 power)").arg(powerLevel) :
|
||||
qsTr("Moderator (%1 power)").arg(powerLevel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
23
src/gui/Base/MediaPlayer/AudioPlayer.qml
Normal file
23
src/gui/Base/MediaPlayer/AudioPlayer.qml
Normal file
@@ -0,0 +1,23 @@
|
||||
import QtQuick 2.12
|
||||
import QtAV 1.7
|
||||
|
||||
OSD {
|
||||
id: osd
|
||||
audioOnly: true
|
||||
media: audioPlayer
|
||||
|
||||
implicitWidth: osd.width
|
||||
implicitHeight: osd.height
|
||||
|
||||
|
||||
property alias source: audioPlayer.source
|
||||
|
||||
|
||||
MediaPlayer {
|
||||
id: audioPlayer
|
||||
autoLoad: window.settings.media.autoLoad
|
||||
autoPlay: window.settings.media.autoPlay
|
||||
volume: window.settings.media.defaultVolume / 100
|
||||
muted: window.settings.media.startMuted
|
||||
}
|
||||
}
|
273
src/gui/Base/MediaPlayer/OSD.qml
Normal file
273
src/gui/Base/MediaPlayer/OSD.qml
Normal file
@@ -0,0 +1,273 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import QtAV 1.7
|
||||
import "../../Base"
|
||||
|
||||
HColumnLayout {
|
||||
id: osd
|
||||
visible: osdScaleTransform.yScale > 0
|
||||
|
||||
transform: Scale {
|
||||
id: osdScaleTransform
|
||||
yScale: audioOnly ||
|
||||
osdHover.hovered ||
|
||||
media.playbackState !== MediaPlayer.PlayingState ||
|
||||
osd.showup ?
|
||||
1 : 0
|
||||
origin.y: osd.height
|
||||
|
||||
Behavior on yScale { HNumberAnimation {} }
|
||||
}
|
||||
|
||||
|
||||
property QtObject media: parent // QtAV.Video or QtAV.MediaPlayer
|
||||
property bool audioOnly: false
|
||||
property bool showup: false
|
||||
property bool fullScreen: false
|
||||
|
||||
property real savedAspectRatio: 16 / 9
|
||||
property int savedDuration: 0
|
||||
readonly property real aspectRatio: media.sourceAspectRatio || 0
|
||||
readonly property int duration: media.duration
|
||||
readonly property int boundPosition:
|
||||
savedDuration ?
|
||||
Math.min(media.position, savedDuration) : media.position
|
||||
|
||||
|
||||
onShowupChanged: if (showup) osdHideTimer.restart()
|
||||
onDurationChanged: if (duration) savedDuration = duration
|
||||
onAspectRatioChanged: if (aspectRatio) savedAspectRatio = aspectRatio
|
||||
|
||||
|
||||
function togglePlay() {
|
||||
media.playbackState === MediaPlayer.PlayingState ?
|
||||
media.pause() : media.play()
|
||||
}
|
||||
|
||||
function seekToPosition(pos) { // pos: 0.0 to 1.0
|
||||
if (media.playbackState === MediaPlayer.StoppedState) media.play()
|
||||
if (media.seekable) media.seek(pos * (savedDuration || boundPosition))
|
||||
}
|
||||
|
||||
|
||||
HoverHandler { id: osdHover }
|
||||
|
||||
Timer {
|
||||
id: osdHideTimer
|
||||
interval: window.settings.media.autoHideOSDAfterMsec
|
||||
onTriggered: osd.showup = false
|
||||
}
|
||||
|
||||
HSlider {
|
||||
id: timeSlider
|
||||
topPadding: 5
|
||||
z: 1
|
||||
to: savedDuration || boundPosition
|
||||
backgroundColor: theme.mediaPlayer.progress.background
|
||||
enableRadius: false
|
||||
fullHeight: true
|
||||
mouseArea.hoverEnabled: true
|
||||
|
||||
onMoved: seekToPosition(timeSlider.position)
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: theme.mediaPlayer.progress.height
|
||||
|
||||
HToolTip {
|
||||
id: previewToolTip
|
||||
x: timeSlider.mouseArea.mouseX - width / 2
|
||||
visible: ! audioOnly &&
|
||||
|
||||
preview.implicitWidth >=
|
||||
previewLabel.implicitWidth + previewLabel.padding &&
|
||||
|
||||
preview.implicitHeight >=
|
||||
previewLabel.implicitHeight + previewLabel.padding &&
|
||||
|
||||
! timeSlider.pressed && timeSlider.mouseArea.containsMouse
|
||||
|
||||
readonly property int wantTimestamp:
|
||||
visible ?
|
||||
savedDuration *
|
||||
(timeSlider.mouseArea.mouseX / timeSlider.mouseArea.width) :
|
||||
-1
|
||||
|
||||
Timer {
|
||||
interval: 300
|
||||
running: previewToolTip.visible
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: preview.timestamp = previewToolTip.wantTimestamp
|
||||
}
|
||||
|
||||
contentItem: VideoPreview {
|
||||
id: preview
|
||||
implicitHeight: Math.min(
|
||||
theme.mediaPlayer.hoverPreview.maxHeight,
|
||||
media.height - osd.height - theme.spacing
|
||||
)
|
||||
implicitWidth: Math.min(
|
||||
implicitHeight * savedAspectRatio,
|
||||
media.width - theme.spacing,
|
||||
)
|
||||
file: media.source
|
||||
|
||||
HLabel {
|
||||
id: previewLabel
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: padding / 4
|
||||
text: utils.formatDuration(previewToolTip.wantTimestamp)
|
||||
padding: theme.spacing / 2
|
||||
opacity: previewToolTip.wantTimestamp === -1 ? 0 : 1
|
||||
|
||||
background: Rectangle {
|
||||
color: theme.mediaPlayer.controls.background
|
||||
radius: theme.radius
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binding on value {
|
||||
value: boundPosition
|
||||
when: ! timeSlider.pressed
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
color: theme.mediaPlayer.controls.background
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: childrenRect.height
|
||||
|
||||
HRowLayout {
|
||||
width: parent.width
|
||||
|
||||
OSDButton {
|
||||
readonly property string mode:
|
||||
media.playbackState === MediaPlayer.StoppedState &&
|
||||
savedDuration &&
|
||||
boundPosition >= savedDuration - 500 ?
|
||||
"restart" :
|
||||
|
||||
media.playbackState === MediaPlayer.PlayingState ? "pause" :
|
||||
|
||||
"play"
|
||||
|
||||
icon.name: "player-" + mode
|
||||
toolTip.text: qsTr(
|
||||
mode === "play" ? "Play" :
|
||||
mode === "pause" ? "Pause" :
|
||||
"Restart"
|
||||
)
|
||||
onClicked: togglePlay()
|
||||
}
|
||||
|
||||
// OSDButton {
|
||||
// icon.name: "player-loop"
|
||||
// visible: false
|
||||
// }
|
||||
|
||||
OSDButton {
|
||||
id: volumeButton
|
||||
icon.name: "player-volume-" + (
|
||||
media.muted ? "mute" : media.volume > 0.5 ? "high" : "low"
|
||||
)
|
||||
text: media.muted ? "" : Math.round(media.volume * 100)
|
||||
toolTip.text: media.muted ? qsTr("Unmute") : qsTr("Mute")
|
||||
onClicked: media.muted = ! media.muted
|
||||
}
|
||||
|
||||
HSlider {
|
||||
value: media.volume
|
||||
onMoved: media.volume = value
|
||||
|
||||
visible: Layout.preferredWidth > 0
|
||||
Layout.preferredWidth:
|
||||
! media.muted &&
|
||||
(hovered || pressed || volumeButton.hovered) ?
|
||||
theme.mediaPlayer.controls.volumeSliderWidth : 0
|
||||
Layout.fillHeight: true
|
||||
|
||||
Behavior on Layout.preferredWidth { HNumberAnimation {} }
|
||||
}
|
||||
|
||||
OSDButton {
|
||||
id: speedButton
|
||||
icon.name: "player-speed"
|
||||
text: qsTr("%1x").arg(utils.round(media.playbackRate))
|
||||
toolTip.text: qsTr("Reset speed")
|
||||
onClicked: media.playbackRate = 1
|
||||
}
|
||||
|
||||
HSlider {
|
||||
id: speedSlider
|
||||
from: 0.2
|
||||
to: 4
|
||||
value: media.playbackRate
|
||||
stepSize: 0.2
|
||||
snapMode: HSlider.SnapAlways
|
||||
|
||||
onMoved: media.playbackRate = value
|
||||
|
||||
visible: Layout.preferredWidth > 0
|
||||
Layout.preferredWidth:
|
||||
(hovered || pressed || speedButton.hovered) ?
|
||||
theme.mediaPlayer.controls.speedSliderWidth : 0
|
||||
Layout.fillHeight: true
|
||||
|
||||
Behavior on Layout.preferredWidth { HNumberAnimation {} }
|
||||
}
|
||||
|
||||
OSDLabel {
|
||||
text: boundPosition && savedDuration ?
|
||||
|
||||
qsTr("%1 / %2")
|
||||
.arg(utils.formatDuration(boundPosition))
|
||||
.arg(utils.formatDuration(savedDuration)) :
|
||||
|
||||
boundPosition || savedDuration ?
|
||||
utils.formatDuration(boundPosition || savedDuration) :
|
||||
|
||||
""
|
||||
}
|
||||
|
||||
HSpacer {}
|
||||
|
||||
OSDLabel {
|
||||
text: boundPosition && savedDuration ?
|
||||
qsTr("-%1").arg(
|
||||
utils.formatDuration(savedDuration - boundPosition)
|
||||
) : ""
|
||||
}
|
||||
|
||||
// OSDButton {
|
||||
// icon.name: "player-track-video"
|
||||
// }
|
||||
|
||||
// OSDButton {
|
||||
// icon.name: "player-track-audio"
|
||||
// }
|
||||
|
||||
// OSDButton {
|
||||
// icon.name: "player-track-subtitle"
|
||||
// }
|
||||
|
||||
OSDButton {
|
||||
icon.name: "download"
|
||||
toolTip.text: qsTr("Download")
|
||||
onClicked: Qt.openUrlExternally(media.source)
|
||||
}
|
||||
|
||||
OSDButton {
|
||||
id: fullScreenButton
|
||||
visible: ! audioOnly
|
||||
icon.name: "player-fullscreen" + (fullScreen ? "-exit" : "")
|
||||
toolTip.text: fullScreen ?
|
||||
qsTr("Exit fullscreen") : qsTr("Fullscreen")
|
||||
onClicked: fullScreen = ! fullScreen
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
8
src/gui/Base/MediaPlayer/OSDButton.qml
Normal file
8
src/gui/Base/MediaPlayer/OSDButton.qml
Normal file
@@ -0,0 +1,8 @@
|
||||
import QtQuick 2.12
|
||||
import "../../Base"
|
||||
|
||||
|
||||
HButton {
|
||||
backgroundColor: "transparent"
|
||||
iconItem.dimension: theme.mediaPlayer.controls.iconSize
|
||||
}
|
9
src/gui/Base/MediaPlayer/OSDLabel.qml
Normal file
9
src/gui/Base/MediaPlayer/OSDLabel.qml
Normal file
@@ -0,0 +1,9 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../../Base"
|
||||
|
||||
|
||||
HLabel {
|
||||
Layout.leftMargin: theme.spacing / 2
|
||||
Layout.rightMargin: Layout.leftMargin
|
||||
}
|
68
src/gui/Base/MediaPlayer/VideoPlayer.qml
Normal file
68
src/gui/Base/MediaPlayer/VideoPlayer.qml
Normal file
@@ -0,0 +1,68 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Window 2.12
|
||||
import QtAV 1.7
|
||||
|
||||
Video {
|
||||
id: video
|
||||
autoLoad: window.settings.media.autoLoad
|
||||
autoPlay: window.settings.media.autoPlay
|
||||
volume: window.settings.media.defaultVolume / 100
|
||||
muted: window.settings.media.startMuted
|
||||
implicitWidth: fullScreen ? window.width : 640
|
||||
implicitHeight: fullScreen ? window.height : (width / osd.savedAspectRatio)
|
||||
|
||||
|
||||
property bool hovered: false
|
||||
property alias fullScreen: osd.fullScreen
|
||||
|
||||
property int oldVisibility: Window.Windowed
|
||||
property QtObject oldParent: video.parent
|
||||
|
||||
|
||||
onFullScreenChanged: {
|
||||
if (fullScreen) {
|
||||
oldVisibility = window.visibility
|
||||
window.visibility = Window.FullScreen
|
||||
|
||||
oldParent = video.parent
|
||||
video.parent = mainUI.fullScreenPopup.contentItem
|
||||
|
||||
mainUI.fullScreenPopup.open()
|
||||
|
||||
} else {
|
||||
window.visibility = oldVisibility
|
||||
mainUI.fullScreenPopup.close()
|
||||
|
||||
video.parent = oldParent
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Connections {
|
||||
target: mainUI.fullScreenPopup
|
||||
onClosed: fullScreen = false
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
onTapped: osd.togglePlay()
|
||||
onDoubleTapped: video.fullScreen = ! video.fullScreen
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
width: parent.width
|
||||
height: parent.height - (osd.visible ? osd.height : 0)
|
||||
acceptedButtons: Qt.NoButton
|
||||
hoverEnabled: true
|
||||
propagateComposedEvents: true
|
||||
|
||||
onContainsMouseChanged: video.hovered = containsMouse
|
||||
onMouseXChanged: osd.showup = true
|
||||
onMouseYChanged: osd.showup = true
|
||||
}
|
||||
|
||||
OSD {
|
||||
id: osd
|
||||
width: parent.width
|
||||
anchors.bottom: parent.bottom
|
||||
}
|
||||
}
|
93
src/gui/Chat/Banners/Banner.qml
Normal file
93
src/gui/Chat/Banners/Banner.qml
Normal file
@@ -0,0 +1,93 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../../Base"
|
||||
|
||||
Rectangle {
|
||||
id: banner
|
||||
implicitHeight: childrenRect.height
|
||||
color: theme.controls.box.background
|
||||
|
||||
property alias avatar: bannerAvatar
|
||||
property alias icon: bannerIcon
|
||||
property alias labelText: bannerLabel.text
|
||||
property alias buttonModel: bannerRepeater.model
|
||||
property var buttonCallbacks: []
|
||||
|
||||
HGridLayout {
|
||||
id: bannerGrid
|
||||
width: parent.width
|
||||
flow: bannerAvatarWrapper.width +
|
||||
bannerIcon.width +
|
||||
bannerLabel.implicitWidth +
|
||||
bannerButtons.width >
|
||||
parent.width ?
|
||||
GridLayout.TopToBottom : GridLayout.LeftToRight
|
||||
|
||||
HRowLayout {
|
||||
id: bannerRow
|
||||
|
||||
Rectangle {
|
||||
id: bannerAvatarWrapper
|
||||
color: "black"
|
||||
|
||||
Layout.preferredWidth: bannerAvatar.width
|
||||
Layout.minimumHeight: bannerAvatar.height
|
||||
Layout.preferredHeight: bannerLabel.height
|
||||
|
||||
HUserAvatar {
|
||||
id: bannerAvatar
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
|
||||
HIcon {
|
||||
id: bannerIcon
|
||||
visible: Boolean(svgName)
|
||||
|
||||
Layout.leftMargin: theme.spacing / 2
|
||||
}
|
||||
|
||||
HLabel {
|
||||
id: bannerLabel
|
||||
textFormat: Text.StyledText
|
||||
wrapMode: Text.Wrap
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: bannerIcon.Layout.leftMargin
|
||||
Layout.rightMargin: Layout.leftMargin
|
||||
}
|
||||
|
||||
HSpacer {}
|
||||
}
|
||||
|
||||
HRowLayout {
|
||||
HRowLayout {
|
||||
id: bannerButtons
|
||||
|
||||
Repeater {
|
||||
id: bannerRepeater
|
||||
model: []
|
||||
|
||||
HButton {
|
||||
id: button
|
||||
text: modelData.text
|
||||
icon.name: modelData.iconName
|
||||
icon.color: modelData.iconColor || theme.icons.colorize
|
||||
onClicked: buttonCallbacks[modelData.name](button)
|
||||
|
||||
Layout.preferredHeight: theme.baseElementsHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: buttonsRightPadding
|
||||
color: theme.controls.button.background
|
||||
visible: bannerGrid.flow === GridLayout.TopToBottom
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
51
src/gui/Chat/Banners/InviteBanner.qml
Normal file
51
src/gui/Chat/Banners/InviteBanner.qml
Normal file
@@ -0,0 +1,51 @@
|
||||
import QtQuick 2.12
|
||||
import "../../Base"
|
||||
|
||||
Banner {
|
||||
property string inviterId: chat.roomInfo.inviter
|
||||
property string inviterName: chat.roomInfo.inviter_name
|
||||
property string inviterAvatar: chat.roomInfo.inviter_avatar
|
||||
|
||||
color: theme.chat.inviteBanner.background
|
||||
|
||||
avatar.userId: inviterId
|
||||
avatar.displayName: inviterName
|
||||
avatar.mxc: inviterAvatar
|
||||
|
||||
labelText: qsTr("%1 invited you to this room").arg(
|
||||
utils.coloredNameHtml(inviterName, inviterId)
|
||||
)
|
||||
|
||||
buttonModel: [
|
||||
{
|
||||
name: "accept",
|
||||
text: qsTr("Join"),
|
||||
iconName: "invite-accept",
|
||||
iconColor: theme.colors.positiveBackground
|
||||
},
|
||||
{
|
||||
name: "decline",
|
||||
text: qsTr("Decline"),
|
||||
iconName: "invite-decline",
|
||||
iconColor: theme.colors.negativeBackground
|
||||
}
|
||||
]
|
||||
|
||||
buttonCallbacks: ({
|
||||
accept: button => {
|
||||
button.loading = true
|
||||
py.callClientCoro(
|
||||
chat.userId, "join", [chat.roomId], () => {
|
||||
button.loading = false
|
||||
})
|
||||
},
|
||||
|
||||
decline: button => {
|
||||
button.loading = true
|
||||
py.callClientCoro(
|
||||
chat.userId, "room_leave", [chat.roomId], () => {
|
||||
button.loading = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
39
src/gui/Chat/Banners/LeftBanner.qml
Normal file
39
src/gui/Chat/Banners/LeftBanner.qml
Normal file
@@ -0,0 +1,39 @@
|
||||
import QtQuick 2.12
|
||||
import "../../Base"
|
||||
|
||||
Banner {
|
||||
color: theme.chat.leftBanner.background
|
||||
|
||||
// TODO: avatar func auto
|
||||
avatar.userId: chat.userId
|
||||
avatar.displayName: chat.userInfo.display_name
|
||||
avatar.mxc: chat.userInfo.avatar_url
|
||||
labelText: qsTr("You are not part of this room anymore")
|
||||
|
||||
buttonModel: [
|
||||
{
|
||||
name: "forget",
|
||||
text: qsTr("Forget"),
|
||||
iconName: "room-forget",
|
||||
iconColor: theme.colors.negativeBackground
|
||||
}
|
||||
]
|
||||
|
||||
buttonCallbacks: ({
|
||||
forget: button => {
|
||||
utils.makePopup(
|
||||
"Popups/ForgetRoomPopup.qml",
|
||||
mainUI, // Must not be destroyed with chat
|
||||
{
|
||||
userId: chat.userId,
|
||||
roomId: chat.roomId,
|
||||
roomName: chat.roomInfo.display_name,
|
||||
},
|
||||
obj => {
|
||||
obj.onOk.connect(() => { button.loading = true })
|
||||
},
|
||||
false,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
24
src/gui/Chat/Banners/UnknownDevicesBanner.qml
Normal file
24
src/gui/Chat/Banners/UnknownDevicesBanner.qml
Normal file
@@ -0,0 +1,24 @@
|
||||
import QtQuick 2.12
|
||||
import "../../Base"
|
||||
|
||||
Banner {
|
||||
color: theme.chat.unknownDevices.background
|
||||
|
||||
avatar.visible: false
|
||||
icon.svgName: "unknown-devices-warning"
|
||||
labelText: qsTr("Unknown devices are present in this encrypted room")
|
||||
|
||||
buttonModel: [
|
||||
{
|
||||
name: "inspect",
|
||||
text: qsTr("Inspect"),
|
||||
iconName: "unknown-devices-inspect",
|
||||
}
|
||||
]
|
||||
|
||||
buttonCallbacks: ({
|
||||
inspect: button => {
|
||||
print("show")
|
||||
}
|
||||
})
|
||||
}
|
59
src/gui/Chat/Chat.qml
Normal file
59
src/gui/Chat/Chat.qml
Normal file
@@ -0,0 +1,59 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../Base"
|
||||
import "RoomPane"
|
||||
|
||||
Item {
|
||||
id: chat
|
||||
onFocusChanged: if (focus && loader.item) loader.item.composer.takeFocus()
|
||||
|
||||
|
||||
property string userId: ""
|
||||
property string roomId: ""
|
||||
|
||||
property bool loadingMessages: false
|
||||
property bool ready: userInfo !== "waiting" && roomInfo !== "waiting"
|
||||
|
||||
readonly property var userInfo:
|
||||
utils.getItem(modelSources["Account"] || [], "user_id", userId) ||
|
||||
"waiting"
|
||||
|
||||
readonly property var roomInfo: utils.getItem(
|
||||
modelSources[["Room", userId]] || [], "room_id", roomId
|
||||
) || "waiting"
|
||||
|
||||
readonly property alias loader: loader
|
||||
readonly property alias roomPane: roomPaneLoader.item
|
||||
|
||||
|
||||
HLoader {
|
||||
id: loader
|
||||
anchors.rightMargin: ready ? roomPane.visibleSize : 0
|
||||
anchors.fill: parent
|
||||
visible:
|
||||
ready ? ! roomPane.hidden || anchors.rightMargin < width : true
|
||||
onLoaded: if (chat.focus) item.composer.takeFocus()
|
||||
|
||||
source: ready ? "ChatPage.qml" : ""
|
||||
|
||||
HLoader {
|
||||
anchors.centerIn: parent
|
||||
width: 96 * theme.uiScale
|
||||
height: width
|
||||
|
||||
source: opacity > 0 ? "../Base/HBusyIndicator.qml" : ""
|
||||
opacity: ready ? 0 : 1
|
||||
|
||||
Behavior on opacity { HNumberAnimation { factor: 2 } }
|
||||
}
|
||||
}
|
||||
|
||||
HLoader {
|
||||
id: roomPaneLoader
|
||||
active: ready
|
||||
sourceComponent: RoomPane {
|
||||
id: roomPane
|
||||
referenceSizeParent: chat
|
||||
}
|
||||
}
|
||||
}
|
70
src/gui/Chat/ChatPage.qml
Normal file
70
src/gui/Chat/ChatPage.qml
Normal file
@@ -0,0 +1,70 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../Base"
|
||||
import "Banners"
|
||||
import "Timeline"
|
||||
import "FileTransfer"
|
||||
|
||||
HPage {
|
||||
id: chatPage
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
|
||||
// The target will be our EventList, not the page itself
|
||||
becomeKeyboardFlickableTarget: false
|
||||
|
||||
|
||||
readonly property alias composer: composer
|
||||
|
||||
|
||||
RoomHeader {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
LoadingRoomProgressBar {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
EventList {
|
||||
id: eventList
|
||||
|
||||
// Avoid a certain binding loop
|
||||
Layout.minimumWidth: theme.minimumSupportedWidth
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
TypingMembersBar {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
TransferList {
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: implicitHeight
|
||||
Layout.preferredHeight: implicitHeight * transferCount
|
||||
Layout.maximumHeight: chatPage.height / 6
|
||||
|
||||
Behavior on Layout.preferredHeight { HNumberAnimation {} }
|
||||
}
|
||||
|
||||
InviteBanner {
|
||||
id: inviteBanner
|
||||
visible: ! chat.roomInfo.left && inviterId
|
||||
inviterId: chat.roomInfo.inviter_id
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
LeftBanner {
|
||||
id: leftBanner
|
||||
visible: chat.roomInfo.left
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Composer {
|
||||
id: composer
|
||||
visible: ! chat.roomInfo.left &&
|
||||
! chat.roomInfo.inviter_id
|
||||
}
|
||||
|
||||
}
|
230
src/gui/Chat/Composer.qml
Normal file
230
src/gui/Chat/Composer.qml
Normal file
@@ -0,0 +1,230 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../Base"
|
||||
import "../Dialogs"
|
||||
|
||||
Rectangle {
|
||||
property string indent: " "
|
||||
|
||||
property var aliases: window.settings.writeAliases
|
||||
property string toSend: ""
|
||||
|
||||
property string writingUserId: chat.userId
|
||||
readonly property var writingUserInfo:
|
||||
utils.getItem(modelSources["Account"] || [], "user_id", writingUserId)
|
||||
|
||||
property bool textChangedSinceLostFocus: false
|
||||
|
||||
property alias textArea: areaScrollView.area
|
||||
|
||||
readonly property int cursorPosition:
|
||||
textArea.cursorPosition
|
||||
|
||||
readonly property int cursorY:
|
||||
textArea.text.substring(0, cursorPosition).split("\n").length - 1
|
||||
|
||||
readonly property int cursorX:
|
||||
cursorPosition - lines.slice(0, cursorY).join("").length - cursorY
|
||||
|
||||
readonly property var lines: textArea.text.split("\n")
|
||||
readonly property string lineText: lines[cursorY] || ""
|
||||
|
||||
readonly property string lineTextUntilCursor:
|
||||
lineText.substring(0, cursorX)
|
||||
|
||||
readonly property int deleteCharsOnBackspace:
|
||||
lineTextUntilCursor.match(/^ +$/) ?
|
||||
lineTextUntilCursor.match(/ {1,4}/g).slice(-1)[0].length :
|
||||
1
|
||||
|
||||
function takeFocus() { areaScrollView.forceActiveFocus() }
|
||||
|
||||
// property var pr: lineTextUntilCursor
|
||||
// onPrChanged: print(
|
||||
// "y", cursorY, "x", cursorX,
|
||||
// "ltuc <" + lineTextUntilCursor + ">", "dob",
|
||||
// deleteCharsOnBackspace, "m", lineTextUntilCursor.match(/^ +$/))
|
||||
|
||||
id: composer
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: theme.baseElementsHeight
|
||||
Layout.preferredHeight: areaScrollView.implicitHeight
|
||||
Layout.maximumHeight: pageLoader.height / 2
|
||||
color: theme.chat.composer.background
|
||||
|
||||
HRowLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
HUserAvatar {
|
||||
id: avatar
|
||||
userId: writingUserId
|
||||
displayName: writingUserInfo.display_name
|
||||
mxc: writingUserInfo.avatar_url
|
||||
}
|
||||
|
||||
HScrollableTextArea {
|
||||
id: areaScrollView
|
||||
saveName: "composer"
|
||||
saveId: [chat.roomId, writingUserId]
|
||||
|
||||
enabled: chat.roomInfo.can_send_messages
|
||||
disabledText:
|
||||
qsTr("You do not have permission to post in this room")
|
||||
placeholderText: qsTr("Type a message...")
|
||||
|
||||
backgroundColor: "transparent"
|
||||
area.tabStopDistance: 4 * 4 // 4 spaces
|
||||
area.focus: true
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
|
||||
function setTyping(typing) {
|
||||
py.callClientCoro(
|
||||
writingUserId,
|
||||
"room_typing",
|
||||
[chat.roomId, typing, 5000]
|
||||
)
|
||||
}
|
||||
|
||||
onTextChanged: {
|
||||
if (utils.isEmptyObject(aliases)) {
|
||||
writingUserId = Qt.binding(() => chat.userId)
|
||||
toSend = text
|
||||
setTyping(Boolean(text))
|
||||
textChangedSinceLostFocus = true
|
||||
return
|
||||
}
|
||||
|
||||
let foundAlias = null
|
||||
|
||||
for (let [user, writing_alias] of Object.entries(aliases)) {
|
||||
if (text.startsWith(writing_alias + " ")) {
|
||||
writingUserId = user
|
||||
foundAlias = new RegExp("^" + writing_alias + " ")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (foundAlias) {
|
||||
toSend = text.replace(foundAlias, "")
|
||||
setTyping(Boolean(text))
|
||||
textChangedSinceLostFocus = true
|
||||
return
|
||||
}
|
||||
|
||||
writingUserId = Qt.binding(() => chat.userId)
|
||||
toSend = text
|
||||
|
||||
let vals = Object.values(aliases)
|
||||
|
||||
let longestAlias =
|
||||
vals.reduce((a, b) => a.length > b.length ? a: b)
|
||||
|
||||
let textNotStartsWithAnyAlias =
|
||||
! vals.some(a => a.startsWith(text))
|
||||
|
||||
let textContainsCharNotInAnyAlias =
|
||||
vals.every(a => text.split("").some(c => ! a.includes(c)))
|
||||
|
||||
// Only set typing when it's sure that the user will not use
|
||||
// an alias and has written something
|
||||
if (toSend &&
|
||||
(text.length > longestAlias.length ||
|
||||
textNotStartsWithAnyAlias ||
|
||||
textContainsCharNotInAnyAlias))
|
||||
{
|
||||
setTyping(Boolean(text))
|
||||
textChangedSinceLostFocus = true
|
||||
}
|
||||
}
|
||||
|
||||
area.onEditingFinished: { // when lost focus
|
||||
if (text && textChangedSinceLostFocus) {
|
||||
setTyping(false)
|
||||
textChangedSinceLostFocus = false
|
||||
}
|
||||
}
|
||||
|
||||
area.onSelectedTextChanged: if (area.selectedText) {
|
||||
eventList.selectableLabelContainer.clearSelection()
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
area.Keys.onReturnPressed.connect(ev => {
|
||||
ev.accepted = true
|
||||
|
||||
if (ev.modifiers & Qt.ShiftModifier ||
|
||||
ev.modifiers & Qt.ControlModifier ||
|
||||
ev.modifiers & Qt.AltModifier)
|
||||
{
|
||||
let indents = 0
|
||||
let parts = lineText.split(indent)
|
||||
|
||||
for (const [i, part] of parts.entries()) {
|
||||
if (i === parts.length - 1 || part) { break }
|
||||
indents += 1
|
||||
}
|
||||
|
||||
let add = indent.repeat(indents)
|
||||
textArea.insert(cursorPosition, "\n" + add)
|
||||
return
|
||||
}
|
||||
|
||||
if (textArea.text === "") { return }
|
||||
|
||||
let args = [chat.roomId, toSend]
|
||||
py.callClientCoro(writingUserId, "send_text", args)
|
||||
|
||||
area.clear()
|
||||
})
|
||||
|
||||
area.Keys.onEnterPressed.connect(area.Keys.onReturnPressed)
|
||||
|
||||
area.Keys.onTabPressed.connect(ev => {
|
||||
ev.accepted = true
|
||||
textArea.insert(cursorPosition, indent)
|
||||
})
|
||||
|
||||
area.Keys.onPressed.connect(ev => {
|
||||
if (ev.matches(StandardKey.Copy) &&
|
||||
eventList.selectableLabelContainer.joinedSelection
|
||||
) {
|
||||
ev.accepted = true
|
||||
Clipboard.text =
|
||||
eventList.selectableLabelContainer.joinedSelection
|
||||
return
|
||||
}
|
||||
|
||||
if (ev.modifiers === Qt.NoModifier &&
|
||||
ev.key === Qt.Key_Backspace &&
|
||||
! textArea.selectedText)
|
||||
{
|
||||
ev.accepted = true
|
||||
textArea.remove(
|
||||
cursorPosition - deleteCharsOnBackspace,
|
||||
cursorPosition
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
HButton {
|
||||
enabled: chat.roomInfo.can_send_messages
|
||||
icon.name: "upload-file"
|
||||
backgroundColor: theme.chat.composer.uploadButton.background
|
||||
toolTip.text: qsTr("Send files")
|
||||
onClicked: sendFilePicker.dialog.open()
|
||||
|
||||
Layout.fillHeight: true
|
||||
|
||||
SendFilePicker {
|
||||
id: sendFilePicker
|
||||
userId: chat.userId
|
||||
roomId: chat.roomId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
192
src/gui/Chat/FileTransfer/Transfer.qml
Normal file
192
src/gui/Chat/FileTransfer/Transfer.qml
Normal file
@@ -0,0 +1,192 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../../Base"
|
||||
|
||||
HColumnLayout {
|
||||
id: transfer
|
||||
|
||||
|
||||
property bool paused: false
|
||||
|
||||
property int msLeft: model.time_left || 0
|
||||
property int uploaded: model.uploaded
|
||||
readonly property int speed: model.speed
|
||||
readonly property int totalSize: model.total_size
|
||||
readonly property string status: model.status
|
||||
|
||||
|
||||
function cancel() {
|
||||
// Python might take a sec to cancel, but we want
|
||||
// immediate visual feedback
|
||||
transfer.height = 0
|
||||
// Python will delete this model item on cancel
|
||||
py.call(py.getattr(model.task, "cancel"))
|
||||
}
|
||||
|
||||
function pause() {
|
||||
transfer.paused = ! transfer.paused
|
||||
py.setattr(model.monitor, "pause", transfer.paused)
|
||||
}
|
||||
|
||||
|
||||
Behavior on height { HNumberAnimation {} }
|
||||
|
||||
HRowLayout {
|
||||
HIcon {
|
||||
svgName: "uploading"
|
||||
colorize:
|
||||
transfer.status === "Error" ? theme.colors.negativeBackground :
|
||||
transfer.paused ? theme.colors.middleBackground :
|
||||
theme.icons.colorize
|
||||
|
||||
Layout.preferredWidth: theme.baseElementsHeight
|
||||
}
|
||||
|
||||
HLabel {
|
||||
id: statusLabel
|
||||
elide: expand ? Text.ElideNone : Text.ElideRight
|
||||
wrapMode: expand ? Text.Wrap : Text.NoWrap
|
||||
|
||||
text:
|
||||
status === "Uploading" ? fileName :
|
||||
|
||||
status === "Caching" ?
|
||||
qsTr("Caching %1...").arg(fileName) :
|
||||
|
||||
model.error === "MatrixForbidden" ?
|
||||
qsTr("Forbidden file type or quota exceeded: %1")
|
||||
.arg(fileName) :
|
||||
|
||||
model.error === "MatrixTooLarge" ?
|
||||
qsTr("Too large for this server: %1").arg(fileName) :
|
||||
|
||||
model.error === "IsADirectoryError" ?
|
||||
qsTr("Can't upload folders, need a file: %1").arg(filePath) :
|
||||
|
||||
model.error === "FileNotFoundError" ?
|
||||
qsTr("Non-existant file: %1").arg(filePath) :
|
||||
|
||||
model.error === "PermissionError" ?
|
||||
qsTr("No permission to read this file: %1").arg(filePath) :
|
||||
|
||||
qsTr("Unknown error for %1: %2 - %3")
|
||||
.arg(filePath).arg(model.error).arg(model.error_args)
|
||||
|
||||
topPadding: theme.spacing / 2
|
||||
bottomPadding: topPadding
|
||||
leftPadding: theme.spacing / 1.5
|
||||
rightPadding: leftPadding
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
|
||||
property bool expand: status === "Error"
|
||||
|
||||
readonly property string fileName:
|
||||
model.filepath.split("/").slice(-1)[0]
|
||||
|
||||
readonly property string filePath:
|
||||
model.filepath.replace(/^file:\/\//, "")
|
||||
|
||||
|
||||
HoverHandler { id: statusLabelHover }
|
||||
|
||||
HToolTip {
|
||||
text: parent.truncated ? parent.text : ""
|
||||
visible: text && statusLabelHover.hovered
|
||||
}
|
||||
}
|
||||
|
||||
HSpacer {}
|
||||
|
||||
Repeater {
|
||||
model: [
|
||||
msLeft ? qsTr("-%1").arg(utils.formatDuration(msLeft)) : "",
|
||||
|
||||
speed ? qsTr("%1/s").arg(CppUtils.formattedBytes(speed)) : "",
|
||||
|
||||
qsTr("%1/%2").arg(CppUtils.formattedBytes(uploaded))
|
||||
.arg(CppUtils.formattedBytes(totalSize)),
|
||||
]
|
||||
|
||||
HLabel {
|
||||
text: modelData
|
||||
visible: text && Layout.preferredWidth > 0
|
||||
leftPadding: theme.spacing / 1.5
|
||||
rightPadding: leftPadding
|
||||
|
||||
Layout.preferredWidth:
|
||||
status === "Uploading" ? implicitWidth : 0
|
||||
|
||||
Behavior on Layout.preferredWidth { HNumberAnimation {} }
|
||||
}
|
||||
}
|
||||
|
||||
HButton {
|
||||
visible: Layout.preferredWidth > 0
|
||||
padded: false
|
||||
|
||||
icon.name: transfer.paused ?
|
||||
"upload-resume" : "upload-pause"
|
||||
|
||||
icon.color: transfer.paused ?
|
||||
theme.colors.positiveBackground :
|
||||
theme.colors.middleBackground
|
||||
|
||||
toolTip.text: transfer.paused ?
|
||||
qsTr("Resume") : qsTr("Pause")
|
||||
|
||||
onClicked: transfer.pause()
|
||||
|
||||
Layout.preferredWidth:
|
||||
status === "Uploading" ?
|
||||
theme.baseElementsHeight : 0
|
||||
|
||||
Layout.fillHeight: true
|
||||
|
||||
Behavior on Layout.preferredWidth { HNumberAnimation {} }
|
||||
}
|
||||
|
||||
HButton {
|
||||
icon.name: "upload-cancel"
|
||||
icon.color: theme.colors.negativeBackground
|
||||
padded: false
|
||||
onClicked: transfer.cancel()
|
||||
|
||||
Layout.preferredWidth: theme.baseElementsHeight
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
onTapped: {
|
||||
if (status === "Error") { transfer.cancel() }
|
||||
else { statusLabel.expand = ! statusLabel.expand }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HProgressBar {
|
||||
id: progressBar
|
||||
visible: Layout.maximumHeight !== 0
|
||||
indeterminate: status !== "Uploading"
|
||||
value: uploaded
|
||||
to: totalSize
|
||||
|
||||
// TODO: bake this in hprogressbar
|
||||
foregroundColor:
|
||||
status === "Error" ?
|
||||
theme.controls.progressBar.errorForeground :
|
||||
|
||||
transfer.paused ?
|
||||
theme.controls.progressBar.pausedForeground :
|
||||
|
||||
theme.controls.progressBar.foreground
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumHeight:
|
||||
status === "Error" && indeterminate ? 0 : -1
|
||||
|
||||
Behavior on value { HNumberAnimation { duration: 1200 } }
|
||||
Behavior on Layout.maximumHeight { HNumberAnimation {} }
|
||||
}
|
||||
}
|
33
src/gui/Chat/FileTransfer/TransferList.qml
Normal file
33
src/gui/Chat/FileTransfer/TransferList.qml
Normal file
@@ -0,0 +1,33 @@
|
||||
import QtQuick 2.12
|
||||
import "../../Base"
|
||||
|
||||
Rectangle {
|
||||
implicitWidth: 800
|
||||
implicitHeight: firstDelegate ? firstDelegate.height : 0
|
||||
color: theme.chat.fileTransfer.background
|
||||
opacity: implicitHeight ? 1 : 0
|
||||
clip: true
|
||||
|
||||
|
||||
property int delegateHeight: 0
|
||||
|
||||
readonly property var firstDelegate:
|
||||
transferList.contentItem.visibleChildren[0]
|
||||
|
||||
readonly property alias transferCount: transferList.count
|
||||
|
||||
|
||||
Behavior on implicitHeight { HNumberAnimation {} }
|
||||
|
||||
HListView {
|
||||
id: transferList
|
||||
anchors.fill: parent
|
||||
|
||||
model: HListModel {
|
||||
keyField: "uuid"
|
||||
source: modelSources[["Upload", chat.roomId]] || []
|
||||
}
|
||||
|
||||
delegate: Transfer { width: transferList.width }
|
||||
}
|
||||
}
|
11
src/gui/Chat/LoadingRoomProgressBar.qml
Normal file
11
src/gui/Chat/LoadingRoomProgressBar.qml
Normal file
@@ -0,0 +1,11 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../Base"
|
||||
|
||||
HProgressBar {
|
||||
indeterminate: true
|
||||
height: chat.loadingMessages ? implicitHeight : 0
|
||||
visible: height > 0
|
||||
|
||||
Behavior on height { HNumberAnimation {} }
|
||||
}
|
69
src/gui/Chat/RoomHeader.qml
Normal file
69
src/gui/Chat/RoomHeader.qml
Normal file
@@ -0,0 +1,69 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../Base"
|
||||
|
||||
Rectangle {
|
||||
implicitHeight: theme.baseElementsHeight
|
||||
color: theme.chat.roomHeader.background
|
||||
|
||||
HRowLayout {
|
||||
id: row
|
||||
anchors.fill: parent
|
||||
|
||||
HRoomAvatar {
|
||||
id: avatar
|
||||
displayName: chat.roomInfo.display_name
|
||||
mxc: chat.roomInfo.avatar_url
|
||||
Layout.alignment: Qt.AlignTop
|
||||
}
|
||||
|
||||
HLabel {
|
||||
id: nameLabel
|
||||
text: chat.roomInfo.display_name || qsTr("Empty room")
|
||||
font.pixelSize: theme.fontSize.big
|
||||
color: theme.chat.roomHeader.name
|
||||
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: theme.spacing
|
||||
rightPadding: leftPadding
|
||||
|
||||
Layout.preferredWidth: Math.min(
|
||||
implicitWidth, row.width - row.spacing - avatar.width
|
||||
)
|
||||
Layout.fillHeight: true
|
||||
|
||||
HoverHandler { id: nameHover }
|
||||
}
|
||||
|
||||
HRichLabel {
|
||||
id: topicLabel
|
||||
text: chat.roomInfo.topic
|
||||
textFormat: Text.StyledText
|
||||
font.pixelSize: theme.fontSize.small
|
||||
color: theme.chat.roomHeader.topic
|
||||
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
rightPadding: nameLabel.rightPadding
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
HoverHandler { id: topicHover }
|
||||
}
|
||||
|
||||
HToolTip {
|
||||
visible: text && (nameHover.hovered || topicHover.hovered)
|
||||
label.textFormat: Text.StyledText
|
||||
text: name && topic ? (`${name}<br>${topic}`) : (name || topic)
|
||||
|
||||
readonly property string name:
|
||||
nameLabel.truncated ?
|
||||
(`<b>${chat.roomInfo.display_name}</b>`) : ""
|
||||
|
||||
readonly property string topic:
|
||||
topicLabel.truncated ? chat.roomInfo.topic : ""
|
||||
}
|
||||
}
|
||||
}
|
39
src/gui/Chat/RoomPane/MemberDelegate.qml
Normal file
39
src/gui/Chat/RoomPane/MemberDelegate.qml
Normal file
@@ -0,0 +1,39 @@
|
||||
import QtQuick 2.12
|
||||
import "../../Base"
|
||||
|
||||
HTileDelegate {
|
||||
id: memberDelegate
|
||||
backgroundColor: theme.chat.roomPane.member.background
|
||||
contentOpacity:
|
||||
model.invited ? theme.chat.roomPane.member.invitedOpacity : 1
|
||||
|
||||
image: HUserAvatar {
|
||||
userId: model.user_id
|
||||
displayName: model.display_name
|
||||
mxc: model.avatar_url
|
||||
powerLevel: model.power_level
|
||||
shiftMembershipIconPosition: ! roomPane.collapsed
|
||||
invited: model.invited
|
||||
}
|
||||
|
||||
title.text: model.display_name || model.user_id
|
||||
title.color:
|
||||
memberDelegate.hovered ?
|
||||
utils.nameColor(model.display_name || model.user_id.substring(1)) :
|
||||
theme.chat.roomPane.member.name
|
||||
|
||||
subtitle.text: model.display_name ? model.user_id : ""
|
||||
subtitle.color: theme.chat.roomPane.member.subtitle
|
||||
|
||||
contextMenu: HMenu {
|
||||
HMenuItem {
|
||||
icon.name: "copy-user-id"
|
||||
text: qsTr("Copy user ID")
|
||||
onTriggered: Clipboard.text = model.user_id
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Behavior on title.color { HColorAnimation {} }
|
||||
Behavior on contentOpacity { HNumberAnimation {} }
|
||||
}
|
95
src/gui/Chat/RoomPane/MemberView.qml
Normal file
95
src/gui/Chat/RoomPane/MemberView.qml
Normal file
@@ -0,0 +1,95 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../../Base"
|
||||
|
||||
HColumnLayout {
|
||||
HListView {
|
||||
id: memberList
|
||||
clip: true
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
|
||||
readonly property var originSource:
|
||||
modelSources[["Member", chat.userId, chat.roomId]] || []
|
||||
|
||||
|
||||
onOriginSourceChanged: filterLimiter.restart()
|
||||
|
||||
|
||||
function filterSource() {
|
||||
model.source =
|
||||
utils.filterModelSource(originSource, filterField.text)
|
||||
}
|
||||
|
||||
|
||||
model: HListModel {
|
||||
keyField: "user_id"
|
||||
source: memberList.originSource
|
||||
}
|
||||
|
||||
delegate: MemberDelegate {
|
||||
width: memberList.width
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: filterLimiter
|
||||
interval: 16
|
||||
onTriggered: memberList.filterSource()
|
||||
}
|
||||
}
|
||||
|
||||
HRowLayout {
|
||||
Layout.minimumHeight: theme.baseElementsHeight
|
||||
Layout.maximumHeight: Layout.minimumHeight
|
||||
|
||||
HTextField {
|
||||
id: filterField
|
||||
saveName: "memberFilterField"
|
||||
saveId: chat.roomId
|
||||
|
||||
placeholderText: qsTr("Filter members")
|
||||
backgroundColor: theme.chat.roomPane.filterMembers.background
|
||||
bordered: false
|
||||
opacity: width >= 16 * theme.uiScale ? 1 : 0
|
||||
|
||||
onTextChanged: filterLimiter.restart()
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
Behavior on opacity { HNumberAnimation {} }
|
||||
}
|
||||
|
||||
HButton {
|
||||
id: inviteButton
|
||||
icon.name: "room-send-invite"
|
||||
backgroundColor: theme.chat.roomPane.inviteButton.background
|
||||
enabled: chat.roomInfo.can_invite
|
||||
|
||||
toolTip.text:
|
||||
enabled ?
|
||||
qsTr("Invite members to this room") :
|
||||
qsTr("No permission to invite members in this room")
|
||||
|
||||
topPadding: 0 // XXX
|
||||
bottomPadding: 0
|
||||
|
||||
onClicked: utils.makePopup(
|
||||
"Popups/InviteToRoomPopup.qml",
|
||||
chat,
|
||||
{
|
||||
userId: chat.userId,
|
||||
roomId: chat.roomId,
|
||||
roomName: chat.roomInfo.display_name,
|
||||
invitingAllowed: Qt.binding(() => inviteButton.enabled),
|
||||
},
|
||||
)
|
||||
|
||||
// onEnabledChanged: if (openedPopup && ! enabled)
|
||||
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
79
src/gui/Chat/RoomPane/RoomPane.qml
Normal file
79
src/gui/Chat/RoomPane/RoomPane.qml
Normal file
@@ -0,0 +1,79 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../../Base"
|
||||
|
||||
HDrawer {
|
||||
id: roomPane
|
||||
saveName: "roomPane"
|
||||
|
||||
edge: Qt.RightEdge
|
||||
defaultSize: buttonRepeater.childrenImplicitWidth
|
||||
minimumSize:
|
||||
buttonRepeater.count > 0 ? buttonRepeater.itemAt(0).implicitWidth : 0
|
||||
|
||||
background: HColumnLayout{
|
||||
Rectangle {
|
||||
color: theme.chat.roomPaneButtons.background
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: theme.baseElementsHeight
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
color: theme.chat.roomPane.background
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
|
||||
HColumnLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
HFlow {
|
||||
populate: null
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
HRepeater {
|
||||
id: buttonRepeater
|
||||
model: [
|
||||
"members", "files", "notifications", "history", "settings"
|
||||
]
|
||||
|
||||
HButton {
|
||||
height: theme.baseElementsHeight
|
||||
backgroundColor: "transparent"
|
||||
icon.name: "room-view-" + modelData
|
||||
toolTip.text: qsTr(
|
||||
modelData.charAt(0).toUpperCase() + modelData.slice(1)
|
||||
)
|
||||
|
||||
autoExclusive: true
|
||||
checked: swipeView.currentIndex === 0 && index === 0 ||
|
||||
swipeView.currentIndex === 1 && index === 4
|
||||
|
||||
enabled: ["members", "settings"].includes(modelData)
|
||||
|
||||
onClicked: swipeView.currentIndex = Math.min(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HSwipeView {
|
||||
id: swipeView
|
||||
clip: true
|
||||
interactive: ! roomPane.collapsed
|
||||
|
||||
saveName: "roomPaneView"
|
||||
saveId: chat.roomId
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
MemberView {}
|
||||
SettingsView { fillAvailableHeight: true }
|
||||
}
|
||||
}
|
||||
}
|
132
src/gui/Chat/RoomPane/SettingsView.qml
Normal file
132
src/gui/Chat/RoomPane/SettingsView.qml
Normal file
@@ -0,0 +1,132 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../../Base"
|
||||
|
||||
HBox {
|
||||
color: "transparent"
|
||||
|
||||
buttonModel: [
|
||||
{
|
||||
name: "apply",
|
||||
text: qsTr("Save"),
|
||||
iconName: "apply",
|
||||
// enabled: anyChange, TODO
|
||||
enabled: false,
|
||||
loading: saveFuture !== null,
|
||||
disableWhileLoading: false,
|
||||
},
|
||||
{
|
||||
name: "cancel",
|
||||
text: qsTr("Cancel"),
|
||||
iconName: "cancel",
|
||||
enabled: anyChange || saveFuture !== null,
|
||||
},
|
||||
]
|
||||
|
||||
buttonCallbacks: ({
|
||||
apply: button => {
|
||||
if (saveFuture) saveFuture.cancel()
|
||||
// TODO
|
||||
},
|
||||
|
||||
cancel: button => {
|
||||
if (saveFuture) {
|
||||
saveFuture.cancel()
|
||||
saveFuture = null
|
||||
}
|
||||
|
||||
nameField.reset()
|
||||
topicField.reset()
|
||||
encryptCheckBox.reset()
|
||||
requireInviteCheckbox.reset()
|
||||
forbidGuestsCheckBox.reset()
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
property var saveFuture: null
|
||||
|
||||
readonly property bool anyChange:
|
||||
nameField.changed || topicField.changed || encryptCheckBox.changed ||
|
||||
requireInviteCheckbox.changed || forbidGuestsCheckBox.changed
|
||||
|
||||
|
||||
HRoomAvatar {
|
||||
id: avatar
|
||||
displayName: chat.roomInfo.display_name
|
||||
mxc: chat.roomInfo.avatar_url
|
||||
// enabled: chat.roomInfo.can_set_avatar # put this in "change avatar"
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: width
|
||||
Layout.maximumWidth: 256 * theme.uiScale
|
||||
}
|
||||
|
||||
HTextField {
|
||||
id: nameField
|
||||
placeholderText: qsTr("Room name")
|
||||
maximumLength: 255
|
||||
defaultText: chat.roomInfo.given_name
|
||||
enabled: chat.roomInfo.can_set_name
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
HScrollableTextArea {
|
||||
id: topicField
|
||||
placeholderText: qsTr("Room topic")
|
||||
defaultText: chat.roomInfo.plain_topic
|
||||
enabled: chat.roomInfo.can_set_topic
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
HCheckBox {
|
||||
id: encryptCheckBox
|
||||
text: qsTr("Encrypt messages")
|
||||
subtitle.text:
|
||||
qsTr("Only you and those you trust will be able to read the " +
|
||||
"conversation") +
|
||||
`<br><font color="${theme.colors.middleBackground}">` +
|
||||
(
|
||||
chat.roomInfo.encrypted ?
|
||||
qsTr("Cannot be disabled") :
|
||||
qsTr("Cannot be disabled later!")
|
||||
) +
|
||||
"</font>"
|
||||
subtitle.textFormat: Text.StyledText
|
||||
defaultChecked: chat.roomInfo.encrypted
|
||||
enabled: chat.roomInfo.can_set_encryption && ! chat.roomInfo.encrypted
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
HCheckBox {
|
||||
id: requireInviteCheckbox
|
||||
text: qsTr("Require being invited")
|
||||
subtitle.text: qsTr("Users will need an invite to join the room")
|
||||
defaultChecked: chat.roomInfo.invite_required
|
||||
enabled: chat.roomInfo.can_set_join_rules
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
HCheckBox {
|
||||
id: forbidGuestsCheckBox
|
||||
text: qsTr("Forbid guests")
|
||||
subtitle.text: qsTr("Users without an account won't be able to join")
|
||||
defaultChecked: ! chat.roomInfo.guests_allowed
|
||||
enabled: chat.roomInfo.can_set_guest_access
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// HCheckBox { TODO
|
||||
// text: qsTr("Make this room visible in the public room directory")
|
||||
// checked: chat.roomInfo.published_in_directory
|
||||
|
||||
// Layout.fillWidth: true
|
||||
// }
|
||||
|
||||
HSpacer {}
|
||||
}
|
9
src/gui/Chat/Timeline/Daybreak.qml
Normal file
9
src/gui/Chat/Timeline/Daybreak.qml
Normal file
@@ -0,0 +1,9 @@
|
||||
import QtQuick 2.12
|
||||
import "../../Base"
|
||||
|
||||
HNoticePage {
|
||||
text: model.date.toLocaleDateString()
|
||||
color: theme.chat.daybreak.text
|
||||
backgroundColor: theme.chat.daybreak.background
|
||||
radius: theme.chat.daybreak.radius
|
||||
}
|
15
src/gui/Chat/Timeline/EventAudio.qml
Normal file
15
src/gui/Chat/Timeline/EventAudio.qml
Normal file
@@ -0,0 +1,15 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import QtAV 1.7
|
||||
import "../../Base"
|
||||
import "../../Base/MediaPlayer"
|
||||
|
||||
AudioPlayer {
|
||||
id: audio
|
||||
|
||||
HoverHandler {
|
||||
onHoveredChanged:
|
||||
eventDelegate.hoveredMediaTypeUrl =
|
||||
hovered ? [EventDelegate.Media.Audio, audio.source] : []
|
||||
}
|
||||
}
|
182
src/gui/Chat/Timeline/EventContent.qml
Normal file
182
src/gui/Chat/Timeline/EventContent.qml
Normal file
@@ -0,0 +1,182 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../../Base"
|
||||
|
||||
HRowLayout {
|
||||
id: eventContent
|
||||
spacing: theme.spacing / 1.25
|
||||
layoutDirection: onRight ? Qt.RightToLeft: Qt.LeftToRight
|
||||
|
||||
|
||||
readonly property string senderText:
|
||||
hideNameLine ? "" : (
|
||||
"<div class='sender'>" +
|
||||
utils.coloredNameHtml(model.sender_name, model.sender_id) +
|
||||
"</div>"
|
||||
)
|
||||
readonly property string contentText: utils.processedEventText(model)
|
||||
readonly property string timeText: utils.formatTime(model.date, false)
|
||||
readonly property string localEchoText:
|
||||
model.is_local_echo ?
|
||||
` <font size=${theme.fontSize.small}px>⏳</font>` :
|
||||
""
|
||||
|
||||
readonly property bool pureMedia: ! contentText && linksRepeater.count
|
||||
|
||||
readonly property string hoveredLink: contentLabel.hoveredLink
|
||||
readonly property bool hoveredSelectable: contentHover.hovered
|
||||
|
||||
readonly property int xOffset:
|
||||
onRight ?
|
||||
contentLabel.width - contentLabel.paintedWidth -
|
||||
contentLabel.leftPadding - contentLabel.rightPadding :
|
||||
0
|
||||
|
||||
// 600px max with a 16px font
|
||||
readonly property int maxMessageWidth: theme.fontSize.normal * 0.5 * 75
|
||||
|
||||
|
||||
TapHandler {
|
||||
enabled: debugMode
|
||||
onDoubleTapped:
|
||||
utils.debug(eventContent, null, con => { con.runJS("json()") })
|
||||
}
|
||||
|
||||
Item {
|
||||
id: avatarWrapper
|
||||
opacity: collapseAvatar ? 0 : 1
|
||||
visible: ! hideAvatar
|
||||
|
||||
Layout.minimumWidth: theme.chat.message.avatarSize
|
||||
Layout.minimumHeight:
|
||||
collapseAvatar ? 1 :
|
||||
smallAvatar ? theme.chat.message.collapsedAvatarSize :
|
||||
Layout.minimumWidth
|
||||
|
||||
Layout.maximumWidth: Layout.minimumWidth
|
||||
Layout.maximumHeight: Layout.minimumHeight
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
HUserAvatar {
|
||||
id: avatar
|
||||
userId: model.sender_id
|
||||
displayName: model.sender_name
|
||||
mxc: model.sender_avatar
|
||||
width: parent.width
|
||||
height: collapseAvatar ? 1 : theme.chat.message.avatarSize
|
||||
}
|
||||
}
|
||||
|
||||
HColumnLayout {
|
||||
id: contentColumn
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
HSelectableLabel {
|
||||
id: contentLabel
|
||||
container: selectableLabelContainer
|
||||
index: model.index
|
||||
visible: ! pureMedia
|
||||
|
||||
topPadding: theme.spacing / 1.75
|
||||
bottomPadding: topPadding
|
||||
leftPadding: eventContent.spacing
|
||||
rightPadding: leftPadding
|
||||
|
||||
color: model.event_type === "RoomMessageNotice" ?
|
||||
theme.chat.message.noticeBody :
|
||||
theme.chat.message.body
|
||||
|
||||
font.italic: model.event_type === "RoomMessageEmote"
|
||||
wrapMode: TextEdit.Wrap
|
||||
textFormat: Text.RichText
|
||||
text:
|
||||
// CSS
|
||||
theme.chat.message.styleInclude +
|
||||
|
||||
// Sender name
|
||||
eventContent.senderText +
|
||||
|
||||
// Message body
|
||||
eventContent.contentText +
|
||||
|
||||
// Time
|
||||
// For some reason, if there's only one space,
|
||||
// times will be on their own lines most of the time.
|
||||
" " +
|
||||
`<font size=${theme.fontSize.small}px ` +
|
||||
`color=${theme.chat.message.date}>` +
|
||||
timeText +
|
||||
"</font>" +
|
||||
|
||||
// Local echo icon
|
||||
(model.is_local_echo ?
|
||||
` <font size=${theme.fontSize.small}px>⏳</font>` : "")
|
||||
|
||||
transform: Translate { x: xOffset }
|
||||
|
||||
Layout.maximumWidth: eventContent.maxMessageWidth
|
||||
Layout.fillWidth: true
|
||||
|
||||
function selectAllText() {
|
||||
// Select the message body without the date or name
|
||||
container.clearSelection()
|
||||
contentLabel.select(
|
||||
0,
|
||||
contentLabel.length -
|
||||
timeText.length - 1 // - 1: separating space
|
||||
)
|
||||
contentLabel.updateContainerSelectedTexts()
|
||||
}
|
||||
|
||||
HoverHandler { id: contentHover }
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(
|
||||
parent.paintedWidth +
|
||||
parent.leftPadding + parent.rightPadding,
|
||||
|
||||
linksRepeater.childrenWidth +
|
||||
(pureMedia ? 0 : parent.leftPadding + parent.rightPadding),
|
||||
)
|
||||
height: contentColumn.height
|
||||
z: -1
|
||||
color: isOwn?
|
||||
theme.chat.message.ownBackground :
|
||||
theme.chat.message.background
|
||||
|
||||
Rectangle {
|
||||
visible: model.event_type === "RoomMessageNotice"
|
||||
width: theme.chat.message.noticeLineWidth
|
||||
height: parent.height
|
||||
color: utils.nameColor(
|
||||
model.sender_name || model.sender_id.substring(1),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HRepeater {
|
||||
id: linksRepeater
|
||||
model: eventDelegate.currentModel.links
|
||||
|
||||
EventMediaLoader {
|
||||
singleMediaInfo: eventDelegate.currentModel
|
||||
mediaUrl: modelData
|
||||
showSender: pureMedia ? senderText : ""
|
||||
showDate: pureMedia ? timeText : ""
|
||||
showLocalEcho: pureMedia ? localEchoText : ""
|
||||
|
||||
transform: Translate { x: xOffset }
|
||||
|
||||
Layout.bottomMargin: pureMedia ? 0 : contentLabel.bottomPadding
|
||||
Layout.leftMargin: pureMedia ? 0 : contentLabel.leftPadding
|
||||
Layout.rightMargin: pureMedia ? 0 : contentLabel.rightPadding
|
||||
|
||||
Layout.preferredWidth: item ? item.width : -1
|
||||
Layout.preferredHeight: item ? item.height : -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HSpacer {}
|
||||
}
|
181
src/gui/Chat/Timeline/EventDelegate.qml
Normal file
181
src/gui/Chat/Timeline/EventDelegate.qml
Normal file
@@ -0,0 +1,181 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../../Base"
|
||||
|
||||
HColumnLayout {
|
||||
id: eventDelegate
|
||||
width: eventList.width
|
||||
|
||||
|
||||
enum Media { Page, File, Image, Video, Audio }
|
||||
|
||||
property var hoveredMediaTypeUrl: []
|
||||
|
||||
// Remember timeline goes from newest message at index 0 to oldest
|
||||
readonly property var previousModel: eventList.model.get(model.index + 1)
|
||||
readonly property var nextModel: eventList.model.get(model.index - 1)
|
||||
readonly property QtObject currentModel: model
|
||||
|
||||
property bool isOwn: chat.userId === model.sender_id
|
||||
property bool onRight: eventList.ownEventsOnRight && isOwn
|
||||
property bool combine: eventList.canCombine(previousModel, model)
|
||||
property bool talkBreak: eventList.canTalkBreak(previousModel, model)
|
||||
property bool dayBreak: eventList.canDayBreak(previousModel, model)
|
||||
|
||||
readonly property bool smallAvatar:
|
||||
eventList.canCombine(model, nextModel) &&
|
||||
(model.event_type === "RoomMessageEmote" ||
|
||||
! (model.event_type.startsWith("RoomMessage") ||
|
||||
model.event_type.startsWith("RoomEncrypted")))
|
||||
|
||||
readonly property bool collapseAvatar: combine
|
||||
readonly property bool hideAvatar: onRight
|
||||
|
||||
readonly property bool hideNameLine:
|
||||
model.event_type === "RoomMessageEmote" ||
|
||||
! (
|
||||
model.event_type.startsWith("RoomMessage") ||
|
||||
model.event_type.startsWith("RoomEncrypted")
|
||||
) ||
|
||||
onRight ||
|
||||
combine
|
||||
|
||||
readonly property int cursorShape:
|
||||
eventContent.hoveredLink || hoveredMediaTypeUrl.length > 0 ?
|
||||
Qt.PointingHandCursor :
|
||||
|
||||
eventContent.hoveredSelectable ? Qt.IBeamCursor :
|
||||
|
||||
Qt.ArrowCursor
|
||||
|
||||
readonly property int separationSpacing:
|
||||
dayBreak ? theme.spacing * 4 :
|
||||
talkBreak ? theme.spacing * 6 :
|
||||
combine ? theme.spacing / 2 :
|
||||
theme.spacing * 2
|
||||
|
||||
// Needed because of eventList's MouseArea which steals the
|
||||
// HSelectableLabel's MouseArea hover events
|
||||
onCursorShapeChanged: eventList.cursorShape = cursorShape
|
||||
|
||||
|
||||
function json() {
|
||||
return JSON.stringify(
|
||||
{
|
||||
"model": utils.getItem(
|
||||
modelSources[[
|
||||
"Event", chat.userId, chat.roomId
|
||||
]],
|
||||
"client_id",
|
||||
model.client_id
|
||||
),
|
||||
"source": py.getattr(model.source, "__dict__"),
|
||||
},
|
||||
null, 4)
|
||||
}
|
||||
|
||||
function openContextMenu() {
|
||||
contextMenu.media = eventDelegate.hoveredMediaTypeUrl
|
||||
contextMenu.link = eventContent.hoveredLink
|
||||
contextMenu.popup()
|
||||
}
|
||||
|
||||
|
||||
Item {
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight:
|
||||
model.event_type === "RoomCreateEvent" ? 0 : separationSpacing
|
||||
}
|
||||
|
||||
Daybreak {
|
||||
visible: dayBreak
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumWidth: parent.width
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: dayBreak
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: separationSpacing
|
||||
}
|
||||
|
||||
EventContent {
|
||||
id: eventContent
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
Behavior on x { HNumberAnimation {} }
|
||||
}
|
||||
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
onTapped: openContextMenu()
|
||||
}
|
||||
|
||||
HMenu {
|
||||
id: contextMenu
|
||||
|
||||
property var media: []
|
||||
property string link: ""
|
||||
|
||||
onClosed: { media = []; link = "" }
|
||||
|
||||
HMenuItem {
|
||||
id: copyMedia
|
||||
icon.name: "copy-link"
|
||||
text:
|
||||
contextMenu.media.length < 1 ? "" :
|
||||
|
||||
contextMenu.media[0] === EventDelegate.Media.Page ?
|
||||
qsTr("Copy page address") :
|
||||
|
||||
contextMenu.media[0] === EventDelegate.Media.File ?
|
||||
qsTr("Copy file address") :
|
||||
|
||||
contextMenu.media[0] === EventDelegate.Media.Image ?
|
||||
qsTr("Copy image address") :
|
||||
|
||||
contextMenu.media[0] === EventDelegate.Media.Video ?
|
||||
qsTr("Copy video address") :
|
||||
|
||||
contextMenu.media[0] === EventDelegate.Media.Audio ?
|
||||
qsTr("Copy audio address") :
|
||||
|
||||
qsTr("Copy media address")
|
||||
|
||||
visible: Boolean(text)
|
||||
onTriggered: Clipboard.text = contextMenu.media[1]
|
||||
}
|
||||
|
||||
HMenuItem {
|
||||
id: copyLink
|
||||
icon.name: "copy-link"
|
||||
text: qsTr("Copy link address")
|
||||
visible: Boolean(contextMenu.link)
|
||||
onTriggered: Clipboard.text = contextMenu.link
|
||||
}
|
||||
|
||||
HMenuItem {
|
||||
icon.name: "copy-text"
|
||||
text: qsTr("Copy text")
|
||||
visible: enabled || (! copyLink.visible && ! copyMedia.visible)
|
||||
enabled: Boolean(selectableLabelContainer.joinedSelection)
|
||||
onTriggered:
|
||||
Clipboard.text = selectableLabelContainer.joinedSelection
|
||||
}
|
||||
|
||||
HMenuItem {
|
||||
icon.name: "clear-messages"
|
||||
text: qsTr("Clear messages")
|
||||
onTriggered: utils.makePopup(
|
||||
"Popups/ClearMessagesPopup.qml",
|
||||
chat,
|
||||
{userId: chat.userId, roomId: chat.roomId},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
42
src/gui/Chat/Timeline/EventFile.qml
Normal file
42
src/gui/Chat/Timeline/EventFile.qml
Normal file
@@ -0,0 +1,42 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../../Base"
|
||||
|
||||
HTile {
|
||||
id: file
|
||||
width: Math.min(
|
||||
eventDelegate.width,
|
||||
eventContent.maxMessageWidth,
|
||||
Math.max(theme.chat.message.fileMinWidth, implicitWidth),
|
||||
)
|
||||
height: Math.max(theme.chat.message.avatarSize, implicitHeight)
|
||||
|
||||
title.text: loader.singleMediaInfo.media_title || qsTr("Untitled file")
|
||||
title.elide: Text.ElideMiddle
|
||||
subtitle.text: CppUtils.formattedBytes(loader.singleMediaInfo.media_size)
|
||||
|
||||
image: HIcon {
|
||||
svgName: "download"
|
||||
}
|
||||
|
||||
onLeftClicked: download(Qt.openUrlExternally)
|
||||
onRightClicked: eventDelegate.openContextMenu()
|
||||
|
||||
onHoveredChanged: {
|
||||
if (! hovered) {
|
||||
eventDelegate.hoveredMediaTypeUrl = []
|
||||
return
|
||||
}
|
||||
|
||||
eventDelegate.hoveredMediaTypeUrl = [
|
||||
EventDelegate.Media.File,
|
||||
loader.downloadedPath.replace(/^file:\/\//, "") || loader.mediaUrl
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
property EventMediaLoader loader
|
||||
|
||||
readonly property bool cryptDict: loader.singleMediaInfo.media_crypt_dict
|
||||
readonly property bool isEncrypted: ! utils.isEmptyObject(cryptDict)
|
||||
}
|
119
src/gui/Chat/Timeline/EventImage.qml
Normal file
119
src/gui/Chat/Timeline/EventImage.qml
Normal file
@@ -0,0 +1,119 @@
|
||||
import QtQuick 2.12
|
||||
import "../../Base"
|
||||
|
||||
HMxcImage {
|
||||
id: image
|
||||
width: fitSize.width
|
||||
height: fitSize.height
|
||||
horizontalAlignment: Image.AlignLeft
|
||||
|
||||
animated: loader.singleMediaInfo.media_mime === "image/gif" ||
|
||||
utils.urlExtension(loader.mediaUrl) === "gif"
|
||||
thumbnail: ! animated && loader.thumbnailMxc
|
||||
mxc: thumbnail ?
|
||||
(loader.thumbnailMxc || loader.mediaUrl) :
|
||||
(loader.mediaUrl || loader.thumbnailMxc)
|
||||
cryptDict: thumbnail && loader.thumbnailMxc ?
|
||||
loader.singleMediaInfo.thumbnail_crypt_dict :
|
||||
loader.singleMediaInfo.media_crypt_dict
|
||||
|
||||
|
||||
property EventMediaLoader loader
|
||||
|
||||
readonly property bool isEncrypted: ! utils.isEmptyObject(cryptDict)
|
||||
|
||||
readonly property real maxHeight:
|
||||
theme.chat.message.thumbnailMaxHeightRatio
|
||||
|
||||
readonly property size fitSize: utils.fitSize(
|
||||
// Minimum display size
|
||||
theme.chat.message.thumbnailMinSize.width,
|
||||
theme.chat.message.thumbnailMinSize.height,
|
||||
|
||||
// Real size
|
||||
(
|
||||
loader.singleMediaInfo.thumbnail_width ||
|
||||
loader.singleMediaInfo.media_width ||
|
||||
implicitWidth ||
|
||||
800
|
||||
) * theme.uiScale,
|
||||
|
||||
(
|
||||
loader.singleMediaInfo.thumbnail_height ||
|
||||
loader.singleMediaInfo.media_height ||
|
||||
implicitHeight ||
|
||||
600
|
||||
) * theme.uiScale,
|
||||
|
||||
// Maximum display size
|
||||
Math.min(
|
||||
eventList.height * maxHeight,
|
||||
eventContent.maxMessageWidth * Math.min(1, theme.uiScale), // XXX
|
||||
),
|
||||
eventList.height * maxHeight,
|
||||
)
|
||||
|
||||
|
||||
function getOpenUrl(callback) {
|
||||
if (image.isEncrypted && loader.mediaUrl) {
|
||||
loader.download(callback)
|
||||
return
|
||||
}
|
||||
|
||||
if (image.isEncrypted) {
|
||||
callback(image.cachedPath)
|
||||
return
|
||||
}
|
||||
|
||||
let toOpen = loader.mediaUrl || loader.thumbnailMxc
|
||||
let isMxc = toOpen.startsWith("mxc://")
|
||||
|
||||
isMxc ?
|
||||
py.callClientCoro(chat.userId, "mxc_to_http", [toOpen], callback) :
|
||||
callback(toOpen)
|
||||
}
|
||||
|
||||
|
||||
TapHandler {
|
||||
onTapped: if (! image.animated) getOpenUrl(Qt.openUrlExternally)
|
||||
onDoubleTapped: getOpenUrl(Qt.openUrlExternally)
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: hover
|
||||
onHoveredChanged: {
|
||||
if (! hovered) {
|
||||
eventDelegate.hoveredMediaTypeUrl = []
|
||||
return
|
||||
}
|
||||
|
||||
eventDelegate.hoveredMediaTypeUrl = [
|
||||
EventDelegate.Media.Image,
|
||||
loader.downloadedPath.replace(/^file:\/\//, "") ||
|
||||
loader.mediaUrl
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
EventImageTextBubble {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
text: loader.showSender
|
||||
textFormat: Text.StyledText
|
||||
opacity: hover.hovered ? 0 : 1
|
||||
visible: opacity > 0
|
||||
|
||||
Behavior on opacity { HNumberAnimation {} }
|
||||
}
|
||||
|
||||
EventImageTextBubble {
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
text: [loader.showDate, loader.showLocalEcho].join(" ").trim()
|
||||
textFormat: Text.StyledText
|
||||
opacity: hover.hovered ? 0 : 1
|
||||
visible: opacity > 0
|
||||
|
||||
Behavior on opacity { HNumberAnimation {} }
|
||||
}
|
||||
}
|
24
src/gui/Chat/Timeline/EventImageTextBubble.qml
Normal file
24
src/gui/Chat/Timeline/EventImageTextBubble.qml
Normal file
@@ -0,0 +1,24 @@
|
||||
import QtQuick 2.12
|
||||
import "../../Base"
|
||||
|
||||
HLabel {
|
||||
id: bubble
|
||||
anchors.margins: theme.spacing / 4
|
||||
|
||||
topPadding: theme.spacing / 2
|
||||
bottomPadding: topPadding
|
||||
leftPadding: theme.spacing / 1.5
|
||||
rightPadding: leftPadding
|
||||
|
||||
font.pixelSize: theme.fontSize.small
|
||||
|
||||
background: Rectangle {
|
||||
color: Qt.hsla(0, 0, 0, 0.7)
|
||||
radius: theme.radius
|
||||
}
|
||||
|
||||
Binding on visible {
|
||||
value: false
|
||||
when: ! Boolean(bubble.text)
|
||||
}
|
||||
}
|
173
src/gui/Chat/Timeline/EventList.qml
Normal file
173
src/gui/Chat/Timeline/EventList.qml
Normal file
@@ -0,0 +1,173 @@
|
||||
import QtQuick 2.12
|
||||
import "../../Base"
|
||||
|
||||
Rectangle {
|
||||
property alias selectableLabelContainer: selectableLabelContainer
|
||||
property alias eventList: eventList
|
||||
|
||||
color: theme.chat.eventList.background
|
||||
|
||||
HSelectableLabelContainer {
|
||||
id: selectableLabelContainer
|
||||
anchors.fill: parent
|
||||
reversed: eventList.verticalLayoutDirection === ListView.BottomToTop
|
||||
|
||||
DragHandler {
|
||||
target: null
|
||||
onActiveChanged: if (! active) dragFlicker.speed = 0
|
||||
onCentroidChanged: {
|
||||
let left = centroid.pressedButtons & Qt.LeftButton
|
||||
let vel = centroid.velocity.y
|
||||
let pos = centroid.position.y
|
||||
let dist = Math.min(selectableLabelContainer.height / 4, 50)
|
||||
let boost = 20 * (pos < dist ? -pos : -(height - pos))
|
||||
|
||||
dragFlicker.speed =
|
||||
left && vel && pos < dist ? 1000 + boost :
|
||||
left && vel && pos > height - dist ? -1000 + -boost :
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: dragFlicker
|
||||
interval: 100
|
||||
running: speed !== 0
|
||||
repeat: true
|
||||
|
||||
onTriggered: {
|
||||
if (eventList.verticalOvershoot !== 0) return
|
||||
if (speed < 0 && eventList.atYEnd) return
|
||||
if (eventList.atYBeggining) {
|
||||
if (bouncedStart) { return } else { bouncedStart = true }
|
||||
}
|
||||
|
||||
eventList.flick(0, speed * acceleration)
|
||||
acceleration = Math.min(8, acceleration * 1.05)
|
||||
}
|
||||
onRunningChanged: if (! running) {
|
||||
acceleration = 1.0
|
||||
bouncedStart = false
|
||||
eventList.cancelFlick()
|
||||
eventList.returnToBounds()
|
||||
}
|
||||
|
||||
property real speed: 0.0
|
||||
property real acceleration: 1.0
|
||||
property bool bouncedStart: false
|
||||
}
|
||||
|
||||
HListView {
|
||||
id: eventList
|
||||
clip: true
|
||||
allowDragging: false
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: theme.spacing
|
||||
anchors.rightMargin: theme.spacing
|
||||
|
||||
topMargin: theme.spacing
|
||||
bottomMargin: theme.spacing
|
||||
verticalLayoutDirection: ListView.BottomToTop
|
||||
|
||||
// Keep x scroll pages cached, to limit images having to be
|
||||
// reloaded from network.
|
||||
cacheBuffer: height * 2
|
||||
|
||||
onYPosChanged:
|
||||
if (canLoad && yPos < 0.1) Qt.callLater(loadPastEvents)
|
||||
|
||||
// When an invited room becomes joined, we should now be able to
|
||||
// fetch past events.
|
||||
onInviterChanged: canLoad = true
|
||||
|
||||
Component.onCompleted: shortcuts.flickTarget = eventList
|
||||
|
||||
|
||||
property string inviter: chat.roomInfo.inviter || ""
|
||||
property real yPos: visibleArea.yPosition
|
||||
property bool canLoad: true
|
||||
|
||||
property bool ownEventsOnRight:
|
||||
width < theme.chat.eventList.ownEventsOnRightUnderWidth
|
||||
|
||||
|
||||
function canCombine(item, itemAfter) {
|
||||
if (! item || ! itemAfter) return false
|
||||
|
||||
return Boolean(
|
||||
! canTalkBreak(item, itemAfter) &&
|
||||
! canDayBreak(item, itemAfter) &&
|
||||
item.sender_id === itemAfter.sender_id &&
|
||||
utils.minutesBetween(item.date, itemAfter.date) <= 5
|
||||
)
|
||||
}
|
||||
|
||||
function canTalkBreak(item, itemAfter) {
|
||||
if (! item || ! itemAfter) return false
|
||||
|
||||
return Boolean(
|
||||
! canDayBreak(item, itemAfter) &&
|
||||
utils.minutesBetween(item.date, itemAfter.date) >= 20
|
||||
)
|
||||
}
|
||||
|
||||
function canDayBreak(item, itemAfter) {
|
||||
if (itemAfter && itemAfter.event_type === "RoomCreateEvent")
|
||||
return true
|
||||
|
||||
if (! item || ! itemAfter || ! item.date || ! itemAfter.date)
|
||||
return false
|
||||
|
||||
return item.date.getDate() !== itemAfter.date.getDate()
|
||||
}
|
||||
|
||||
function loadPastEvents() {
|
||||
// try/catch blocks to hide pyotherside error when the
|
||||
// component is destroyed but func is still running
|
||||
|
||||
try {
|
||||
eventList.canLoad = false
|
||||
chat.loadingMessages = true
|
||||
|
||||
py.callClientCoro(
|
||||
chat.userId, "load_past_events", [chat.roomId],
|
||||
moreToLoad => {
|
||||
try {
|
||||
eventList.canLoad = moreToLoad
|
||||
|
||||
// Call yPosChanged() to run this func again
|
||||
// if the loaded messages aren't enough to fill
|
||||
// the screen.
|
||||
if (moreToLoad) yPosChanged()
|
||||
|
||||
chat.loadingMessages = false
|
||||
} catch (err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
)
|
||||
} catch (err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
model: HListModel {
|
||||
keyField: "client_id"
|
||||
source: modelSources[[
|
||||
"Event", chat.userId, chat.roomId
|
||||
]] || []
|
||||
}
|
||||
|
||||
delegate: EventDelegate {}
|
||||
}
|
||||
}
|
||||
|
||||
HNoticePage {
|
||||
text: qsTr("No messages to show yet")
|
||||
|
||||
visible: eventList.model.count < 1
|
||||
anchors.fill: parent
|
||||
}
|
||||
}
|
89
src/gui/Chat/Timeline/EventMediaLoader.qml
Normal file
89
src/gui/Chat/Timeline/EventMediaLoader.qml
Normal file
@@ -0,0 +1,89 @@
|
||||
import QtQuick 2.12
|
||||
import "../../Base"
|
||||
|
||||
HLoader {
|
||||
id: loader
|
||||
x: eventContent.spacing
|
||||
|
||||
onTypeChanged: {
|
||||
if (type === EventDelegate.Media.Image) {
|
||||
var file = "EventImage.qml"
|
||||
|
||||
} else if (type !== EventDelegate.Media.Page) {
|
||||
var file = "EventFile.qml"
|
||||
|
||||
} else { return }
|
||||
|
||||
loader.setSource(file, {loader})
|
||||
}
|
||||
|
||||
|
||||
property QtObject singleMediaInfo
|
||||
property string mediaUrl
|
||||
property string showSender: ""
|
||||
property string showDate: ""
|
||||
property string showLocalEcho: ""
|
||||
|
||||
property string downloadedPath: ""
|
||||
|
||||
readonly property var imageExtensions: [
|
||||
"bmp", "gif", "jpg", "jpeg", "png", "pbm", "pgm", "ppm", "xbm", "xpm",
|
||||
"tiff", "webp", "svg",
|
||||
]
|
||||
|
||||
readonly property var videoExtensions: [
|
||||
"3gp", "avi", "flv", "m4p", "m4v", "mkv", "mov", "mp4",
|
||||
"mpeg", "mpg", "ogv", "qt", "vob", "webm", "wmv", "yuv",
|
||||
]
|
||||
|
||||
readonly property var audioExtensions: [
|
||||
"pcm", "wav", "raw", "aiff", "flac", "m4a", "tta", "aac", "mp3",
|
||||
"ogg", "oga", "opus",
|
||||
]
|
||||
|
||||
readonly property int type: {
|
||||
if (singleMediaInfo.event_type === "RoomAvatarEvent")
|
||||
return EventDelegate.Media.Image
|
||||
|
||||
let mainType = singleMediaInfo.media_mime.split("/")[0].toLowerCase()
|
||||
|
||||
if (mainType === "image") return EventDelegate.Media.Image
|
||||
if (mainType === "video") return EventDelegate.Media.Video
|
||||
if (mainType === "audio") return EventDelegate.Media.Audio
|
||||
|
||||
let fileEvents = ["RoomMessageFile", "RoomEncryptedFile"]
|
||||
|
||||
if (fileEvents.includes(singleMediaInfo.event_type))
|
||||
return EventDelegate.Media.File
|
||||
|
||||
// If this is a preview for a link in a normal message
|
||||
let ext = utils.urlExtension(mediaUrl)
|
||||
|
||||
if (imageExtensions.includes(ext)) return EventDelegate.Media.Image
|
||||
if (videoExtensions.includes(ext)) return EventDelegate.Media.Video
|
||||
if (audioExtensions.includes(ext)) return EventDelegate.Media.Audio
|
||||
|
||||
return EventDelegate.Media.Page
|
||||
}
|
||||
|
||||
readonly property string thumbnailMxc: singleMediaInfo.thumbnail_url
|
||||
|
||||
|
||||
function download(callback) {
|
||||
if (! loader.mediaUrl.startsWith("mxc://")) {
|
||||
downloadedPath = loader.mediaUrl
|
||||
callback(loader.mediaUrl)
|
||||
return
|
||||
}
|
||||
|
||||
if (! downloadedPath) print("Downloading " + loader.mediaUrl + " ...")
|
||||
|
||||
const args = [loader.mediaUrl, loader.singleMediaInfo.media_crypt_dict]
|
||||
|
||||
py.callCoro("media_cache.get_media", args, path => {
|
||||
if (! downloadedPath) print("Done: " + path)
|
||||
downloadedPath = path
|
||||
callback(path)
|
||||
})
|
||||
}
|
||||
}
|
13
src/gui/Chat/Timeline/EventVideo.qml
Normal file
13
src/gui/Chat/Timeline/EventVideo.qml
Normal file
@@ -0,0 +1,13 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import QtAV 1.7
|
||||
import "../../Base"
|
||||
import "../../Base/MediaPlayer"
|
||||
|
||||
VideoPlayer {
|
||||
id: video
|
||||
|
||||
onHoveredChanged:
|
||||
eventDelegate.hoveredMediaTypeUrl =
|
||||
hovered ? [EventDelegate.Media.Video, video.source] : []
|
||||
}
|
50
src/gui/Chat/TypingMembersBar.qml
Normal file
50
src/gui/Chat/TypingMembersBar.qml
Normal file
@@ -0,0 +1,50 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../Base"
|
||||
|
||||
Rectangle {
|
||||
id: typingMembersBar
|
||||
|
||||
property alias label: typingLabel
|
||||
|
||||
color: theme.chat.typingMembers.background
|
||||
implicitHeight: typingLabel.text ? rowLayout.height : 0
|
||||
opacity: implicitHeight ? 1 : 0
|
||||
|
||||
Behavior on implicitHeight { HNumberAnimation {} }
|
||||
|
||||
HRowLayout {
|
||||
id: rowLayout
|
||||
spacing: theme.spacing
|
||||
|
||||
HIcon {
|
||||
id: icon
|
||||
svgName: "typing" // TODO: animate
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.leftMargin: rowLayout.spacing / 2
|
||||
}
|
||||
|
||||
HLabel {
|
||||
id: typingLabel
|
||||
textFormat: Text.StyledText
|
||||
elide: Text.ElideRight
|
||||
text: {
|
||||
let tm = chat.roomInfo.typing_members
|
||||
|
||||
if (tm.length === 0) return ""
|
||||
if (tm.length === 1) return qsTr("%1 is typing...").arg(tm[0])
|
||||
|
||||
return qsTr("%1 and %2 are typing...")
|
||||
.arg(tm.slice(0, -1).join(", ")).arg(tm.slice(-1)[0])
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.topMargin: rowLayout.spacing / 4
|
||||
Layout.bottomMargin: rowLayout.spacing / 4
|
||||
Layout.leftMargin: rowLayout.spacing / 2
|
||||
Layout.rightMargin: rowLayout.spacing / 2
|
||||
}
|
||||
}
|
||||
}
|
193
src/gui/DebugConsole.qml
Normal file
193
src/gui/DebugConsole.qml
Normal file
@@ -0,0 +1,193 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Window 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "Base"
|
||||
|
||||
HDrawer {
|
||||
id: debugConsole
|
||||
objectName: "debugConsole"
|
||||
edge: Qt.TopEdge
|
||||
x: horizontal ? 0 : referenceSizeParent.width / 2 - width / 2
|
||||
y: vertical ? 0 : referenceSizeParent.height / 2 - height / 2
|
||||
width: horizontal ? calculatedSize : Math.min(window.width, 720)
|
||||
height: vertical ? calculatedSize : Math.min(window.height, 720)
|
||||
defaultSize: 400
|
||||
z: 9999
|
||||
position: 0
|
||||
|
||||
property var previouslyFocused: null
|
||||
|
||||
property var target: null
|
||||
property alias t: debugConsole.target
|
||||
|
||||
property var history: window.history.console
|
||||
property alias his: debugConsole.history
|
||||
property int historyEntry: -1
|
||||
property int maxHistoryLength: 4096
|
||||
|
||||
property string help: qsTr(
|
||||
`Javascript debugging console
|
||||
|
||||
Useful variables:
|
||||
window, theme, settings, shortcuts, utils, mainUI, pageLoader
|
||||
py Python interpreter
|
||||
this The console itself
|
||||
t Target item to debug for which this console was opened
|
||||
his History, list of commands entered
|
||||
|
||||
Special commands:
|
||||
.j OBJECT, .json OBJECT Print OBJECT as human-readable JSON
|
||||
|
||||
.t, .top Attach the console to the parent window's top
|
||||
.b, .bottom Attach the console to the parent window's bottom
|
||||
.l, .left Attach the console to the parent window's left
|
||||
.r, .right Attach the console to the parent window's right
|
||||
.h, .help Show this help`.replace(/^ {8}/gm, "")
|
||||
)
|
||||
|
||||
readonly property alias commandsView: commandsView
|
||||
|
||||
|
||||
Component.onCompleted: {
|
||||
if (mainUI.shortcuts.debugConsole)
|
||||
mainUI.shortcuts.debugConsole.destroy()
|
||||
|
||||
mainUI.shortcuts.debugConsole = debugConsole
|
||||
position = 1
|
||||
commandsView.model.insert(0, {
|
||||
input: "t = " + String(target),
|
||||
output: "",
|
||||
error: false,
|
||||
})
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
previouslyFocused = window.activeFocusItem
|
||||
forceActiveFocus()
|
||||
} else if (previouslyFocused) {
|
||||
previouslyFocused.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
onHistoryEntryChanged:
|
||||
inputField.text =
|
||||
historyEntry === -1 ? "" : history.slice(-historyEntry - 1)[0]
|
||||
|
||||
|
||||
function runJS(input) {
|
||||
if (history.slice(-1)[0] !== input) {
|
||||
history.push(input)
|
||||
while (history.length > maxHistoryLength) history.shift()
|
||||
window.historyChanged()
|
||||
}
|
||||
|
||||
let output = ""
|
||||
let error = false
|
||||
|
||||
try {
|
||||
if ([".h", ".help"].includes(input)) {
|
||||
output = debugConsole.help
|
||||
|
||||
} else if ([".t", ".top"].includes(input)) {
|
||||
debugConsole.edge = Qt.TopEdge
|
||||
|
||||
} else if ([".b", ".bottom"].includes(input)) {
|
||||
debugConsole.edge = Qt.BottomEdge
|
||||
|
||||
} else if ([".l", ".left"].includes(input)) {
|
||||
debugConsole.edge = Qt.LeftEdge
|
||||
|
||||
} else if ([".r", ".right"].includes(input)) {
|
||||
debugConsole.edge = Qt.RightEdge
|
||||
|
||||
} else if (input.startsWith(".j ") || input.startsWith(".json ")) {
|
||||
output = JSON.stringify(eval(input.substring(2)), null, 4)
|
||||
|
||||
} else {
|
||||
let result = eval(input)
|
||||
output = result instanceof Array ?
|
||||
"[" + String(result) + "]" : String(result)
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
error = true
|
||||
output = err.toString()
|
||||
}
|
||||
|
||||
commandsView.model.insert(0, { input, output, error })
|
||||
}
|
||||
|
||||
|
||||
HColumnLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
Keys.onEscapePressed: debugConsole.visible = false
|
||||
|
||||
HListView {
|
||||
id: commandsView
|
||||
spacing: theme.spacing
|
||||
topMargin: theme.spacing
|
||||
bottomMargin: topMargin
|
||||
leftMargin: theme.spacing
|
||||
rightMargin: leftMargin
|
||||
clip: true
|
||||
verticalLayoutDirection: ListView.BottomToTop
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
model: ListModel {}
|
||||
|
||||
delegate: HColumnLayout {
|
||||
width: commandsView.width -
|
||||
commandsView.leftMargin - commandsView.rightMargin
|
||||
|
||||
HLabel {
|
||||
text: "> " + model.input
|
||||
wrapMode: Text.Wrap
|
||||
color: theme.chat.message.quote
|
||||
font.family: theme.fontFamily.mono
|
||||
visible: Boolean(model.input)
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
HLabel {
|
||||
text: "" + model.output
|
||||
wrapMode: Text.Wrap
|
||||
color: model.error ?
|
||||
theme.colors.errorText : theme.colors.text
|
||||
font.family: theme.fontFamily.mono
|
||||
visible: Boolean(model.output)
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
z: -10
|
||||
anchors.fill: parent
|
||||
color: theme.colors.weakBackground
|
||||
}
|
||||
}
|
||||
|
||||
HTextField {
|
||||
id: inputField
|
||||
focus: true
|
||||
onAccepted: if (text) { runJS(text); text = ""; historyEntry = -1 }
|
||||
backgroundColor: Qt.hsla(0, 0, 0, 0.85)
|
||||
bordered: false
|
||||
placeholderText: qsTr("Javascript debug console - Try .help")
|
||||
font.family: theme.fontFamily.mono
|
||||
|
||||
Keys.onUpPressed:
|
||||
if (historyEntry + 1 < history.length ) historyEntry += 1
|
||||
Keys.onDownPressed:
|
||||
if (historyEntry - 1 >= -1) historyEntry -= 1
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
}
|
||||
}
|
||||
}
|
45
src/gui/Dialogs/ExportKeys.qml
Normal file
45
src/gui/Dialogs/ExportKeys.qml
Normal file
@@ -0,0 +1,45 @@
|
||||
import QtQuick 2.12
|
||||
import Qt.labs.platform 1.1
|
||||
import "../Popups"
|
||||
|
||||
HFileDialogOpener {
|
||||
fill: false
|
||||
dialog.title: qsTr("Save decryption keys file as...")
|
||||
dialog.fileMode: FileDialog.SaveFile
|
||||
onFilePicked: {
|
||||
exportPasswordPopup.file = file
|
||||
exportPasswordPopup.open()
|
||||
}
|
||||
|
||||
|
||||
// This is used for the SignOutPopup to know when the export is done
|
||||
// so it can close
|
||||
signal done()
|
||||
|
||||
|
||||
property string userId: ""
|
||||
property bool exporting: false
|
||||
|
||||
|
||||
function exportKeys(file, passphrase) {
|
||||
exporting = true
|
||||
|
||||
let path = file.toString().replace(/^file:\/\//, "")
|
||||
|
||||
py.callClientCoro(userId, "export_keys", [path, passphrase], () => {
|
||||
exporting = false
|
||||
done()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
PasswordPopup {
|
||||
id: exportPasswordPopup
|
||||
details.text: qsTr("Please enter a passphrase to protect this file:")
|
||||
okText: qsTr("Export")
|
||||
|
||||
onAcceptedPasswordChanged: exportKeys(file, acceptedPassword)
|
||||
|
||||
property url file: ""
|
||||
}
|
||||
}
|
90
src/gui/Dialogs/HFileDialogOpener.qml
Normal file
90
src/gui/Dialogs/HFileDialogOpener.qml
Normal file
@@ -0,0 +1,90 @@
|
||||
import QtQuick 2.12
|
||||
import Qt.labs.platform 1.1
|
||||
|
||||
Item {
|
||||
id: opener
|
||||
anchors.fill: fill ? parent : undefined
|
||||
|
||||
|
||||
signal filePicked(string file)
|
||||
signal filesPicked(var files)
|
||||
signal cancelled()
|
||||
|
||||
|
||||
property bool fill: true
|
||||
|
||||
property alias dialog: fileDialog
|
||||
property string selectedFile: ""
|
||||
property string file: ""
|
||||
property var selectedFiles: []
|
||||
property var files: []
|
||||
|
||||
property string selectSubject:
|
||||
dialog.fileMode === FileDialog.SaveFile ? qsTr("file") : qsTr("open")
|
||||
|
||||
enum FileType { All, Images }
|
||||
property int fileType: HFileDialogOpener.FileType.All
|
||||
|
||||
|
||||
TapHandler { enabled: fill; 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: fileMode === FileDialog.OpenFile ?
|
||||
qsTr("Select a file to open") :
|
||||
|
||||
fileMode === FileDialog.OpenFiles ?
|
||||
qsTr("Select files to open") :
|
||||
|
||||
fileMode === FileDialog.SaveFile ?
|
||||
qsTr("Save as...") :
|
||||
|
||||
""
|
||||
|
||||
modality: Qt.NonModal
|
||||
|
||||
onVisibleChanged: if (visible) {
|
||||
opener.selectedFile = Qt.binding(() => Qt.resolvedUrl(currentFile))
|
||||
opener.file = Qt.binding(() => Qt.resolvedUrl(file))
|
||||
opener.files = Qt.binding(() => Qt.resolvedUrl(files))
|
||||
opener.selectedFiles =
|
||||
Qt.binding(() => Qt.resolvedUrl(currentFiles))
|
||||
}
|
||||
|
||||
onAccepted: {
|
||||
opener.selectedFile = currentFile
|
||||
opener.selectedFiles = currentFiles
|
||||
opener.file = file
|
||||
opener.files = files
|
||||
|
||||
opener.filePicked(file)
|
||||
opener.filesPicked(files)
|
||||
}
|
||||
|
||||
onRejected: {
|
||||
selectedFile = ""
|
||||
file = ""
|
||||
selectedFiles = ""
|
||||
files = ""
|
||||
cancelled()
|
||||
}
|
||||
}
|
||||
}
|
59
src/gui/Dialogs/ImportKeys.qml
Normal file
59
src/gui/Dialogs/ImportKeys.qml
Normal file
@@ -0,0 +1,59 @@
|
||||
import QtQuick 2.12
|
||||
import Qt.labs.platform 1.1
|
||||
import "../Popups"
|
||||
|
||||
HFileDialogOpener {
|
||||
fill: false
|
||||
dialog.title: qsTr("Select a decryption keys file to import")
|
||||
onFilePicked: {
|
||||
importPasswordPopup.file = file
|
||||
importPasswordPopup.open()
|
||||
}
|
||||
|
||||
|
||||
property string userId: ""
|
||||
property bool importing: false
|
||||
|
||||
|
||||
PasswordPopup {
|
||||
id: importPasswordPopup
|
||||
details.text: qsTr(
|
||||
"Please enter the passphrase that was used to protect this file:"
|
||||
)
|
||||
okText: qsTr("Import")
|
||||
|
||||
|
||||
property url file: ""
|
||||
|
||||
|
||||
function verifyPassword(pass, callback) {
|
||||
importing = true
|
||||
let path = file.toString().replace(/^file:\/\//, "")
|
||||
|
||||
py.callClientCoro(userId, "import_keys", [path, pass], () => {
|
||||
importing = false
|
||||
callback(true)
|
||||
|
||||
}, (type, args) => {
|
||||
callback(
|
||||
type === "EncryptionError" ?
|
||||
false :
|
||||
|
||||
type === "ValueError" ?
|
||||
qsTr("Invalid file format") :
|
||||
|
||||
type === "FileNotFoundError" ?
|
||||
qsTr("This file doesn't exist") :
|
||||
|
||||
type === "IsADirectoryError" ?
|
||||
qsTr("A folder was given, expecting a file") :
|
||||
|
||||
type === "PermissionError" ?
|
||||
qsTr("No permission to read this file") :
|
||||
|
||||
qsTr("Unknown error: %1 - %2").arg(type).arg(args)
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
29
src/gui/Dialogs/SendFilePicker.qml
Normal file
29
src/gui/Dialogs/SendFilePicker.qml
Normal file
@@ -0,0 +1,29 @@
|
||||
import QtQuick 2.12
|
||||
import Qt.labs.platform 1.1
|
||||
|
||||
HFileDialogOpener {
|
||||
fill: false
|
||||
dialog.title: qsTr("Select a file to send")
|
||||
dialog.fileMode: FileDialog.OpenFiles
|
||||
|
||||
onFilesPicked: {
|
||||
for (let file of files) {
|
||||
let path = Qt.resolvedUrl(file).replace(/^file:/, "")
|
||||
|
||||
utils.sendFile(userId, roomId, path, () => {
|
||||
if (destroyWhenDone) destroy()
|
||||
},
|
||||
(type, args, error, traceback) => {
|
||||
console.error(`python:\n${traceback}`)
|
||||
if (destroyWhenDone) destroy()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onCancelled: if (destroyWhenDone) destroy()
|
||||
|
||||
|
||||
property string userId
|
||||
property string roomId
|
||||
property bool destroyWhenDone: false
|
||||
}
|
221
src/gui/GlobalShortcuts.qml
Normal file
221
src/gui/GlobalShortcuts.qml
Normal file
@@ -0,0 +1,221 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import "Base"
|
||||
|
||||
Item {
|
||||
visible: false
|
||||
|
||||
// Flickable or ListView that should be affected by scroll shortcuts
|
||||
property Item flickTarget
|
||||
|
||||
// A QQC Container that should be affected by tab navigation shortcuts
|
||||
property Container tabsTarget
|
||||
|
||||
// DebugConsole that should be affected by console shortcuts
|
||||
property DebugConsole debugConsole
|
||||
|
||||
readonly property Item toFlick:
|
||||
debugConsole && debugConsole.activeFocus ?
|
||||
debugConsole.commandsView : flickTarget
|
||||
|
||||
|
||||
// App
|
||||
|
||||
HShortcut {
|
||||
enabled: debugMode
|
||||
sequences: settings.keys.startPythonDebugger
|
||||
onActivated: py.call("APP.pdb")
|
||||
}
|
||||
|
||||
HShortcut {
|
||||
enabled: debugMode
|
||||
sequences: settings.keys.toggleDebugConsole
|
||||
onActivated: {
|
||||
if (debugConsole) {
|
||||
debugConsole.visible = ! debugConsole.visible
|
||||
} else {
|
||||
utils.debug(mainUI || window)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HShortcut {
|
||||
sequences: settings.keys.reloadConfig
|
||||
onActivated: py.loadSettings(() => { mainUI.pressAnimation.start() })
|
||||
}
|
||||
|
||||
HShortcut {
|
||||
sequences: settings.keys.zoomIn
|
||||
onActivated: theme.uiScale += 0.1
|
||||
}
|
||||
|
||||
HShortcut {
|
||||
sequences: settings.keys.zoomOut
|
||||
onActivated: theme.uiScale = Math.max(0.1, theme.uiScale - 0.1)
|
||||
}
|
||||
|
||||
HShortcut {
|
||||
sequences: settings.keys.zoomReset
|
||||
onActivated: theme.uiScale = 1
|
||||
}
|
||||
|
||||
// Pages
|
||||
|
||||
HShortcut {
|
||||
sequences: settings.keys.goToLastPage
|
||||
onActivated: mainUI.pageLoader.showPrevious()
|
||||
}
|
||||
|
||||
// Page scrolling
|
||||
|
||||
HShortcut {
|
||||
enabled: toFlick
|
||||
sequences: settings.keys.scrollUp
|
||||
onActivated: utils.flickPages(toFlick, -1 / 10)
|
||||
}
|
||||
|
||||
HShortcut {
|
||||
enabled: toFlick
|
||||
sequences: settings.keys.scrollDown
|
||||
onActivated: utils.flickPages(toFlick, 1 / 10)
|
||||
}
|
||||
|
||||
HShortcut {
|
||||
enabled: toFlick
|
||||
sequences: settings.keys.scrollPageUp
|
||||
onActivated: utils.flickPages(toFlick, -1)
|
||||
}
|
||||
|
||||
HShortcut {
|
||||
enabled: toFlick
|
||||
sequences: settings.keys.scrollPageDown
|
||||
onActivated: utils.flickPages(toFlick, 1)
|
||||
}
|
||||
|
||||
HShortcut {
|
||||
enabled: toFlick
|
||||
sequences: settings.keys.scrollToTop
|
||||
onActivated: utils.flickToTop(toFlick)
|
||||
}
|
||||
|
||||
HShortcut {
|
||||
enabled: toFlick
|
||||
sequences: settings.keys.scrollToBottom
|
||||
onActivated: utils.flickToBottom(toFlick)
|
||||
}
|
||||
|
||||
|
||||
// Tab navigation
|
||||
|
||||
HShortcut {
|
||||
enabled: tabsTarget
|
||||
sequences: settings.keys.previousTab
|
||||
onActivated: tabsTarget.setCurrentIndex(
|
||||
utils.numberWrapAt(tabsTarget.currentIndex - 1, tabsTarget.count),
|
||||
)
|
||||
}
|
||||
|
||||
HShortcut {
|
||||
enabled: tabsTarget
|
||||
sequences: settings.keys.nextTab
|
||||
onActivated: tabsTarget.setCurrentIndex(
|
||||
utils.numberWrapAt(tabsTarget.currentIndex + 1, tabsTarget.count),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// MainPane
|
||||
|
||||
HShortcut {
|
||||
enabled: mainUI.accountsPresent
|
||||
sequences: settings.keys.focusMainPane
|
||||
onActivated: mainUI.mainPane.toggleFocus()
|
||||
context: Qt.ApplicationShortcut
|
||||
}
|
||||
|
||||
HShortcut {
|
||||
enabled: mainUI.accountsPresent
|
||||
sequences: settings.keys.clearRoomFilter
|
||||
onActivated: mainUI.mainPane.toolBar.roomFilter = ""
|
||||
}
|
||||
|
||||
HShortcut {
|
||||
enabled: mainUI.accountsPresent
|
||||
sequences: settings.keys.addNewAccount
|
||||
onActivated: mainUI.mainPane.toolBar.addAccountButton.clicked()
|
||||
}
|
||||
|
||||
HShortcut {
|
||||
enabled: mainUI.accountsPresent
|
||||
sequences: settings.keys.addNewChat
|
||||
onActivated: mainUI.mainPane.mainPaneList.addNewChat()
|
||||
}
|
||||
|
||||
|
||||
HShortcut {
|
||||
enabled: mainUI.accountsPresent
|
||||
sequences: settings.keys.accountSettings
|
||||
onActivated: mainUI.mainPane.mainPaneList.accountSettings()
|
||||
}
|
||||
|
||||
|
||||
HShortcut {
|
||||
enabled: mainUI.accountsPresent
|
||||
sequences: settings.keys.toggleCollapseAccount
|
||||
onActivated: mainUI.mainPane.mainPaneList.toggleCollapseAccount()
|
||||
}
|
||||
|
||||
|
||||
HShortcut {
|
||||
enabled: mainUI.accountsPresent
|
||||
sequences: settings.keys.goToPreviousRoom
|
||||
onActivated: mainUI.mainPane.mainPaneList.previous()
|
||||
}
|
||||
|
||||
HShortcut {
|
||||
enabled: mainUI.accountsPresent
|
||||
sequences: settings.keys.goToNextRoom
|
||||
onActivated: mainUI.mainPane.mainPaneList.next()
|
||||
}
|
||||
|
||||
|
||||
// Chat
|
||||
|
||||
HShortcut {
|
||||
enabled: window.uiState.page === "Chat/Chat.qml"
|
||||
sequences: settings.keys.clearRoomMessages
|
||||
onActivated: utils.makePopup(
|
||||
"Popups/ClearMessagesPopup.qml",
|
||||
mainUI,
|
||||
{
|
||||
userId: window.uiState.pageProperties.userId,
|
||||
roomId: window.uiState.pageProperties.roomId,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
HShortcut {
|
||||
enabled: window.uiState.page === "Chat/Chat.qml"
|
||||
sequences: settings.keys.sendFile
|
||||
onActivated: utils.makeObject(
|
||||
"Dialogs/SendFilePicker.qml",
|
||||
mainUI,
|
||||
{
|
||||
userId: window.uiState.pageProperties.userId,
|
||||
roomId: window.uiState.pageProperties.roomId,
|
||||
destroyWhenDone: true,
|
||||
},
|
||||
picker => { picker.dialog.open() }
|
||||
)
|
||||
}
|
||||
|
||||
HShortcut {
|
||||
enabled: window.uiState.page === "Chat/Chat.qml"
|
||||
sequences: settings.keys.sendFileFromPathInClipboard
|
||||
onActivated: utils.sendFile(
|
||||
window.uiState.pageProperties.userId,
|
||||
window.uiState.pageProperties.roomId,
|
||||
Clipboard.text.trim(),
|
||||
)
|
||||
}
|
||||
}
|
26
src/gui/LoadingScreen.qml
Normal file
26
src/gui/LoadingScreen.qml
Normal file
@@ -0,0 +1,26 @@
|
||||
import QtQuick 2.12
|
||||
import QtGraphicalEffects 1.12
|
||||
import "Base"
|
||||
|
||||
Item {
|
||||
LinearGradient {
|
||||
anchors.fill: parent
|
||||
start: Qt.point(0, 0)
|
||||
end: Qt.point(window.width, window.height)
|
||||
|
||||
gradient: Gradient {
|
||||
GradientStop { position: 0.0; color: "#001b20" }
|
||||
GradientStop { position: 1.0; color: "#3c2f4b" }
|
||||
}
|
||||
}
|
||||
|
||||
HBusyIndicator {
|
||||
anchors.centerIn: parent
|
||||
width: Math.min(160, parent.width - 16, parent.height - 16)
|
||||
height: width
|
||||
indeterminateSpan: 0.5
|
||||
|
||||
foregroundColor: Qt.hsla(0, 0, 1, 0.15)
|
||||
progressColor: Qt.hsla(0, 0, 1, 0.4)
|
||||
}
|
||||
}
|
122
src/gui/MainPane/AccountDelegate.qml
Normal file
122
src/gui/MainPane/AccountDelegate.qml
Normal file
@@ -0,0 +1,122 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../Base"
|
||||
|
||||
HTileDelegate {
|
||||
id: accountDelegate
|
||||
spacing: 0
|
||||
topPadding: model.index > 0 ? theme.spacing / 2 : 0
|
||||
bottomPadding: topPadding
|
||||
backgroundColor: theme.mainPane.account.background
|
||||
opacity: collapsed && ! forceExpand ?
|
||||
theme.mainPane.account.collapsedOpacity : 1
|
||||
|
||||
shouldBeCurrent:
|
||||
window.uiState.page === "Pages/AccountSettings/AccountSettings.qml" &&
|
||||
window.uiState.pageProperties.userId === model.data.user_id
|
||||
|
||||
setCurrentTimer.running:
|
||||
! mainPaneList.activateLimiter.running && ! mainPane.hasFocus
|
||||
|
||||
|
||||
Behavior on opacity { HNumberAnimation {} }
|
||||
|
||||
|
||||
readonly property bool forceExpand: Boolean(mainPaneList.filter)
|
||||
|
||||
// Hide harmless error when a filter matches nothing
|
||||
readonly property bool collapsed: try {
|
||||
return mainPaneList.collapseAccounts[model.data.user_id] || false
|
||||
} catch (err) {}
|
||||
|
||||
|
||||
onActivated: pageLoader.showPage(
|
||||
"AccountSettings/AccountSettings", { "userId": model.data.user_id }
|
||||
)
|
||||
|
||||
|
||||
function toggleCollapse() {
|
||||
window.uiState.collapseAccounts[model.data.user_id] = ! collapsed
|
||||
window.uiStateChanged()
|
||||
}
|
||||
|
||||
|
||||
image: HUserAvatar {
|
||||
userId: model.data.user_id
|
||||
displayName: model.data.display_name
|
||||
mxc: model.data.avatar_url
|
||||
}
|
||||
|
||||
title.color: theme.mainPane.account.name
|
||||
title.text: model.data.display_name || model.data.user_id
|
||||
title.font.pixelSize: theme.fontSize.big
|
||||
title.leftPadding: theme.spacing
|
||||
|
||||
HButton {
|
||||
id: addChat
|
||||
iconItem.small: true
|
||||
icon.name: "add-chat"
|
||||
backgroundColor: "transparent"
|
||||
toolTip.text: qsTr("Add new chat")
|
||||
onClicked: pageLoader.showPage(
|
||||
"AddChat/AddChat", {userId: model.data.user_id},
|
||||
)
|
||||
|
||||
leftPadding: theme.spacing / 2
|
||||
rightPadding: leftPadding
|
||||
|
||||
visible: opacity > 0
|
||||
opacity: expand.loading ? 0 : 1
|
||||
|
||||
Layout.fillHeight: true
|
||||
|
||||
Behavior on opacity { HNumberAnimation {} }
|
||||
}
|
||||
|
||||
HButton {
|
||||
id: expand
|
||||
loading: ! model.data.first_sync_done || ! model.data.profile_updated
|
||||
iconItem.small: true
|
||||
icon.name: "expand"
|
||||
backgroundColor: "transparent"
|
||||
toolTip.text: collapsed ? qsTr("Expand") : qsTr("Collapse")
|
||||
onClicked: accountDelegate.toggleCollapse()
|
||||
|
||||
leftPadding: theme.spacing / 2
|
||||
rightPadding: leftPadding
|
||||
|
||||
visible: opacity > 0
|
||||
opacity: ! loading && accountDelegate.forceExpand ? 0 : 1
|
||||
|
||||
Layout.fillHeight: true
|
||||
|
||||
iconItem.transform: Rotation {
|
||||
origin.x: expand.iconItem.width / 2
|
||||
origin.y: expand.iconItem.height / 2
|
||||
angle: expand.loading ? 0 : collapsed ? 180 : 90
|
||||
|
||||
Behavior on angle { HNumberAnimation {} }
|
||||
}
|
||||
|
||||
Behavior on opacity { HNumberAnimation {} }
|
||||
}
|
||||
|
||||
contextMenu: HMenu {
|
||||
HMenuItem {
|
||||
icon.name: "copy-user-id"
|
||||
text: qsTr("Copy user ID")
|
||||
onTriggered: Clipboard.text = model.data.user_id
|
||||
}
|
||||
|
||||
HMenuItem {
|
||||
icon.name: "sign-out"
|
||||
icon.color: theme.colors.negativeBackground
|
||||
text: qsTr("Sign out")
|
||||
onTriggered: utils.makePopup(
|
||||
"Popups/SignOutPopup.qml",
|
||||
window,
|
||||
{ "userId": model.data.user_id },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
141
src/gui/MainPane/AccountRoomList.qml
Normal file
141
src/gui/MainPane/AccountRoomList.qml
Normal file
@@ -0,0 +1,141 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../Base"
|
||||
|
||||
HListView {
|
||||
id: mainPaneList
|
||||
|
||||
|
||||
readonly property var originSource: window.mainPaneModelSource
|
||||
readonly property var collapseAccounts: window.uiState.collapseAccounts
|
||||
readonly property string filter: toolBar.roomFilter
|
||||
readonly property alias activateLimiter: activateLimiter
|
||||
|
||||
onOriginSourceChanged: filterLimiter.restart()
|
||||
onFilterChanged: filterLimiter.restart()
|
||||
onCollapseAccountsChanged: filterLimiter.restart()
|
||||
|
||||
|
||||
function filterSource() {
|
||||
let show = []
|
||||
|
||||
// Hide a harmless error when activating a RoomDelegate
|
||||
try { window.mainPaneModelSource } catch (err) { return }
|
||||
|
||||
for (let i = 0; i < window.mainPaneModelSource.length; i++) {
|
||||
let item = window.mainPaneModelSource[i]
|
||||
|
||||
if (item.type === "Account" ||
|
||||
(filter ?
|
||||
utils.filterMatches(filter, item.data.filter_string) :
|
||||
! window.uiState.collapseAccounts[item.user_id]))
|
||||
{
|
||||
if (filter && show.length && item.type === "Account" &&
|
||||
show[show.length - 1].type === "Account" &&
|
||||
! utils.filterMatches(
|
||||
filter, show[show.length - 1].data.filter_string)
|
||||
) {
|
||||
// If filter active, current and previous items are
|
||||
// both accounts and previous account doesn't match filter,
|
||||
// that means the previous account had no matching rooms.
|
||||
show.pop()
|
||||
}
|
||||
|
||||
show.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
let last = show[show.length - 1]
|
||||
if (show.length && filter && last.type === "Account" &&
|
||||
! utils.filterMatches(filter, last.data.filter_string))
|
||||
{
|
||||
// If filter active, last item is an account and last item
|
||||
// doesn't match filter, that account had no matching rooms.
|
||||
show.pop()
|
||||
}
|
||||
|
||||
model.source = show
|
||||
}
|
||||
|
||||
function previous(activate=true) {
|
||||
decrementCurrentIndex()
|
||||
if (activate) activateLimiter.restart()
|
||||
}
|
||||
|
||||
function next(activate=true) {
|
||||
incrementCurrentIndex()
|
||||
if (activate) activateLimiter.restart()
|
||||
}
|
||||
|
||||
function activate() {
|
||||
currentItem.item.activated()
|
||||
}
|
||||
|
||||
function accountSettings() {
|
||||
if (! currentItem) incrementCurrentIndex()
|
||||
|
||||
pageLoader.showPage(
|
||||
"AccountSettings/AccountSettings",
|
||||
{userId: currentItem.item.delegateModel.user_id},
|
||||
)
|
||||
}
|
||||
|
||||
function addNewChat() {
|
||||
if (! currentItem) incrementCurrentIndex()
|
||||
|
||||
pageLoader.showPage(
|
||||
"AddChat/AddChat",
|
||||
{userId: currentItem.item.delegateModel.user_id},
|
||||
)
|
||||
}
|
||||
|
||||
function toggleCollapseAccount() {
|
||||
if (filter) return
|
||||
|
||||
if (! currentItem) incrementCurrentIndex()
|
||||
|
||||
if (currentItem.item.delegateModel.type === "Account") {
|
||||
currentItem.item.toggleCollapse()
|
||||
return
|
||||
}
|
||||
|
||||
for (let i = 0; i < model.source.length; i++) {
|
||||
let item = model.source[i]
|
||||
|
||||
if (item.type === "Account" && item.user_id ==
|
||||
currentItem.item.delegateModel.user_id)
|
||||
{
|
||||
currentIndex = i
|
||||
currentItem.item.toggleCollapse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
model: HListModel {
|
||||
keyField: "id"
|
||||
source: originSource
|
||||
}
|
||||
|
||||
delegate: Loader {
|
||||
width: mainPaneList.width
|
||||
Component.onCompleted: setSource(
|
||||
model.type === "Account" ?
|
||||
"AccountDelegate.qml" : "RoomDelegate.qml",
|
||||
{view: mainPaneList}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Timer {
|
||||
id: filterLimiter
|
||||
interval: 16
|
||||
onTriggered: filterSource()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: activateLimiter
|
||||
interval: 300
|
||||
onTriggered: activate()
|
||||
}
|
||||
}
|
56
src/gui/MainPane/MainPane.qml
Normal file
56
src/gui/MainPane/MainPane.qml
Normal file
@@ -0,0 +1,56 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../Base"
|
||||
|
||||
HDrawer {
|
||||
id: mainPane
|
||||
saveName: "mainPane"
|
||||
color: theme.mainPane.background
|
||||
minimumSize: theme.controls.avatar.size + theme.spacing * 2
|
||||
|
||||
|
||||
property bool hasFocus: toolBar.filterField.activeFocus
|
||||
property alias mainPaneList: mainPaneList
|
||||
property alias toolBar: toolBar
|
||||
|
||||
|
||||
function toggleFocus() {
|
||||
if (toolBar.filterField.activeFocus) {
|
||||
pageLoader.takeFocus()
|
||||
return
|
||||
}
|
||||
|
||||
mainPane.open()
|
||||
toolBar.filterField.forceActiveFocus()
|
||||
}
|
||||
|
||||
|
||||
Behavior on opacity { HNumberAnimation {} }
|
||||
|
||||
Binding on visible {
|
||||
value: false
|
||||
when: ! mainUI.accountsPresent
|
||||
}
|
||||
|
||||
HColumnLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
AccountRoomList {
|
||||
id: mainPaneList
|
||||
clip: true
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
MainPaneToolBar {
|
||||
id: toolBar
|
||||
mainPaneList: mainPaneList
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: false
|
||||
Layout.preferredHeight: theme.baseElementsHeight
|
||||
|
||||
}
|
||||
}
|
||||
}
|
58
src/gui/MainPane/MainPaneToolBar.qml
Normal file
58
src/gui/MainPane/MainPaneToolBar.qml
Normal file
@@ -0,0 +1,58 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../Base"
|
||||
|
||||
HRowLayout {
|
||||
id: toolBar
|
||||
// Hide filter field overflowing for a sec on size changes
|
||||
clip: true
|
||||
|
||||
property AccountRoomList mainPaneList
|
||||
readonly property alias addAccountButton: addAccountButton
|
||||
readonly property alias filterField: filterField
|
||||
property alias roomFilter: filterField.text
|
||||
|
||||
HButton {
|
||||
id: addAccountButton
|
||||
icon.name: "add-account"
|
||||
toolTip.text: qsTr("Add another account")
|
||||
backgroundColor: theme.mainPane.settingsButton.background
|
||||
onClicked: pageLoader.showPage("AddAccount/AddAccount")
|
||||
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
HTextField {
|
||||
id: filterField
|
||||
saveName: "roomFilterField"
|
||||
|
||||
placeholderText: qsTr("Filter rooms")
|
||||
backgroundColor: theme.mainPane.filterRooms.background
|
||||
bordered: false
|
||||
opacity: width >= 16 * theme.uiScale ? 1 : 0
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
Keys.onUpPressed: mainPaneList.previous(false) // do not activate
|
||||
Keys.onDownPressed: mainPaneList.next(false)
|
||||
|
||||
Keys.onEnterPressed: Keys.onReturnPressed(event)
|
||||
Keys.onReturnPressed: {
|
||||
if (event.modifiers & Qt.ShiftModifier) {
|
||||
mainPaneList.toggleCollapseAccount()
|
||||
return
|
||||
}
|
||||
|
||||
if (window.settings.clearRoomFilterOnEnter) text = ""
|
||||
mainPaneList.activate()
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: {
|
||||
if (window.settings.clearRoomFilterOnEscape) text = ""
|
||||
mainUI.pageLoader.forceActiveFocus()
|
||||
}
|
||||
|
||||
Behavior on opacity { HNumberAnimation {} }
|
||||
}
|
||||
}
|
165
src/gui/MainPane/RoomDelegate.qml
Normal file
165
src/gui/MainPane/RoomDelegate.qml
Normal file
@@ -0,0 +1,165 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../Base"
|
||||
|
||||
HTileDelegate {
|
||||
id: roomDelegate
|
||||
spacing: theme.spacing
|
||||
backgroundColor: theme.mainPane.room.background
|
||||
opacity: model.data.left ? theme.mainPane.room.leftRoomOpacity : 1
|
||||
|
||||
shouldBeCurrent:
|
||||
window.uiState.page === "Chat/Chat.qml" &&
|
||||
window.uiState.pageProperties.userId === model.user_id &&
|
||||
window.uiState.pageProperties.roomId === model.data.room_id
|
||||
|
||||
setCurrentTimer.running:
|
||||
! mainPaneList.activateLimiter.running && ! mainPane.hasFocus
|
||||
|
||||
|
||||
Behavior on opacity { HNumberAnimation {} }
|
||||
|
||||
|
||||
readonly property bool joined: ! invited && ! parted
|
||||
readonly property bool invited: model.data.inviter_id && ! parted
|
||||
readonly property bool parted: model.data.left
|
||||
readonly property var lastEvent: model.data.last_event
|
||||
|
||||
|
||||
onActivated: pageLoader.showRoom(model.user_id, model.data.room_id)
|
||||
|
||||
|
||||
image: HRoomAvatar {
|
||||
displayName: model.data.display_name
|
||||
mxc: model.data.avatar_url
|
||||
}
|
||||
|
||||
title.color: theme.mainPane.room.name
|
||||
title.text: model.data.display_name || qsTr("Empty room")
|
||||
|
||||
additionalInfo.children: HIcon {
|
||||
svgName: "invite-received"
|
||||
colorize: theme.colors.alertBackground
|
||||
|
||||
visible: invited
|
||||
Layout.maximumWidth: invited ? implicitWidth : 0
|
||||
|
||||
Behavior on Layout.maximumWidth { HNumberAnimation {} }
|
||||
}
|
||||
|
||||
rightInfo.color: theme.mainPane.room.lastEventDate
|
||||
rightInfo.text: {
|
||||
! lastEvent || ! lastEvent.date ?
|
||||
"" :
|
||||
|
||||
utils.dateIsToday(lastEvent.date) ?
|
||||
utils.formatTime(lastEvent.date, false) : // no seconds
|
||||
|
||||
lastEvent.date.getFullYear() === new Date().getFullYear() ?
|
||||
Qt.formatDate(lastEvent.date, "d MMM") : // e.g. "5 Dec"
|
||||
|
||||
lastEvent.date.getFullYear()
|
||||
}
|
||||
|
||||
subtitle.color: theme.mainPane.room.subtitle
|
||||
subtitle.font.italic:
|
||||
Boolean(lastEvent && lastEvent.event_type === "RoomMessageEmote")
|
||||
subtitle.textFormat: Text.StyledText
|
||||
subtitle.text: {
|
||||
if (! lastEvent) return ""
|
||||
|
||||
let isEmote = lastEvent.event_type === "RoomMessageEmote"
|
||||
let isMsg = lastEvent.event_type.startsWith("RoomMessage")
|
||||
let isUnknownMsg = lastEvent.event_type === "RoomMessageUnknown"
|
||||
let isCryptMedia = lastEvent.event_type.startsWith("RoomEncrypted")
|
||||
|
||||
// If it's a general event
|
||||
if (isEmote || isUnknownMsg || (! isMsg && ! isCryptMedia)) {
|
||||
return utils.processedEventText(lastEvent)
|
||||
}
|
||||
|
||||
let text = utils.coloredNameHtml(
|
||||
lastEvent.sender_name, lastEvent.sender_id
|
||||
) + ": " + lastEvent.inline_content
|
||||
|
||||
return text.replace(
|
||||
/< *span +class=['"]?quote['"]? *>(.+?)<\/ *span *>/g,
|
||||
`<font color="${theme.mainPane.room.subtitleQuote}">$1</font>`,
|
||||
)
|
||||
}
|
||||
|
||||
contextMenu: HMenu {
|
||||
HMenuItem {
|
||||
visible: joined
|
||||
enabled: model.data.can_invite
|
||||
icon.name: "room-send-invite"
|
||||
text: qsTr("Invite members")
|
||||
|
||||
onTriggered: utils.makePopup(
|
||||
"Popups/InviteToRoomPopup.qml",
|
||||
window,
|
||||
{
|
||||
userId: model.user_id,
|
||||
roomId: model.data.room_id,
|
||||
roomName: model.data.display_name,
|
||||
invitingAllowed: Qt.binding(() => model.data.can_invite)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
HMenuItem {
|
||||
icon.name: "copy-room-id"
|
||||
text: qsTr("Copy room ID")
|
||||
onTriggered: Clipboard.text = model.data.room_id
|
||||
}
|
||||
|
||||
HMenuItem {
|
||||
visible: invited
|
||||
icon.name: "invite-accept"
|
||||
icon.color: theme.colors.positiveBackground
|
||||
text: qsTr("Accept %1's invite").arg(utils.coloredNameHtml(
|
||||
model.data.inviter_name, model.data.inviter_id
|
||||
))
|
||||
label.textFormat: Text.StyledText
|
||||
|
||||
onTriggered: py.callClientCoro(
|
||||
model.user_id, "join", [model.data.room_id]
|
||||
)
|
||||
}
|
||||
|
||||
HMenuItem {
|
||||
visible: invited || joined
|
||||
icon.name: invited ? "invite-decline" : "room-leave"
|
||||
icon.color: theme.colors.negativeBackground
|
||||
text: invited ? qsTr("Decline invite") : qsTr("Leave")
|
||||
|
||||
onTriggered: utils.makePopup(
|
||||
"Popups/LeaveRoomPopup.qml",
|
||||
window,
|
||||
{
|
||||
userId: model.user_id,
|
||||
roomId: model.data.room_id,
|
||||
roomName: model.data.display_name,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
HMenuItem {
|
||||
icon.name: "room-forget"
|
||||
icon.color: theme.colors.negativeBackground
|
||||
text: qsTr("Forget")
|
||||
|
||||
onTriggered: utils.makePopup(
|
||||
"Popups/ForgetRoomPopup.qml",
|
||||
window,
|
||||
{
|
||||
userId: model.user_id,
|
||||
roomId: model.data.room_id,
|
||||
roomName: model.data.display_name,
|
||||
},
|
||||
null,
|
||||
false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
61
src/gui/Pages/AccountSettings/AccountSettings.qml
Normal file
61
src/gui/Pages/AccountSettings/AccountSettings.qml
Normal file
@@ -0,0 +1,61 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../../Base"
|
||||
|
||||
HPage {
|
||||
id: accountSettings
|
||||
|
||||
property int avatarPreferredSize: 256 * theme.uiScale
|
||||
|
||||
property string userId: ""
|
||||
|
||||
readonly property bool ready:
|
||||
accountInfo !== "waiting" && Boolean(accountInfo.profile_updated)
|
||||
|
||||
readonly property var accountInfo: utils.getItem(
|
||||
modelSources["Account"] || [], "user_id", userId
|
||||
) || "waiting"
|
||||
|
||||
property string headerName: ready ? accountInfo.display_name : userId
|
||||
|
||||
hideHeaderUnderHeight: avatarPreferredSize
|
||||
headerLabel.text: qsTr("Account settings for %1").arg(
|
||||
utils.coloredNameHtml(headerName, userId)
|
||||
)
|
||||
|
||||
|
||||
HSpacer {}
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
model: ["Profile.qml", "ImportExportKeys.qml"]
|
||||
|
||||
Rectangle {
|
||||
color: ready ? theme.controls.box.background : "transparent"
|
||||
Behavior on color { HColorAnimation {} }
|
||||
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.topMargin: header.visible || index > 0 ? theme.spacing : 0
|
||||
Layout.bottomMargin:
|
||||
header.visible || index < repeater.count - 1? theme.spacing : 0
|
||||
|
||||
Layout.maximumWidth: Math.min(parent.width, 640)
|
||||
Layout.preferredWidth:
|
||||
pageLoader.isWide ? parent.width : avatarPreferredSize
|
||||
|
||||
Layout.preferredHeight: childrenRect.height
|
||||
|
||||
HLoader {
|
||||
anchors.centerIn: parent
|
||||
width: ready ? parent.width : 96
|
||||
source: ready ?
|
||||
modelData :
|
||||
(modelData === "Profile.qml" ?
|
||||
"../../Base/HBusyIndicator.qml" : "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HSpacer {}
|
||||
}
|
46
src/gui/Pages/AccountSettings/ImportExportKeys.qml
Normal file
46
src/gui/Pages/AccountSettings/ImportExportKeys.qml
Normal file
@@ -0,0 +1,46 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../../Base"
|
||||
|
||||
HBox {
|
||||
buttonModel: [
|
||||
{ name: "export", text: qsTr("Export"), iconName: "export-keys"},
|
||||
{ name: "import", text: qsTr("Import"), iconName: "import-keys"},
|
||||
]
|
||||
|
||||
buttonCallbacks: ({
|
||||
export: button => {
|
||||
utils.makeObject(
|
||||
"Dialogs/ExportKeys.qml",
|
||||
accountSettings,
|
||||
{ userId: accountSettings.userId },
|
||||
obj => {
|
||||
button.loading = Qt.binding(() => obj.exporting)
|
||||
obj.dialog.open()
|
||||
}
|
||||
)
|
||||
},
|
||||
import: button => {
|
||||
utils.makeObject(
|
||||
"Dialogs/ImportKeys.qml",
|
||||
accountSettings,
|
||||
{ userId: accountSettings.userId },
|
||||
obj => { obj.dialog.open() }
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
HLabel {
|
||||
wrapMode: Text.Wrap
|
||||
text: qsTr(
|
||||
"The decryption keys for messages you received in encrypted " +
|
||||
"rooms can be exported to a passphrase-protected file.\n\n" +
|
||||
|
||||
"You can then import this file on any Matrix account or " +
|
||||
"client, to be able to decrypt these messages again."
|
||||
)
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
219
src/gui/Pages/AccountSettings/Profile.qml
Normal file
219
src/gui/Pages/AccountSettings/Profile.qml
Normal file
@@ -0,0 +1,219 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../../Base"
|
||||
import "../../Dialogs"
|
||||
|
||||
HGridLayout {
|
||||
function applyChanges() {
|
||||
if (nameField.changed) {
|
||||
saveButton.nameChangeRunning = true
|
||||
|
||||
py.callClientCoro(
|
||||
userId, "set_displayname", [nameField.field.text], () => {
|
||||
saveButton.nameChangeRunning = false
|
||||
accountSettings.headerName =
|
||||
Qt.binding(() => accountInfo.display_name)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (aliasField.changed) {
|
||||
window.settings.writeAliases[userId] = aliasField.field.text
|
||||
window.settingsChanged()
|
||||
}
|
||||
|
||||
if (avatar.changed) {
|
||||
saveButton.avatarChangeRunning = true
|
||||
|
||||
let path =
|
||||
Qt.resolvedUrl(avatar.sourceOverride).replace(/^file:/, "")
|
||||
|
||||
py.callClientCoro(userId, "set_avatar_from_file", [path], () => {
|
||||
saveButton.avatarChangeRunning = false
|
||||
}, (errType, [httpCode]) => {
|
||||
console.error("Avatar upload failed:", httpCode, errType)
|
||||
saveButton.avatarChangeRunning = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function cancelChanges() {
|
||||
nameField.field.text = accountInfo.display_name
|
||||
aliasField.field.text = aliasField.currentAlias
|
||||
fileDialog.selectedFile = ""
|
||||
fileDialog.file = ""
|
||||
|
||||
accountSettings.headerName = Qt.binding(() => accountInfo.display_name)
|
||||
}
|
||||
|
||||
columns: 2
|
||||
flow: pageLoader.isWide ? GridLayout.LeftToRight : GridLayout.TopToBottom
|
||||
rowSpacing: currentSpacing
|
||||
|
||||
Component.onCompleted: nameField.field.forceActiveFocus()
|
||||
|
||||
HUserAvatar {
|
||||
property bool changed: Boolean(sourceOverride)
|
||||
|
||||
id: avatar
|
||||
userId: accountSettings.userId
|
||||
displayName: nameField.field.text
|
||||
mxc: accountInfo.avatar_url
|
||||
toolTipMxc: ""
|
||||
sourceOverride: fileDialog.selectedFile || fileDialog.file
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
Layout.preferredWidth: Math.min(flickable.height, avatarPreferredSize)
|
||||
Layout.preferredHeight: Layout.preferredWidth
|
||||
|
||||
Rectangle {
|
||||
z: 10
|
||||
visible: opacity > 0
|
||||
opacity: ! fileDialog.dialog.visible &&
|
||||
((! avatar.mxc && ! avatar.changed) || avatar.hovered) ?
|
||||
1 : 0
|
||||
|
||||
anchors.fill: parent
|
||||
color: utils.hsluv(0, 0, 0,
|
||||
(! avatar.mxc && overlayHover.hovered) ? 0.8 : 0.7
|
||||
)
|
||||
|
||||
Behavior on opacity { HNumberAnimation {} }
|
||||
Behavior on color { HColorAnimation {} }
|
||||
|
||||
HoverHandler { id: overlayHover }
|
||||
|
||||
HColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: currentSpacing
|
||||
width: parent.width
|
||||
|
||||
HIcon {
|
||||
svgName: "upload-avatar"
|
||||
colorize: (! avatar.mxc && overlayHover.hovered) ?
|
||||
theme.colors.accentText : theme.icons.colorize
|
||||
dimension: avatar.width / 3
|
||||
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
}
|
||||
|
||||
Item { Layout.preferredHeight: theme.spacing }
|
||||
|
||||
HLabel {
|
||||
text: avatar.mxc ?
|
||||
qsTr("Change profile picture") :
|
||||
qsTr("Upload profile picture")
|
||||
|
||||
color: (! avatar.mxc && overlayHover.hovered) ?
|
||||
theme.colors.accentText : theme.colors.brightText
|
||||
Behavior on color { HColorAnimation {} }
|
||||
|
||||
font.pixelSize: theme.fontSize.big *
|
||||
avatar.height / avatarPreferredSize
|
||||
wrapMode: Text.WordWrap
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HFileDialogOpener {
|
||||
id: fileDialog
|
||||
fileType: HFileDialogOpener.FileType.Images
|
||||
dialog.title: qsTr("Select profile picture for %1")
|
||||
.arg(accountInfo.display_name)
|
||||
}
|
||||
}
|
||||
|
||||
HColumnLayout {
|
||||
id: profileInfo
|
||||
spacing: theme.spacing
|
||||
|
||||
HColumnLayout {
|
||||
spacing: theme.spacing
|
||||
Layout.margins: currentSpacing
|
||||
|
||||
HLabel {
|
||||
text: qsTr("User ID:<br>%1")
|
||||
.arg(utils.coloredNameHtml(userId, userId, userId))
|
||||
textFormat: Text.StyledText
|
||||
wrapMode: Text.Wrap
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
HLabeledTextField {
|
||||
property bool changed: field.text !== accountInfo.display_name
|
||||
|
||||
readonly property string fText: field.text
|
||||
onFTextChanged: accountSettings.headerName = field.text
|
||||
|
||||
id: nameField
|
||||
label.text: qsTr("Display name:")
|
||||
field.onAccepted: applyChanges()
|
||||
|
||||
Component.onCompleted: field.text = accountInfo.display_name
|
||||
|
||||
Keys.onEscapePressed: cancelChanges()
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: 480
|
||||
}
|
||||
|
||||
HLabeledTextField {
|
||||
property string currentAlias:
|
||||
window.settings.writeAliases[userId] || ""
|
||||
|
||||
property bool changed: field.text !== currentAlias
|
||||
|
||||
id: aliasField
|
||||
label.text: qsTr("Write alias:")
|
||||
field.onAccepted: applyChanges()
|
||||
|
||||
Component.onCompleted: field.text = currentAlias
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: 480
|
||||
|
||||
Keys.onEscapePressed: cancelChanges()
|
||||
}
|
||||
}
|
||||
|
||||
HSpacer {}
|
||||
|
||||
HRowLayout {
|
||||
Layout.alignment: Qt.AlignBottom
|
||||
|
||||
HButton {
|
||||
property bool nameChangeRunning: false
|
||||
property bool avatarChangeRunning: false
|
||||
|
||||
id: saveButton
|
||||
icon.name: "apply"
|
||||
icon.color: theme.colors.positiveBackground
|
||||
text: qsTr("Save")
|
||||
loading: nameChangeRunning || avatarChangeRunning
|
||||
enabled:
|
||||
nameField.changed || aliasField.changed || avatar.changed
|
||||
onClicked: applyChanges()
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignBottom
|
||||
}
|
||||
|
||||
HButton {
|
||||
icon.name: "cancel"
|
||||
icon.color: theme.colors.negativeBackground
|
||||
text: qsTr("Cancel")
|
||||
enabled: saveButton.enabled && ! saveButton.loading
|
||||
onClicked: cancelChanges()
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignBottom
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
15
src/gui/Pages/AddAccount/AddAccount.qml
Normal file
15
src/gui/Pages/AddAccount/AddAccount.qml
Normal file
@@ -0,0 +1,15 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../../Base"
|
||||
|
||||
HPage {
|
||||
HTabContainer {
|
||||
tabModel: [
|
||||
qsTr("Sign in"), qsTr("Register"), qsTr("Reset"),
|
||||
]
|
||||
|
||||
SignIn { Component.onCompleted: forceActiveFocus() }
|
||||
Register {}
|
||||
Reset {}
|
||||
}
|
||||
}
|
29
src/gui/Pages/AddAccount/Register.qml
Normal file
29
src/gui/Pages/AddAccount/Register.qml
Normal file
@@ -0,0 +1,29 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../../Base"
|
||||
|
||||
HBox {
|
||||
id: signInBox
|
||||
clickButtonOnEnter: "ok"
|
||||
|
||||
buttonModel: [
|
||||
{ name: "ok", text: qsTr("Register from Riot"), iconName: "register" },
|
||||
]
|
||||
|
||||
buttonCallbacks: ({
|
||||
ok: button => {
|
||||
Qt.openUrlExternally("https://riot.im/app/#/register")
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
HLabel {
|
||||
wrapMode: Text.Wrap
|
||||
text: qsTr(
|
||||
"Registering is not implemented yet. You can create a new " +
|
||||
"account from a client that supports it, like Riot."
|
||||
)
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
33
src/gui/Pages/AddAccount/Reset.qml
Normal file
33
src/gui/Pages/AddAccount/Reset.qml
Normal file
@@ -0,0 +1,33 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../../Base"
|
||||
|
||||
HBox {
|
||||
id: signInBox
|
||||
clickButtonOnEnter: "ok"
|
||||
|
||||
buttonModel: [
|
||||
{
|
||||
name: "ok",
|
||||
text: qsTr("Reset password from Riot"),
|
||||
iconName: "reset-password"
|
||||
},
|
||||
]
|
||||
|
||||
buttonCallbacks: ({
|
||||
ok: button => {
|
||||
Qt.openUrlExternally("https://riot.im/app/#/forgot_password")
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
HLabel {
|
||||
wrapMode: Text.Wrap
|
||||
text: qsTr(
|
||||
"Account recovery is not implemented yet. You can reset your " +
|
||||
"password using a client that supports it, like Riot."
|
||||
)
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
201
src/gui/Pages/AddAccount/SignIn.qml
Normal file
201
src/gui/Pages/AddAccount/SignIn.qml
Normal file
@@ -0,0 +1,201 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../../Base"
|
||||
|
||||
HBox {
|
||||
id: signInBox
|
||||
clickButtonOnEnter: "apply"
|
||||
|
||||
onFocusChanged: idField.forceActiveFocus()
|
||||
|
||||
buttonModel: [
|
||||
{
|
||||
name: "apply",
|
||||
text: qsTr("Sign in"),
|
||||
enabled: canSignIn,
|
||||
iconName: "sign-in",
|
||||
loading: loginFuture !== null,
|
||||
disableWhileLoading: false,
|
||||
},
|
||||
{ name: "cancel", text: qsTr("Cancel"), iconName: "cancel"},
|
||||
]
|
||||
|
||||
buttonCallbacks: ({
|
||||
apply: button => {
|
||||
if (loginFuture) loginFuture.cancel()
|
||||
|
||||
signInTimeout.restart()
|
||||
|
||||
errorMessage.text = ""
|
||||
|
||||
let args = [
|
||||
idField.text.trim(), passwordField.text,
|
||||
undefined, serverField.text.trim(),
|
||||
]
|
||||
|
||||
loginFuture = py.callCoro("login_client", args, userId => {
|
||||
signInTimeout.stop()
|
||||
errorMessage.text = ""
|
||||
loginFuture = null
|
||||
|
||||
py.callCoro(
|
||||
rememberAccount.checked ?
|
||||
"saved_accounts.add": "saved_accounts.delete",
|
||||
|
||||
[userId]
|
||||
)
|
||||
|
||||
pageLoader.showPage(
|
||||
"AccountSettings/AccountSettings", {userId}
|
||||
)
|
||||
|
||||
}, type => {
|
||||
loginFuture = null
|
||||
signInTimeout.stop()
|
||||
|
||||
let txt = qsTr("Invalid request or login type")
|
||||
|
||||
if (type === "MatrixForbidden")
|
||||
txt = qsTr("Invalid username or password")
|
||||
|
||||
if (type === "MatrixUserDeactivated")
|
||||
txt = qsTr("This account was deactivated")
|
||||
|
||||
errorMessage.text = txt
|
||||
})
|
||||
},
|
||||
|
||||
cancel: button => {
|
||||
if (! loginFuture) return
|
||||
|
||||
signInTimeout.stop()
|
||||
loginFuture.cancel()
|
||||
loginFuture = null
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
property var loginFuture: null
|
||||
|
||||
property string signInWith: "username"
|
||||
|
||||
readonly property bool canSignIn:
|
||||
serverField.text.trim() && idField.text.trim() && passwordField.text &&
|
||||
! serverField.error
|
||||
|
||||
|
||||
Timer {
|
||||
id: signInTimeout
|
||||
interval: 30 * 1000
|
||||
onTriggered: {
|
||||
errorMessage.text =
|
||||
serverField.knownServerChosen ?
|
||||
|
||||
qsTr("This server seems unavailable. Verify your inter" +
|
||||
"net connection or try again in a few minutes.") :
|
||||
|
||||
qsTr("This server seems unavailable. Verify the " +
|
||||
"entered URL, your internet connection or try " +
|
||||
"again in a few minutes.")
|
||||
}
|
||||
}
|
||||
|
||||
HRowLayout {
|
||||
visible: false // TODO
|
||||
spacing: theme.spacing * 1.25
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
Layout.topMargin: theme.spacing
|
||||
Layout.bottomMargin: Layout.topMargin
|
||||
|
||||
Repeater {
|
||||
model: ["username", "email", "phone"]
|
||||
|
||||
HButton {
|
||||
icon.name: modelData
|
||||
circle: true
|
||||
checked: signInWith === modelData
|
||||
enabled: modelData === "username"
|
||||
autoExclusive: true
|
||||
onClicked: signInWith = modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HTextField {
|
||||
id: idField
|
||||
placeholderText: qsTr(
|
||||
signInWith === "email" ? "Email" :
|
||||
signInWith === "phone" ? "Phone" :
|
||||
"Username"
|
||||
)
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
HTextField {
|
||||
id: passwordField
|
||||
placeholderText: qsTr("Password")
|
||||
echoMode: HTextField.Password
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
HTextField {
|
||||
id: serverField
|
||||
placeholderText: qsTr("Homeserver URL")
|
||||
text: "https://matrix.org"
|
||||
error: ! /.+:\/\/.+/.test(cleanText)
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
|
||||
readonly property string cleanText: text.toLowerCase().trim()
|
||||
|
||||
// 2019-11-11 https://www.hello-matrix.net/public_servers.php
|
||||
readonly property var knownServers: [
|
||||
"https://matrix.org",
|
||||
"https://chat.weho.st",
|
||||
"https://tchncs.de",
|
||||
"https://chat.privacytools.io",
|
||||
"https://hackerspaces.be",
|
||||
"https://matrix.allmende.io",
|
||||
"https://feneas.org",
|
||||
"https://junta.pl",
|
||||
"https://perthchat.org",
|
||||
"https://matrix.tedomum.net",
|
||||
"https://converser.eu",
|
||||
"https://ru-matrix.org",
|
||||
"https://matrix.sibnsk.net",
|
||||
"https://alternanet.fr",
|
||||
]
|
||||
|
||||
readonly property bool knownServerChosen:
|
||||
knownServers.includes(cleanText)
|
||||
}
|
||||
|
||||
HCheckBox {
|
||||
id: rememberAccount
|
||||
checked: true
|
||||
text: qsTr("Remember my account")
|
||||
subtitle.text: qsTr(
|
||||
"An access token will be stored on this device to " +
|
||||
"automatically sign you in."
|
||||
)
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
HLabel {
|
||||
id: errorMessage
|
||||
wrapMode: Text.Wrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
color: theme.colors.errorText
|
||||
|
||||
visible: Layout.maximumHeight > 0
|
||||
Layout.maximumHeight: text ? implicitHeight : 0
|
||||
Behavior on Layout.maximumHeight { HNumberAnimation {} }
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
24
src/gui/Pages/AddChat/AddChat.qml
Normal file
24
src/gui/Pages/AddChat/AddChat.qml
Normal file
@@ -0,0 +1,24 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../../Base"
|
||||
|
||||
HPage {
|
||||
id: addChatPage
|
||||
|
||||
|
||||
property string userId
|
||||
|
||||
readonly property var account:
|
||||
utils.getItem(modelSources["Account"] || [], "user_id", userId)
|
||||
|
||||
|
||||
HTabContainer {
|
||||
tabModel: [
|
||||
qsTr("Direct chat"), qsTr("Join room"), qsTr("Create room"),
|
||||
]
|
||||
|
||||
DirectChat { Component.onCompleted: forceActiveFocus() }
|
||||
JoinRoom {}
|
||||
CreateRoom {}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user