Add a public server list to the initial login page
This commit is contained in:
parent
1a6273681d
commit
2fa8b2c5f9
4
TODO.md
4
TODO.md
|
@ -1,5 +1,9 @@
|
||||||
# TODO
|
# TODO
|
||||||
|
|
||||||
|
- clicking cancel on SSO "waiting" box doesn't do anything the first time
|
||||||
|
- spam alt+shift+a when starting app on server browser → segfault
|
||||||
|
- remove items.Device
|
||||||
|
- register tab for sso servers?
|
||||||
- sever list
|
- sever list
|
||||||
- cursor shape in HBox/HTabbedBox pages over fields
|
- cursor shape in HBox/HTabbedBox pages over fields
|
||||||
- login with account already added → infinite spinner in room list
|
- login with account already added → infinite spinner in room list
|
||||||
|
|
|
@ -2,12 +2,17 @@
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging as log
|
import logging as log
|
||||||
|
import math
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, DefaultDict, Dict, List, Optional, Tuple
|
from typing import Any, DefaultDict, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
from appdirs import AppDirs
|
from appdirs import AppDirs
|
||||||
|
|
||||||
import nio
|
import nio
|
||||||
|
@ -18,7 +23,7 @@ from .matrix_client import MatrixClient
|
||||||
from .media_cache import MediaCache
|
from .media_cache import MediaCache
|
||||||
from .models import SyncId
|
from .models import SyncId
|
||||||
from .models.filters import FieldSubstringFilter
|
from .models.filters import FieldSubstringFilter
|
||||||
from .models.items import Account, Event
|
from .models.items import Account, Event, Homeserver, PingStatus
|
||||||
from .models.model import Model
|
from .models.model import Model
|
||||||
from .models.model_store import ModelStore
|
from .models.model_store import ModelStore
|
||||||
from .presence import Presence
|
from .presence import Presence
|
||||||
|
@ -107,6 +112,9 @@ class Backend:
|
||||||
self._sso_server: Optional[SSOServer] = None
|
self._sso_server: Optional[SSOServer] = None
|
||||||
self._sso_server_task: Optional[asyncio.Future] = None
|
self._sso_server_task: Optional[asyncio.Future] = None
|
||||||
|
|
||||||
|
self._ping_tasks: Dict[str, asyncio.Future] = {}
|
||||||
|
self._stability_tasks: Dict[str, asyncio.Future] = {}
|
||||||
|
|
||||||
self.profile_cache: Dict[str, nio.ProfileGetResponse] = {}
|
self.profile_cache: Dict[str, nio.ProfileGetResponse] = {}
|
||||||
self.get_profile_locks: DefaultDict[str, asyncio.Lock] = \
|
self.get_profile_locks: DefaultDict[str, asyncio.Lock] = \
|
||||||
DefaultDict(asyncio.Lock) # {user_id: lock}
|
DefaultDict(asyncio.Lock) # {user_id: lock}
|
||||||
|
@ -474,3 +482,139 @@ class Backend:
|
||||||
|
|
||||||
if device.ed25519 == ed25519_key:
|
if device.ed25519 == ed25519_key:
|
||||||
client.blacklist_device(device)
|
client.blacklist_device(device)
|
||||||
|
|
||||||
|
|
||||||
|
async def _ping_homeserver(
|
||||||
|
self, session: aiohttp.ClientSession, homeserver_url: str,
|
||||||
|
) -> None:
|
||||||
|
"""Ping a homeserver present in our model and set its `ping` field."""
|
||||||
|
|
||||||
|
item = self.models["homeservers"][homeserver_url]
|
||||||
|
times = []
|
||||||
|
|
||||||
|
for i in range(16):
|
||||||
|
start = time.time()
|
||||||
|
|
||||||
|
try:
|
||||||
|
await session.get(f"{homeserver_url}/_matrix/client/versions")
|
||||||
|
except Exception as err:
|
||||||
|
log.warning("Failed pinging %s: %r", homeserver_url, err)
|
||||||
|
item.status = PingStatus.Failed
|
||||||
|
return
|
||||||
|
|
||||||
|
times.append(round((time.time() - start) * 1000))
|
||||||
|
|
||||||
|
if i == 7 or i == 15:
|
||||||
|
item.set_fields(
|
||||||
|
ping=sum(times) // len(times), status=PingStatus.Done,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _get_homeserver_stability(
|
||||||
|
self,
|
||||||
|
session: aiohttp.ClientSession,
|
||||||
|
homeserver_url: str,
|
||||||
|
uptimerobot_id: int,
|
||||||
|
) -> None:
|
||||||
|
api = "https://matrixservers.anchel.nl/api/getMonitor/wkMJmFGvo2?m={}"
|
||||||
|
|
||||||
|
response = await session.get(api.format(uptimerobot_id))
|
||||||
|
logs = (await response.json())["monitor"]["logs"]
|
||||||
|
stability = 100.0
|
||||||
|
|
||||||
|
for period in logs:
|
||||||
|
started_at = datetime.fromtimestamp(period["time"])
|
||||||
|
time_since_now = datetime.now() - started_at
|
||||||
|
|
||||||
|
if time_since_now.days > 30 or period["class"] != "danger":
|
||||||
|
continue
|
||||||
|
|
||||||
|
lasted_hours, lasted_mins = [
|
||||||
|
int(x.split()[0]) for x in period["duration"].split(", ")
|
||||||
|
]
|
||||||
|
lasted_mins += lasted_hours * 60
|
||||||
|
stability -= (
|
||||||
|
(lasted_mins * stability / 1000) /
|
||||||
|
max(1, time_since_now.days / 3)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.models["homeservers"][homeserver_url].stability = stability
|
||||||
|
|
||||||
|
|
||||||
|
async def _add_homeserver_item(
|
||||||
|
self,
|
||||||
|
session: aiohttp.ClientSession,
|
||||||
|
homeserver_url: str,
|
||||||
|
uptimerobot_id: int,
|
||||||
|
**fields,
|
||||||
|
) -> Homeserver:
|
||||||
|
"""Add homeserver to our model & start info-gathering tasks."""
|
||||||
|
|
||||||
|
if not re.match(r"^https?://.+", homeserver_url):
|
||||||
|
homeserver_url = f"https://{homeserver_url}"
|
||||||
|
|
||||||
|
if fields.get("country") == "USA":
|
||||||
|
fields["country"] = "United States"
|
||||||
|
|
||||||
|
if homeserver_url in self._ping_tasks:
|
||||||
|
self._ping_tasks[homeserver_url].cancel()
|
||||||
|
|
||||||
|
if homeserver_url in self._stability_tasks:
|
||||||
|
self._stability_tasks[homeserver_url].cancel()
|
||||||
|
|
||||||
|
item = Homeserver(id=homeserver_url, **fields)
|
||||||
|
self.models["homeservers"][homeserver_url] = item
|
||||||
|
|
||||||
|
self._ping_tasks[homeserver_url] = asyncio.ensure_future(
|
||||||
|
self._ping_homeserver(session, homeserver_url),
|
||||||
|
)
|
||||||
|
|
||||||
|
self._stability_tasks[homeserver_url] = asyncio.ensure_future(
|
||||||
|
self._get_homeserver_stability(
|
||||||
|
session, item.id, uptimerobot_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_homeservers(self) -> None:
|
||||||
|
"""Retrieve a list of public homeservers and add them to our model."""
|
||||||
|
|
||||||
|
api_list = "https://publiclist.anchel.nl/staticlist.json"
|
||||||
|
|
||||||
|
tmout = aiohttp.ClientTimeout(total=20)
|
||||||
|
session = aiohttp.ClientSession(raise_for_status=True, timeout=tmout)
|
||||||
|
response = await session.get(api_list)
|
||||||
|
data = (await response.json())["staticlist"]
|
||||||
|
|
||||||
|
await self._add_homeserver_item(
|
||||||
|
session = session,
|
||||||
|
homeserver_url = "https://matrix-client.matrix.org",
|
||||||
|
name = "matrix.org",
|
||||||
|
site_url = "https://matrix.org",
|
||||||
|
country = "Cloudflare",
|
||||||
|
uptimerobot_id = 783115140,
|
||||||
|
)
|
||||||
|
|
||||||
|
await self._add_homeserver_item(
|
||||||
|
session = session,
|
||||||
|
homeserver_url = "https://mozilla.modular.im",
|
||||||
|
name = "mozilla.org",
|
||||||
|
site_url = "https://mozilla.org",
|
||||||
|
country = "United States",
|
||||||
|
uptimerobot_id = 784321494,
|
||||||
|
)
|
||||||
|
|
||||||
|
for server in data:
|
||||||
|
if server["homeserver"].startswith("http://"): # insecure server
|
||||||
|
continue
|
||||||
|
|
||||||
|
await self._add_homeserver_item(
|
||||||
|
session = session,
|
||||||
|
homeserver_url = server["homeserver"],
|
||||||
|
name = server["name"],
|
||||||
|
site_url = server["url"],
|
||||||
|
country = server["country"],
|
||||||
|
uptimerobot_id = server["uptrid"],
|
||||||
|
)
|
||||||
|
|
|
@ -30,6 +30,30 @@ class TypeSpecifier(AutoStrEnum):
|
||||||
MembershipChange = auto()
|
MembershipChange = auto()
|
||||||
|
|
||||||
|
|
||||||
|
class PingStatus(AutoStrEnum):
|
||||||
|
"""Enum for the status of a homeserver ping operation."""
|
||||||
|
|
||||||
|
Done = auto()
|
||||||
|
Pinging = auto()
|
||||||
|
Failed = auto()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Homeserver(ModelItem):
|
||||||
|
"""A homeserver we can connect to. The `id` field is the server's URL."""
|
||||||
|
|
||||||
|
id: str = field()
|
||||||
|
name: str = field()
|
||||||
|
site_url: str = field()
|
||||||
|
country: str = field()
|
||||||
|
ping: int = -1
|
||||||
|
status: PingStatus = PingStatus.Pinging
|
||||||
|
stability: float = -1
|
||||||
|
|
||||||
|
def __lt__(self, other: "Homeserver") -> bool:
|
||||||
|
return (self.name.lower(), self.id) < (other.name.lower(), other.id)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Account(ModelItem):
|
class Account(ModelItem):
|
||||||
"""A logged in matrix account."""
|
"""A logged in matrix account."""
|
||||||
|
|
|
@ -3,8 +3,9 @@
|
||||||
import QtQuick 2.12
|
import QtQuick 2.12
|
||||||
import QtQuick.Layouts 1.12
|
import QtQuick.Layouts 1.12
|
||||||
|
|
||||||
HFlickableColumnPage {
|
HColumnPage {
|
||||||
implicitWidth: Math.min(parent.width, theme.controls.box.defaultWidth)
|
implicitWidth: Math.min(parent.width, theme.controls.box.defaultWidth)
|
||||||
|
padding: theme.spacing
|
||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
color: theme.controls.box.background
|
color: theme.controls.box.background
|
||||||
|
|
|
@ -2,12 +2,13 @@
|
||||||
|
|
||||||
import QtQuick 2.12
|
import QtQuick 2.12
|
||||||
import QtQuick.Layouts 1.12
|
import QtQuick.Layouts 1.12
|
||||||
|
import "../.."
|
||||||
import "../../Base"
|
import "../../Base"
|
||||||
import "../../Base/Buttons"
|
import "../../Base/Buttons"
|
||||||
import "../../PythonBridge"
|
import "../../PythonBridge"
|
||||||
|
|
||||||
HBox {
|
HBox {
|
||||||
id: page
|
id: box
|
||||||
|
|
||||||
property string acceptedUserUrl: ""
|
property string acceptedUserUrl: ""
|
||||||
property string acceptedUrl: ""
|
property string acceptedUrl: ""
|
||||||
|
@ -15,21 +16,34 @@ HBox {
|
||||||
|
|
||||||
property string saveName: "serverBrowser"
|
property string saveName: "serverBrowser"
|
||||||
property var saveProperties: ["acceptedUserUrl"]
|
property var saveProperties: ["acceptedUserUrl"]
|
||||||
|
property string loadingIconStep: "server-ping-bad"
|
||||||
|
|
||||||
property Future connectFuture: null
|
property Future connectFuture: null
|
||||||
|
property Future fetchServersFuture: null
|
||||||
|
|
||||||
signal accepted()
|
signal accepted()
|
||||||
|
|
||||||
function takeFocus() { serverField.item.forceActiveFocus() }
|
function takeFocus() { serverField.item.field.forceActiveFocus() }
|
||||||
|
|
||||||
|
function fetchServers() {
|
||||||
|
fetchServersFuture = py.callCoro("fetch_homeservers", [], () => {
|
||||||
|
fetchServersFuture = null
|
||||||
|
}, (type, args, error, traceback) => {
|
||||||
|
fetchServersFuture = null
|
||||||
|
// TODO
|
||||||
|
print( traceback)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function connect() {
|
function connect() {
|
||||||
if (connectFuture) connectFuture.cancel()
|
if (connectFuture) connectFuture.cancel()
|
||||||
connectTimeout.restart()
|
connectTimeout.restart()
|
||||||
|
|
||||||
const args = [serverField.item.cleanText]
|
const args = [serverField.item.field.cleanText]
|
||||||
|
|
||||||
connectFuture = py.callCoro("server_info", args, ([url, flows]) => {
|
connectFuture = py.callCoro("server_info", args, ([url, flows]) => {
|
||||||
connectTimeout.stop()
|
connectTimeout.stop()
|
||||||
errorMessage.text = ""
|
serverField.errorLabel.text = ""
|
||||||
connectFuture = null
|
connectFuture = null
|
||||||
|
|
||||||
if (! (
|
if (! (
|
||||||
|
@ -39,7 +53,7 @@ HBox {
|
||||||
flows.includes("m.login.token")
|
flows.includes("m.login.token")
|
||||||
)
|
)
|
||||||
)) {
|
)) {
|
||||||
errorMessage.text =
|
serverField.errorLabel.text =
|
||||||
qsTr("No supported sign-in method for this homeserver.")
|
qsTr("No supported sign-in method for this homeserver.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -63,106 +77,137 @@ HBox {
|
||||||
|
|
||||||
py.showError(type, traceback, uuid)
|
py.showError(type, traceback, uuid)
|
||||||
|
|
||||||
errorMessage.text = text
|
serverField.errorLabel.text = text
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancel() {
|
|
||||||
if (page.connectFuture) return
|
|
||||||
|
|
||||||
connectTimeout.stop()
|
padding: 0
|
||||||
connectFuture.cancel()
|
implicitWidth: theme.controls.box.defaultWidth * 1.25
|
||||||
connectFuture = null
|
contentHeight: Math.min(
|
||||||
|
window.height,
|
||||||
|
Math.max(
|
||||||
|
serverList.contentHeight,
|
||||||
|
// busyIndicatorLoader.height + theme.spacing * 2, TODO
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
header: HLabel {
|
||||||
|
text: qsTr(
|
||||||
|
"Choose a homeserver to create your account on, or the " +
|
||||||
|
"server on which you made an account to sign in to:"
|
||||||
|
)
|
||||||
|
wrapMode: HLabel.Wrap
|
||||||
|
padding: theme.spacing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
footer: HLabeledItem {
|
||||||
|
id: serverField
|
||||||
|
|
||||||
footer: AutoDirectionLayout {
|
readonly property bool knownServerChosen:
|
||||||
ApplyButton {
|
serverList.model.find(item.cleanText) !== null
|
||||||
id: applyButton
|
|
||||||
enabled: serverField.item.cleanText && ! serverField.item.error
|
label.text: qsTr("Homeserver address:")
|
||||||
text: qsTr("Connect")
|
label.topPadding: theme.spacing
|
||||||
icon.name: "server-connect"
|
label.leftPadding: label.topPadding
|
||||||
loading: page.connectFuture !== null
|
label.rightPadding: label.topPadding
|
||||||
|
errorLabel.leftPadding: label.topPadding
|
||||||
|
errorLabel.rightPadding: label.topPadding
|
||||||
|
errorLabel.bottomPadding: label.topPadding
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.margins: theme.spacing
|
||||||
|
|
||||||
|
HRowLayout {
|
||||||
|
readonly property alias field: field
|
||||||
|
readonly property alias apply: apply
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
|
||||||
|
HTextField {
|
||||||
|
id: field
|
||||||
|
|
||||||
|
readonly property string cleanText:
|
||||||
|
text.toLowerCase().trim().replace(/\/+$/, "")
|
||||||
|
|
||||||
|
error: text && ! /https?:\/\/.+/.test(cleanText)
|
||||||
|
defaultText: window.getState(
|
||||||
|
box, "acceptedUserUrl", "",
|
||||||
|
)
|
||||||
|
placeholderText: "https://example.org"
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
}
|
||||||
|
|
||||||
|
HButton {
|
||||||
|
id: apply
|
||||||
|
enabled: field.cleanText && ! field.error
|
||||||
|
icon.name: "apply"
|
||||||
|
icon.color: theme.colors.positiveBackground
|
||||||
|
loading: box.connectFuture !== null
|
||||||
disableWhileLoading: false
|
disableWhileLoading: false
|
||||||
onClicked: page.connect()
|
onClicked: box.connect()
|
||||||
}
|
|
||||||
|
|
||||||
CancelButton {
|
Layout.fillHeight: true
|
||||||
id: cancelButton
|
}
|
||||||
enabled: page.connectFuture !== null
|
|
||||||
onClicked: page.cancel()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onKeyboardAccept: if (applyButton.enabled) page.connect()
|
onKeyboardAccept: if (serverField.item.apply.enabled) box.connect()
|
||||||
onKeyboardCancel: if (cancelButton.enabled) page.cancel()
|
|
||||||
onAccepted: window.saveState(this)
|
onAccepted: window.saveState(this)
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: connectTimeout
|
id: connectTimeout
|
||||||
interval: 30 * 1000
|
interval: 30 * 1000
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
errorMessage.text =
|
serverField.errorLabel.text =
|
||||||
serverField.knownServerChosen ?
|
serverField.knownServerChosen ?
|
||||||
|
|
||||||
qsTr("This homeserver seems unavailable. Verify your inter" +
|
qsTr("This homeserver seems unavailable. Verify your inter" +
|
||||||
"net connection or try again in a few minutes.") :
|
"net connection or try again later.") :
|
||||||
|
|
||||||
qsTr("This homeserver seems unavailable. Verify the " +
|
qsTr("This homeserver seems unavailable. Verify the " +
|
||||||
"entered address, your internet connection or try " +
|
"entered address, your internet connection or try " +
|
||||||
"again in a few minutes.")
|
"again later.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HLabeledItem {
|
Timer {
|
||||||
id: serverField
|
interval: 1000
|
||||||
|
running: fetchServersFuture === null && serverList.count === 0
|
||||||
|
repeat: true
|
||||||
|
triggeredOnStart: true
|
||||||
|
onTriggered: box.fetchServers()
|
||||||
|
}
|
||||||
|
|
||||||
// 2019-11-11 https://www.hello-matrix.net/public_servers.php
|
Timer {
|
||||||
readonly property var knownServers: [
|
interval: theme.animationDuration * 2
|
||||||
"https://matrix.org",
|
running: true
|
||||||
"https://chat.weho.st",
|
repeat: true
|
||||||
"https://tchncs.de",
|
onTriggered:
|
||||||
"https://chat.privacytools.io",
|
box.loadingIconStep = "server-ping-" + (
|
||||||
"https://hackerspaces.be",
|
box.loadingIconStep === "server-ping-bad" ? "medium" :
|
||||||
"https://matrix.allmende.io",
|
box.loadingIconStep === "server-ping-medium" ? "good" :
|
||||||
"https://feneas.org",
|
"bad"
|
||||||
"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:
|
HListView {
|
||||||
knownServers.includes(item.cleanText)
|
id: serverList
|
||||||
|
clip: true
|
||||||
|
model: ModelStore.get("homeservers")
|
||||||
|
|
||||||
label.text: qsTr("Homeserver:")
|
delegate: ServerDelegate {
|
||||||
|
width: serverList.width
|
||||||
Layout.fillWidth: true
|
loadingIconStep: box.loadingIconStep
|
||||||
|
onClicked: {
|
||||||
HTextField {
|
serverField.item.field.text = model.id
|
||||||
readonly property string cleanText:
|
serverField.item.apply.clicked()
|
||||||
text.toLowerCase().trim().replace(/\/+$/, "")
|
}
|
||||||
|
}
|
||||||
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
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
83
src/gui/Pages/AddAccount/ServerDelegate.qml
Normal file
83
src/gui/Pages/AddAccount/ServerDelegate.qml
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
|
||||||
|
import QtQuick 2.12
|
||||||
|
import QtQuick.Layouts 1.12
|
||||||
|
import "../../Base"
|
||||||
|
import "../../Base/HTile"
|
||||||
|
|
||||||
|
HTile {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string loadingIconStep
|
||||||
|
|
||||||
|
|
||||||
|
contentOpacity: model.status === "Failed" ? 0.3 : 1 // XXX
|
||||||
|
rightPadding: 0
|
||||||
|
|
||||||
|
contentItem: ContentRow {
|
||||||
|
tile: root
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
HIcon {
|
||||||
|
id: signalIcon
|
||||||
|
|
||||||
|
svgName:
|
||||||
|
model.status === "Failed" ? "server-ping-fail" :
|
||||||
|
model.status === "Pinging" ? root.loadingIconStep :
|
||||||
|
model.ping < 400 ? "server-ping-good" :
|
||||||
|
model.ping < 800 ? "server-ping-medium" :
|
||||||
|
"server-ping-bad"
|
||||||
|
|
||||||
|
colorize:
|
||||||
|
model.status === "Failed" ? theme.colors.negativeBackground :
|
||||||
|
model.status === "Pinging" ? theme.colors.accentBackground :
|
||||||
|
model.ping < 400 ? theme.colors.positiveBackground :
|
||||||
|
model.ping < 800 ? theme.colors.middleBackground :
|
||||||
|
theme.colors.negativeBackground
|
||||||
|
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.rightMargin: theme.spacing
|
||||||
|
|
||||||
|
Behavior on colorize { HColorAnimation {} }
|
||||||
|
}
|
||||||
|
|
||||||
|
HColumnLayout {
|
||||||
|
Layout.rightMargin: theme.spacing
|
||||||
|
|
||||||
|
TitleLabel {
|
||||||
|
text: model.name
|
||||||
|
}
|
||||||
|
|
||||||
|
SubtitleLabel {
|
||||||
|
tile: root
|
||||||
|
text: model.country
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TitleRightInfoLabel {
|
||||||
|
tile: root
|
||||||
|
font.pixelSize: theme.fontSize.normal
|
||||||
|
|
||||||
|
text:
|
||||||
|
model.stability === -1 ?
|
||||||
|
"" :
|
||||||
|
qsTr("%1%").arg(Math.max(0, parseInt(model.stability, 10)))
|
||||||
|
|
||||||
|
color:
|
||||||
|
model.stability >= 95 ? theme.colors.positiveText :
|
||||||
|
model.stability >= 85 ? theme.colors.warningText :
|
||||||
|
theme.colors.errorText
|
||||||
|
}
|
||||||
|
|
||||||
|
HButton {
|
||||||
|
icon.name: "server-visit-website"
|
||||||
|
toolTip.text: qsTr("Visit website")
|
||||||
|
backgroundColor: "transparent"
|
||||||
|
onClicked: Qt.openUrlExternally(model.site_url)
|
||||||
|
|
||||||
|
Layout.fillHeight: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on contentOpacity { HNumberAnimation {} }
|
||||||
|
}
|
|
@ -397,8 +397,8 @@ QtObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function round(floatNumber) {
|
function round(floatNumber, decimalDigits=2) {
|
||||||
return parseFloat(floatNumber.toFixed(2))
|
return parseFloat(floatNumber.toFixed(decimalDigits))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
<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>
|
|
Before Width: | Height: | Size: 1.3 KiB |
5
src/icons/thin/server-ping-bad.svg
Normal file
5
src/icons/thin/server-ping-bad.svg
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m12 3c-4.687 0-8.929 2.015-12 5.272l2.388 2.533c2.46-2.609 5.859-4.222 9.612-4.222s7.152 1.613 9.612 4.222l2.388-2.533c-3.071-3.257-7.313-5.272-12-5.272z" fill-opacity=".45"/>
|
||||||
|
<path d="m6.466 15.13c1.417-1.502 3.373-2.431 5.534-2.431s4.118.929 5.534 2.431l2.33-2.472c-2.012-2.134-4.793-3.454-7.864-3.454s-5.852 1.32-7.864 3.455z" fill-opacity=".5"/>
|
||||||
|
<path d="m8.213 16.984c.97-1.028 2.308-1.664 3.787-1.664s2.817.636 3.787 1.664l-3.787 4.016z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 558 B |
3
src/icons/thin/server-ping-fail.svg
Normal file
3
src/icons/thin/server-ping-fail.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="m23 20.168-8.185-8.187 8.185-8.174-2.832-2.807-8.182 8.179-8.176-8.179-2.81 2.81 8.186 8.196-8.186 8.184 2.81 2.81 8.203-8.192 8.18 8.192z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 246 B |
3
src/icons/thin/server-ping-good.svg
Normal file
3
src/icons/thin/server-ping-good.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.213 16.984c.97-1.028 2.308-1.664 3.787-1.664s2.817.636 3.787 1.664l-3.787 4.016zm-1.747-1.854c1.417-1.502 3.373-2.431 5.534-2.431s4.118.929 5.534 2.431l2.33-2.472c-2.012-2.134-4.793-3.454-7.864-3.454s-5.852 1.32-7.864 3.455zm-4.078-4.325c2.46-2.609 5.859-4.222 9.612-4.222s7.152 1.613 9.612 4.222l2.388-2.533c-3.071-3.257-7.313-5.272-12-5.272s-8.929 2.015-12 5.272z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 476 B |
5
src/icons/thin/server-ping-medium.svg
Normal file
5
src/icons/thin/server-ping-medium.svg
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m12 3c-4.687 0-8.929 2.015-12 5.272l2.388 2.533c2.46-2.609 5.859-4.222 9.612-4.222s7.152 1.613 9.612 4.222l2.388-2.533c-3.071-3.257-7.313-5.272-12-5.272z" fill-opacity=".45"/>
|
||||||
|
<path d="m6.466 15.13c1.417-1.502 3.373-2.431 5.534-2.431s4.118.929 5.534 2.431l2.33-2.472c-2.012-2.134-4.793-3.454-7.864-3.454s-5.852 1.32-7.864 3.455z"/>
|
||||||
|
<path d="m8.213 16.984c.97-1.028 2.308-1.664 3.787-1.664s2.817.636 3.787 1.664l-3.787 4.016z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 540 B |
5
src/icons/thin/server-visit-website.svg
Normal file
5
src/icons/thin/server-visit-website.svg
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<svg fill="none" height="24" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m18 13v6a2 2 0 0 1 -2 2h-11a2 2 0 0 1 -2-2v-11a2 2 0 0 1 2-2h6"/>
|
||||||
|
<path d="m15 3h6v6"/>
|
||||||
|
<path d="m10 14 11-11"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 315 B |
Loading…
Reference in New Issue
Block a user