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
|
||||
|
||||
- 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
|
||||
- cursor shape in HBox/HTabbedBox pages over fields
|
||||
- login with account already added → infinite spinner in room list
|
||||
|
|
|
@ -2,12 +2,17 @@
|
|||
|
||||
import asyncio
|
||||
import logging as log
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, DefaultDict, Dict, List, Optional, Tuple
|
||||
|
||||
import aiohttp
|
||||
from appdirs import AppDirs
|
||||
|
||||
import nio
|
||||
|
@ -18,7 +23,7 @@ from .matrix_client import MatrixClient
|
|||
from .media_cache import MediaCache
|
||||
from .models import SyncId
|
||||
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_store import ModelStore
|
||||
from .presence import Presence
|
||||
|
@ -107,6 +112,9 @@ class Backend:
|
|||
self._sso_server: Optional[SSOServer] = 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.get_profile_locks: DefaultDict[str, asyncio.Lock] = \
|
||||
DefaultDict(asyncio.Lock) # {user_id: lock}
|
||||
|
@ -474,3 +482,139 @@ class Backend:
|
|||
|
||||
if device.ed25519 == ed25519_key:
|
||||
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()
|
||||
|
||||
|
||||
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
|
||||
class Account(ModelItem):
|
||||
"""A logged in matrix account."""
|
||||
|
|
|
@ -3,8 +3,9 @@
|
|||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
|
||||
HFlickableColumnPage {
|
||||
HColumnPage {
|
||||
implicitWidth: Math.min(parent.width, theme.controls.box.defaultWidth)
|
||||
padding: theme.spacing
|
||||
|
||||
background: Rectangle {
|
||||
color: theme.controls.box.background
|
||||
|
|
|
@ -2,12 +2,13 @@
|
|||
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../.."
|
||||
import "../../Base"
|
||||
import "../../Base/Buttons"
|
||||
import "../../PythonBridge"
|
||||
|
||||
HBox {
|
||||
id: page
|
||||
id: box
|
||||
|
||||
property string acceptedUserUrl: ""
|
||||
property string acceptedUrl: ""
|
||||
|
@ -15,22 +16,35 @@ HBox {
|
|||
|
||||
property string saveName: "serverBrowser"
|
||||
property var saveProperties: ["acceptedUserUrl"]
|
||||
property string loadingIconStep: "server-ping-bad"
|
||||
|
||||
property Future connectFuture: null
|
||||
property Future fetchServersFuture: null
|
||||
|
||||
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() {
|
||||
if (connectFuture) connectFuture.cancel()
|
||||
connectTimeout.restart()
|
||||
|
||||
const args = [serverField.item.cleanText]
|
||||
const args = [serverField.item.field.cleanText]
|
||||
|
||||
connectFuture = py.callCoro("server_info", args, ([url, flows]) => {
|
||||
connectTimeout.stop()
|
||||
errorMessage.text = ""
|
||||
connectFuture = null
|
||||
serverField.errorLabel.text = ""
|
||||
connectFuture = null
|
||||
|
||||
if (! (
|
||||
flows.includes("m.login.password") ||
|
||||
|
@ -39,7 +53,7 @@ HBox {
|
|||
flows.includes("m.login.token")
|
||||
)
|
||||
)) {
|
||||
errorMessage.text =
|
||||
serverField.errorLabel.text =
|
||||
qsTr("No supported sign-in method for this homeserver.")
|
||||
return
|
||||
}
|
||||
|
@ -63,106 +77,137 @@ HBox {
|
|||
|
||||
py.showError(type, traceback, uuid)
|
||||
|
||||
errorMessage.text = text
|
||||
serverField.errorLabel.text = text
|
||||
})
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
if (page.connectFuture) return
|
||||
|
||||
connectTimeout.stop()
|
||||
connectFuture.cancel()
|
||||
connectFuture = null
|
||||
padding: 0
|
||||
implicitWidth: theme.controls.box.defaultWidth * 1.25
|
||||
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 {
|
||||
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()
|
||||
}
|
||||
readonly property bool knownServerChosen:
|
||||
serverList.model.find(item.cleanText) !== null
|
||||
|
||||
CancelButton {
|
||||
id: cancelButton
|
||||
enabled: page.connectFuture !== null
|
||||
onClicked: page.cancel()
|
||||
label.text: qsTr("Homeserver address:")
|
||||
label.topPadding: theme.spacing
|
||||
label.leftPadding: label.topPadding
|
||||
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
|
||||
onClicked: box.connect()
|
||||
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onKeyboardAccept: if (applyButton.enabled) page.connect()
|
||||
onKeyboardCancel: if (cancelButton.enabled) page.cancel()
|
||||
onKeyboardAccept: if (serverField.item.apply.enabled) box.connect()
|
||||
onAccepted: window.saveState(this)
|
||||
|
||||
Timer {
|
||||
id: connectTimeout
|
||||
interval: 30 * 1000
|
||||
onTriggered: {
|
||||
errorMessage.text =
|
||||
serverField.errorLabel.text =
|
||||
serverField.knownServerChosen ?
|
||||
|
||||
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 " +
|
||||
"entered address, your internet connection or try " +
|
||||
"again in a few minutes.")
|
||||
"again later.")
|
||||
}
|
||||
}
|
||||
|
||||
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().replace(/\/+$/, "")
|
||||
|
||||
width: parent.width
|
||||
error: ! /https?:\/\/.+/.test(cleanText)
|
||||
defaultText:
|
||||
window.getState(page, "acceptedUserUrl", "https://matrix.org")
|
||||
}
|
||||
Timer {
|
||||
interval: 1000
|
||||
running: fetchServersFuture === null && serverList.count === 0
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: box.fetchServers()
|
||||
}
|
||||
|
||||
HLabel {
|
||||
id: errorMessage
|
||||
wrapMode: HLabel.Wrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
color: theme.colors.errorText
|
||||
Timer {
|
||||
interval: theme.animationDuration * 2
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered:
|
||||
box.loadingIconStep = "server-ping-" + (
|
||||
box.loadingIconStep === "server-ping-bad" ? "medium" :
|
||||
box.loadingIconStep === "server-ping-medium" ? "good" :
|
||||
"bad"
|
||||
)
|
||||
}
|
||||
|
||||
visible: Layout.maximumHeight > 0
|
||||
Layout.maximumHeight: text ? implicitHeight : 0
|
||||
Behavior on Layout.maximumHeight { HNumberAnimation {} }
|
||||
HListView {
|
||||
id: serverList
|
||||
clip: true
|
||||
model: ModelStore.get("homeservers")
|
||||
|
||||
delegate: ServerDelegate {
|
||||
width: serverList.width
|
||||
loadingIconStep: box.loadingIconStep
|
||||
onClicked: {
|
||||
serverField.item.field.text = model.id
|
||||
serverField.item.apply.clicked()
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
return parseFloat(floatNumber.toFixed(2))
|
||||
function round(floatNumber, decimalDigits=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