Ask for server URL before showing sign in box
Contact the server's .well-known API before anything to get available login flows instead of blindly assuming it will be m.login.password, and to get the server's real URL instead of requiring users to remember that e.g. it's "chat.privacytools.io" and not just "privacytools.io" despite user IDs making it look like so. The server field will also now remember the last accepted URL.
This commit is contained in:
parent
b94d1e8168
commit
d7907db547
8
TODO.md
8
TODO.md
|
@ -1,7 +1,11 @@
|
||||||
# TODO
|
# TODO
|
||||||
|
|
||||||
- Image viewer:
|
- fallback page
|
||||||
- hflickable: support kinetic scrolling disabler
|
- SSO page
|
||||||
|
- sever list
|
||||||
|
- cursor shape in HBox/HTabbedBox pages over fields
|
||||||
|
- login with account already added → infinite spinner in room list
|
||||||
|
- verify onKeyboardAccept/Cancel things respect button enabled state
|
||||||
|
|
||||||
- global presence control
|
- global presence control
|
||||||
|
|
||||||
|
|
|
@ -6,14 +6,14 @@ import os
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, DefaultDict, Dict, List, Optional
|
from typing import Any, DefaultDict, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from appdirs import AppDirs
|
from appdirs import AppDirs
|
||||||
|
|
||||||
import nio
|
import nio
|
||||||
|
|
||||||
from . import __app_name__
|
from . import __app_name__
|
||||||
from .errors import MatrixError
|
from .errors import MatrixError, MatrixForbidden, MatrixNotFound
|
||||||
from .matrix_client import MatrixClient
|
from .matrix_client import MatrixClient
|
||||||
from .media_cache import MediaCache
|
from .media_cache import MediaCache
|
||||||
from .models import SyncId
|
from .models import SyncId
|
||||||
|
@ -129,6 +129,30 @@ class Backend:
|
||||||
|
|
||||||
# Clients management
|
# Clients management
|
||||||
|
|
||||||
|
async def server_info(self, homeserver: str) -> Tuple[str, List[str]]:
|
||||||
|
"""Return server's real URL and supported login flows.
|
||||||
|
|
||||||
|
Retrieving the real URL uses the `.well-known` API.
|
||||||
|
Possible login methods include `m.login.password` or `m.login.sso`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
client = MatrixClient(self, homeserver=homeserver)
|
||||||
|
|
||||||
|
try:
|
||||||
|
homeserver = (await client.discovery_info()).homeserver_url
|
||||||
|
except (MatrixNotFound, MatrixForbidden):
|
||||||
|
# This is either already the real URL, or an invalid URL.
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
await client.close()
|
||||||
|
client = MatrixClient(self, homeserver=homeserver)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return (homeserver, (await client.login_info()).flows)
|
||||||
|
finally:
|
||||||
|
await client.close()
|
||||||
|
|
||||||
|
|
||||||
async def login_client(self,
|
async def login_client(self,
|
||||||
user: str,
|
user: str,
|
||||||
password: str,
|
password: str,
|
||||||
|
|
|
@ -152,11 +152,13 @@ class MatrixClient(nio.AsyncClient):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(
|
||||||
backend,
|
self,
|
||||||
user: str,
|
backend,
|
||||||
homeserver: str = "https://matrix.org",
|
user: str = "",
|
||||||
device_id: Optional[str] = None) -> None:
|
homeserver: str = "https://matrix.org",
|
||||||
|
device_id: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
if not urlparse(homeserver).scheme:
|
if not urlparse(homeserver).scheme:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
|
|
|
@ -5,7 +5,6 @@ import QtQuick.Layouts 1.12
|
||||||
|
|
||||||
HFlickableColumnPage {
|
HFlickableColumnPage {
|
||||||
implicitWidth: Math.min(parent.width, theme.controls.box.defaultWidth)
|
implicitWidth: Math.min(parent.width, theme.controls.box.defaultWidth)
|
||||||
implicitHeight: Math.min(parent.height, flickable.contentHeight)
|
|
||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
color: theme.controls.box.background
|
color: theme.controls.box.background
|
||||||
|
|
|
@ -18,6 +18,7 @@ HPage {
|
||||||
|
|
||||||
|
|
||||||
implicitWidth: theme.controls.box.defaultWidth
|
implicitWidth: theme.controls.box.defaultWidth
|
||||||
|
implicitHeight: contentHeight + implicitHeaderHeight + implicitFooterHeight
|
||||||
contentHeight:
|
contentHeight:
|
||||||
flickable.contentHeight + flickable.topMargin + flickable.bottomMargin
|
flickable.contentHeight + flickable.topMargin + flickable.bottomMargin
|
||||||
|
|
||||||
|
|
|
@ -4,22 +4,48 @@ import QtQuick 2.12
|
||||||
import QtQuick.Layouts 1.12
|
import QtQuick.Layouts 1.12
|
||||||
import "../../Base"
|
import "../../Base"
|
||||||
|
|
||||||
HPage {
|
HSwipeView {
|
||||||
id: page
|
id: swipeView
|
||||||
|
clip: true
|
||||||
|
interactive: currentIndex !== 0 || signIn.serverUrl
|
||||||
|
onCurrentItemChanged: if (currentIndex === 0) serverBrowser.takeFocus()
|
||||||
|
Component.onCompleted: serverBrowser.takeFocus()
|
||||||
|
|
||||||
HTabbedBox {
|
HPage {
|
||||||
anchors.centerIn: parent
|
id: serverPage
|
||||||
width: Math.min(implicitWidth, page.availableWidth)
|
|
||||||
height: Math.min(implicitHeight, page.availableHeight)
|
|
||||||
|
|
||||||
header: HTabBar {
|
ServerBrowser {
|
||||||
HTabButton { text: qsTr("Sign in") }
|
id: serverBrowser
|
||||||
HTabButton { text: qsTr("Register") }
|
anchors.centerIn: parent
|
||||||
HTabButton { text: qsTr("Reset") }
|
width: Math.min(implicitWidth, serverPage.availableWidth)
|
||||||
|
height: Math.min(implicitHeight, serverPage.availableHeight)
|
||||||
|
onAccepted: swipeView.currentIndex = 1
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SignIn {}
|
HPage {
|
||||||
Register {}
|
id: tabPage
|
||||||
Reset {}
|
|
||||||
|
HTabbedBox {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: Math.min(implicitWidth, tabPage.availableWidth)
|
||||||
|
height: Math.min(implicitHeight, tabPage.availableHeight)
|
||||||
|
|
||||||
|
header: HTabBar {
|
||||||
|
HTabButton { text: qsTr("Sign in") }
|
||||||
|
HTabButton { text: qsTr("Register") }
|
||||||
|
HTabButton { text: qsTr("Reset") }
|
||||||
|
}
|
||||||
|
|
||||||
|
SignIn {
|
||||||
|
id: signIn
|
||||||
|
serverUrl: serverBrowser.acceptedUrl
|
||||||
|
displayUrl: serverBrowser.acceptedUserUrl
|
||||||
|
onExitRequested: swipeView.currentIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Register {}
|
||||||
|
Reset {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
155
src/gui/Pages/AddAccount/ServerBrowser.qml
Normal file
155
src/gui/Pages/AddAccount/ServerBrowser.qml
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
|
||||||
|
import QtQuick 2.12
|
||||||
|
import QtQuick.Layouts 1.12
|
||||||
|
import "../../Base"
|
||||||
|
import "../../Base/Buttons"
|
||||||
|
import "../../PythonBridge"
|
||||||
|
|
||||||
|
HBox {
|
||||||
|
id: page
|
||||||
|
|
||||||
|
property string acceptedUserUrl: ""
|
||||||
|
property string acceptedUrl: ""
|
||||||
|
property var loginFlows: ["m.login.password"]
|
||||||
|
|
||||||
|
property string saveName: "serverBrowser"
|
||||||
|
property var saveProperties: ["acceptedUserUrl"]
|
||||||
|
property Future connectFuture: null
|
||||||
|
|
||||||
|
signal accepted()
|
||||||
|
|
||||||
|
function takeFocus() { serverField.item.forceActiveFocus() }
|
||||||
|
|
||||||
|
function connect() {
|
||||||
|
if (connectFuture) connectFuture.cancel()
|
||||||
|
connectTimeout.restart()
|
||||||
|
|
||||||
|
const args = [serverField.item.cleanText]
|
||||||
|
|
||||||
|
connectFuture = py.callCoro("server_info", args, ([url, flows]) => {
|
||||||
|
connectTimeout.stop()
|
||||||
|
errorMessage.text = ""
|
||||||
|
|
||||||
|
connectFuture = null
|
||||||
|
acceptedUrl = url
|
||||||
|
acceptedUserUrl = args[0]
|
||||||
|
loginFlows = flows
|
||||||
|
accepted()
|
||||||
|
|
||||||
|
}, (type, args, error, traceback, uuid) => {
|
||||||
|
connectTimeout.stop()
|
||||||
|
connectFuture = null
|
||||||
|
|
||||||
|
let text = qsTr("Unexpected error: %1 [%2]").arg(type).arg(args)
|
||||||
|
|
||||||
|
type === "MatrixNotFound" ?
|
||||||
|
text = qsTr("Invalid homeserver address") :
|
||||||
|
|
||||||
|
type.startsWith("Matrix") ?
|
||||||
|
text = qsTr("Error contacting server: %1").arg(type) :
|
||||||
|
|
||||||
|
utils.showError(type, traceback, uuid)
|
||||||
|
|
||||||
|
errorMessage.text = text
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
if (page.connectFuture) return
|
||||||
|
|
||||||
|
connectTimeout.stop()
|
||||||
|
connectFuture.cancel()
|
||||||
|
connectFuture = null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
footer: AutoDirectionLayout {
|
||||||
|
ApplyButton {
|
||||||
|
id: applyButton
|
||||||
|
enabled: serverField.item.cleanText && ! serverField.item.error
|
||||||
|
text: qsTr("Connect")
|
||||||
|
icon.name: "server-connect"
|
||||||
|
loading: page.connectFuture !== null
|
||||||
|
disableWhileLoading: false
|
||||||
|
onClicked: page.connect()
|
||||||
|
}
|
||||||
|
|
||||||
|
CancelButton {
|
||||||
|
id: cancelButton
|
||||||
|
enabled: page.connectFuture !== null
|
||||||
|
onClicked: page.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeyboardAccept: if (applyButton.enabled) page.connect()
|
||||||
|
onKeyboardCancel: if (cancelButton.enabled) page.cancel()
|
||||||
|
onAccepted: window.saveState(this)
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: connectTimeout
|
||||||
|
interval: 30 * 1000
|
||||||
|
onTriggered: {
|
||||||
|
errorMessage.text =
|
||||||
|
serverField.knownServerChosen ?
|
||||||
|
|
||||||
|
qsTr("This homeserver seems unavailable. Verify your inter" +
|
||||||
|
"net connection or try again in a few minutes.") :
|
||||||
|
|
||||||
|
qsTr("This homeserver seems unavailable. Verify the " +
|
||||||
|
"entered address, your internet connection or try " +
|
||||||
|
"again in a few minutes.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HLabeledItem {
|
||||||
|
id: serverField
|
||||||
|
|
||||||
|
// 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(item.cleanText)
|
||||||
|
|
||||||
|
label.text: qsTr("Homeserver:")
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
HTextField {
|
||||||
|
readonly property string cleanText: text.toLowerCase().trim()
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
error: ! /https?:\/\/.+/.test(cleanText)
|
||||||
|
defaultText:
|
||||||
|
window.getState(page, "acceptedUserUrl", "https://matrix.org")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HLabel {
|
||||||
|
id: errorMessage
|
||||||
|
wrapMode: HLabel.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
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,33 +8,42 @@ import "../../Base/Buttons"
|
||||||
HFlickableColumnPage {
|
HFlickableColumnPage {
|
||||||
id: page
|
id: page
|
||||||
|
|
||||||
|
enum Security { Insecure, LocalHttp, Secure }
|
||||||
|
|
||||||
|
property string serverUrl
|
||||||
|
property string displayUrl: serverUrl
|
||||||
property var loginFuture: null
|
property var loginFuture: null
|
||||||
|
|
||||||
property string signInWith: "username"
|
signal exitRequested()
|
||||||
|
|
||||||
readonly property bool canSignIn:
|
readonly property int security:
|
||||||
serverField.item.text.trim() && idField.item.text.trim() &&
|
serverUrl.startsWith("https://") ?
|
||||||
passwordField.item.text && ! serverField.item.error
|
SignIn.Security.Secure :
|
||||||
|
|
||||||
|
["//localhost", "//127.0.0.1", "//:1"].includes(
|
||||||
|
serverUrl.split(":")[1],
|
||||||
|
) ?
|
||||||
|
SignIn.Security.LocalHttp :
|
||||||
|
|
||||||
|
SignIn.Security.Insecure
|
||||||
|
|
||||||
function takeFocus() { idField.item.forceActiveFocus() }
|
function takeFocus() { idField.item.forceActiveFocus() }
|
||||||
|
|
||||||
function signIn() {
|
function signIn() {
|
||||||
if (page.loginFuture) page.loginFuture.cancel()
|
if (page.loginFuture) page.loginFuture.cancel()
|
||||||
|
|
||||||
signInTimeout.restart()
|
|
||||||
|
|
||||||
errorMessage.text = ""
|
errorMessage.text = ""
|
||||||
|
|
||||||
const args = [
|
const args = [
|
||||||
idField.item.text.trim(), passwordField.item.text,
|
idField.item.text.trim(), passwordField.item.text,
|
||||||
undefined, serverField.item.text.trim(),
|
undefined, page.serverUrl,
|
||||||
]
|
]
|
||||||
|
|
||||||
page.loginFuture = py.callCoro("login_client", args, userId => {
|
page.loginFuture = py.callCoro("login_client", args, userId => {
|
||||||
signInTimeout.stop()
|
|
||||||
errorMessage.text = ""
|
errorMessage.text = ""
|
||||||
page.loginFuture = null
|
page.loginFuture = null
|
||||||
|
|
||||||
|
print(rememberAccount.checked)
|
||||||
py.callCoro(
|
py.callCoro(
|
||||||
rememberAccount.checked ?
|
rememberAccount.checked ?
|
||||||
"saved_accounts.add": "saved_accounts.delete",
|
"saved_accounts.add": "saved_accounts.delete",
|
||||||
|
@ -48,7 +57,6 @@ HFlickableColumnPage {
|
||||||
|
|
||||||
}, (type, args, error, traceback, uuid) => {
|
}, (type, args, error, traceback, uuid) => {
|
||||||
page.loginFuture = null
|
page.loginFuture = null
|
||||||
signInTimeout.stop()
|
|
||||||
|
|
||||||
let txt = qsTr(
|
let txt = qsTr(
|
||||||
"Invalid request, login type or unknown error: %1",
|
"Invalid request, login type or unknown error: %1",
|
||||||
|
@ -67,17 +75,23 @@ HFlickableColumnPage {
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancel() {
|
function cancel() {
|
||||||
if (! page.loginFuture) return
|
if (! page.loginFuture) {
|
||||||
|
page.exitRequested()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
signInTimeout.stop()
|
|
||||||
page.loginFuture.cancel()
|
page.loginFuture.cancel()
|
||||||
page.loginFuture = null
|
page.loginFuture = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
flickable.topMargin: theme.spacing * 1.5
|
||||||
|
flickable.bottomMargin: flickable.topMargin
|
||||||
|
|
||||||
footer: AutoDirectionLayout {
|
footer: AutoDirectionLayout {
|
||||||
ApplyButton {
|
ApplyButton {
|
||||||
enabled: page.canSignIn
|
id: applyButton
|
||||||
|
enabled: idField.item.text.trim() && passwordField.item.text
|
||||||
text: qsTr("Sign in")
|
text: qsTr("Sign in")
|
||||||
icon.name: "sign-in"
|
icon.name: "sign-in"
|
||||||
loading: page.loginFuture !== null
|
loading: page.loginFuture !== null
|
||||||
|
@ -90,54 +104,39 @@ HFlickableColumnPage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onKeyboardAccept: page.signIn()
|
onKeyboardAccept: if (applyButton.enabled) page.signIn()
|
||||||
onKeyboardCancel: page.cancel()
|
onKeyboardCancel: page.cancel()
|
||||||
|
|
||||||
Timer {
|
HButton {
|
||||||
id: signInTimeout
|
icon.name: "sign-in-" + (
|
||||||
interval: 30 * 1000
|
page.security === SignIn.Security.Insecure ? "insecure" :
|
||||||
onTriggered: {
|
page.security === SignIn.Security.LocalHttp ? "local-http" :
|
||||||
errorMessage.text =
|
"secure"
|
||||||
serverField.knownServerChosen ?
|
)
|
||||||
|
|
||||||
qsTr("This server seems unavailable. Verify your inter" +
|
icon.color:
|
||||||
"net connection or try again in a few minutes.") :
|
page.security === SignIn.Security.Insecure ?
|
||||||
|
theme.colors.negativeBackground :
|
||||||
|
|
||||||
qsTr("This server seems unavailable. Verify the " +
|
page.security === SignIn.Security.LocalHttp ?
|
||||||
"entered URL, your internet connection or try " +
|
theme.colors.middleBackground :
|
||||||
"again in a few minutes.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HRowLayout {
|
theme.colors.positiveBackground
|
||||||
visible: false // TODO
|
|
||||||
spacing: theme.spacing * 1.25
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
|
|
||||||
Layout.topMargin: theme.spacing
|
text:
|
||||||
Layout.bottomMargin: Layout.topMargin
|
page.security === SignIn.Security.Insecure ?
|
||||||
|
page.serverUrl :
|
||||||
|
page.displayUrl.replace(/^(https?:\/\/)?(www\.)?/, "")
|
||||||
|
|
||||||
Repeater {
|
onClicked: page.exitRequested()
|
||||||
model: ["username", "email", "phone"]
|
|
||||||
|
|
||||||
HButton {
|
Layout.alignment: Qt.AlignCenter
|
||||||
icon.name: modelData
|
Layout.maximumWidth: parent.width
|
||||||
circle: true
|
|
||||||
checked: page.signInWith === modelData
|
|
||||||
enabled: modelData === "username"
|
|
||||||
autoExclusive: true
|
|
||||||
onClicked: page.signInWith = modelData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
HLabeledItem {
|
HLabeledItem {
|
||||||
id: idField
|
id: idField
|
||||||
label.text: qsTr(
|
label.text: qsTr("Username:")
|
||||||
page.signInWith === "email" ? "Email:" :
|
|
||||||
page.signInWith === "phone" ? "Phone:" :
|
|
||||||
"Username:"
|
|
||||||
)
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
@ -158,43 +157,6 @@ HFlickableColumnPage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HLabeledItem {
|
|
||||||
id: serverField
|
|
||||||
|
|
||||||
// 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(item.cleanText)
|
|
||||||
|
|
||||||
label.text: qsTr("Homeserver:")
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
HTextField {
|
|
||||||
readonly property string cleanText: text.toLowerCase().trim()
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
text: "https://matrix.org"
|
|
||||||
error: ! /.+:\/\/.+/.test(cleanText)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HCheckBox {
|
HCheckBox {
|
||||||
id: rememberAccount
|
id: rememberAccount
|
||||||
checked: true
|
checked: true
|
||||||
|
@ -206,7 +168,6 @@ HFlickableColumnPage {
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: theme.spacing / 2
|
Layout.topMargin: theme.spacing / 2
|
||||||
Layout.bottomMargin: Layout.topMargin
|
|
||||||
}
|
}
|
||||||
|
|
||||||
HLabel {
|
HLabel {
|
||||||
|
|
|
@ -440,6 +440,7 @@ QtObject {
|
||||||
|
|
||||||
flickable.maximumFlickVelocity = 5000
|
flickable.maximumFlickVelocity = 5000
|
||||||
|
|
||||||
|
|
||||||
flickable.flickDeceleration = Math.max(
|
flickable.flickDeceleration = Math.max(
|
||||||
goFaster ? normalDecel : -Infinity,
|
goFaster ? normalDecel : -Infinity,
|
||||||
Math.abs(normalDecel * magicNumber * pages),
|
Math.abs(normalDecel * magicNumber * pages),
|
||||||
|
|
3
src/icons/thin/server-connect.svg
Normal file
3
src/icons/thin/server-connect.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m12 0c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm5.73 20.183c-.106-.526-.236-1.046-.398-1.555.897-.811.86-2.197-.072-2.883.582-2.318.61-4.849-.002-7.288.186-.111.348-.254.475-.425 1.466.412 2.831 1.066 4.052 1.911.14.665.215 1.352.215 2.057 0 3.382-1.692 6.372-4.27 8.183zm-15.73-8.183c0-.855.12-1.682.323-2.475.699-.044 1.393-.04 2.147.032l.04.226c-.921.775-1.75 1.661-2.487 2.674zm3.183-1.19c.848.643 2.083.436 2.662-.49 2.898 1.06 5.339 3.077 6.94 5.666-.766.775-.756 1.998.019 2.695-.681 1.231-1.548 2.345-2.56 3.307-4.902.117-8.924-3.262-9.969-7.697.772-1.316 1.755-2.494 2.908-3.481zm2.886-1.901c1.991-.974 4.155-1.432 6.324-1.377.305.611.93 1.076 1.666 1.166h.006c.557 2.157.583 4.472.029 6.7l-.223.023c-1.724-2.825-4.433-5.131-7.763-6.301zm6.062 12.857c.702-.817 1.311-1.695 1.813-2.627l.27-.008c.172.562.308 1.139.408 1.729-.777.406-1.612.714-2.491.906zm7.103-13.598c-1.009-.56-2.076-1.002-3.189-1.311-.108-.995-1.041-1.824-2.119-1.816-.552-1.019-1.232-1.975-2.024-2.854 3.321.642 6.061 2.93 7.332 5.981zm-6.472-2.708c-.257.22-.443.515-.524.858-2.456-.03-4.778.526-6.848 1.565-.85-.638-2.07-.421-2.646.483-.728-.076-1.379-.092-2.024-.072 1.476-3.683 5.076-6.294 9.28-6.294h.001c1.097.994 2.034 2.16 2.761 3.46z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
3
src/icons/thin/sign-in-insecure.svg
Normal file
3
src/icons/thin/sign-in-insecure.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m8 9v-3c0-2.206 1.794-4 4-4s4 1.794 4 4v3h2v-3c0-3.313-2.687-6-6-6s-6 2.687-6 6v3zm.746 2h2.831l-8.577 8.787v-2.9zm12.254 1.562v-1.562h-1.37l-12.69 13h2.894zm-6.844-1.562-11.156 11.431v1.569h1.361l12.689-13zm6.844 7.13v-2.927l-8.586 8.797h2.858zm-3.149 5.87h3.149v-3.226zm-11.685-13h-3.166v3.244z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 404 B |
3
src/icons/thin/sign-in-local-http.svg
Normal file
3
src/icons/thin/sign-in-local-http.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m8 9v-3c0-2.206 1.794-4 4-4s4 1.794 4 4v3h2v-3c0-3.313-2.687-6-6-6s-6 2.687-6 6v3zm.746 2h2.831l-8.577 8.787v-2.9zm12.254 1.562v-1.562h-1.37l-12.69 13h2.894zm-6.844-1.562-11.156 11.431v1.569h1.361l12.689-13zm6.844 7.13v-2.927l-8.586 8.797h2.858zm-3.149 5.87h3.149v-3.226zm-11.685-13h-3.166v3.244z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 404 B |
3
src/icons/thin/sign-in-secure.svg
Normal file
3
src/icons/thin/sign-in-secure.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m21 21v3h-18v-3zm-13-15c0-2.206 1.795-4 4-4s4 1.794 4 4v3h2v-3c0-3.313-2.687-6-6-6s-6 2.687-6 6v3h2zm13 8v-3h-18v3zm0 5v-3h-18v3z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 237 B |
Loading…
Reference in New Issue
Block a user