mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-17 08:41:55 +00:00
android: add server view (#5221)
This commit is contained in:
-2
@@ -3861,7 +3861,6 @@ sealed class UserServersError {
|
||||
@Serializable @SerialName("noServers") data class NoServers(val protocol: ServerProtocol, val user: UserRef?): UserServersError()
|
||||
@Serializable @SerialName("storageMissing") data class StorageMissing(val protocol: ServerProtocol, val user: UserRef?): UserServersError()
|
||||
@Serializable @SerialName("proxyMissing") data class ProxyMissing(val protocol: ServerProtocol, val user: UserRef?): UserServersError()
|
||||
@Serializable @SerialName("invalidServer") data class InvalidServer(val protocol: ServerProtocol, val invalidServer: String): UserServersError()
|
||||
@Serializable @SerialName("duplicateServer") data class DuplicateServer(val protocol: ServerProtocol, val duplicateServer: String, val duplicateHost: String): UserServersError()
|
||||
|
||||
val globalError: String?
|
||||
@@ -3876,7 +3875,6 @@ sealed class UserServersError {
|
||||
is NoServers -> this.protocol
|
||||
is StorageMissing -> this.protocol
|
||||
is ProxyMissing -> this.protocol
|
||||
is InvalidServer -> this.protocol
|
||||
is DuplicateServer -> this.protocol
|
||||
}
|
||||
val globalSMPError: String?
|
||||
|
||||
+3
-16
@@ -48,7 +48,6 @@ import chat.simplex.common.model.localTimestamp
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.networkAndServers.ProtocolServersView
|
||||
import chat.simplex.common.views.usersettings.SettingsPreferenceItem
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
@@ -540,15 +539,8 @@ fun XFTPServerSummaryLayout(summary: XFTPServerSummary, statsStartedAt: Instant,
|
||||
)
|
||||
)
|
||||
}
|
||||
if (summary.known == true) {
|
||||
SectionItemView(click = {
|
||||
ModalManager.start.showCustomModal { close -> ProtocolServersView(chatModel, rhId = rh?.remoteHostId, ServerProtocol.XFTP, close) }
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.open_server_settings_button))
|
||||
}
|
||||
if (summary.stats != null || summary.sessions != null) {
|
||||
SectionDividerSpaced()
|
||||
}
|
||||
if (summary.stats != null || summary.sessions != null) {
|
||||
SectionDividerSpaced()
|
||||
}
|
||||
|
||||
if (summary.stats != null) {
|
||||
@@ -579,12 +571,7 @@ fun SMPServerSummaryLayout(summary: SMPServerSummary, statsStartedAt: Instant, r
|
||||
)
|
||||
)
|
||||
}
|
||||
if (summary.known == true) {
|
||||
SectionItemView(click = {
|
||||
ModalManager.start.showCustomModal { close -> ProtocolServersView(chatModel, rhId = rh?.remoteHostId, ServerProtocol.SMP, close) }
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.open_server_settings_button))
|
||||
}
|
||||
if (summary.stats != null || summary.subs != null || summary.sessions != null) {
|
||||
SectionDividerSpaced()
|
||||
}
|
||||
|
||||
|
||||
+11
-1
@@ -188,7 +188,17 @@ fun ModalData.NetworkAndServersView(close: () -> Unit) {
|
||||
val nullOperatorIndex = userServers.value.indexOfFirst { it.operator == null }
|
||||
|
||||
if (nullOperatorIndex != -1) {
|
||||
SectionItemView({ ModalManager.start.showModal { YourServersView(userServers = userServers, operatorIndex = nullOperatorIndex) } }) {
|
||||
SectionItemView({
|
||||
ModalManager.start.showModal {
|
||||
YourServersView(
|
||||
currUserServers = currUserServers,
|
||||
userServers = userServers,
|
||||
serverErrors = serverErrors,
|
||||
operatorIndex = nullOperatorIndex,
|
||||
rhId = currentRemoteHost?.remoteHostId
|
||||
)
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_dns),
|
||||
stringResource(MR.strings.your_servers),
|
||||
|
||||
+248
@@ -0,0 +1,248 @@
|
||||
package chat.simplex.common.views.usersettings.networkAndServers
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionDividerSpaced
|
||||
import SectionItemView
|
||||
import SectionItemViewSpaceBetween
|
||||
import SectionView
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ServerAddress.Companion.parseServerAddress
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.newchat.QRCode
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.usersettings.PreferenceToggle
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
||||
@Composable
|
||||
fun NewServerView(
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
serverErrors: MutableState<List<UserServersError>>,
|
||||
operatorIndex: Int,
|
||||
rhId: Long?,
|
||||
close: () -> Unit
|
||||
) {
|
||||
val newServer = remember { mutableStateOf(UserServer.empty) }
|
||||
val testing = remember { mutableStateOf(false) }
|
||||
|
||||
BackHandler(onBack = {
|
||||
addServer(
|
||||
newServer.value,
|
||||
userServers,
|
||||
serverErrors,
|
||||
rhId,
|
||||
close = close
|
||||
)
|
||||
})
|
||||
|
||||
NewServerLayout(
|
||||
userServers,
|
||||
serverErrors,
|
||||
operatorIndex,
|
||||
rhId,
|
||||
newServer,
|
||||
testing
|
||||
// testServer = {
|
||||
// testing = true
|
||||
// withLongRunningApi {
|
||||
// val res = testServerConnection(server, m)
|
||||
// if (isActive) {
|
||||
// onUpdate(res.first)
|
||||
// testing = false
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
)
|
||||
if (testing.value) {
|
||||
Box(
|
||||
Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
Modifier
|
||||
.padding(horizontal = 2.dp)
|
||||
.size(30.dp),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
strokeWidth = 2.5.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NewServerLayout(
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
serverErrors: MutableState<List<UserServersError>>,
|
||||
operatorIndex: Int,
|
||||
rhId: Long?,
|
||||
server: MutableState<UserServer>,
|
||||
testing: MutableState<Boolean>
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.smp_servers_new_server))
|
||||
CustomServer(userServers, serverErrors, operatorIndex, rhId, server, testing)
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CustomServer(
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
serverErrors: MutableState<List<UserServersError>>,
|
||||
operatorIndex: Int,
|
||||
rhId: Long?,
|
||||
server: MutableState<UserServer>,
|
||||
testing: MutableState<Boolean>
|
||||
) {
|
||||
val serverAddress = remember { mutableStateOf(server.value.server) }
|
||||
val valid = remember {
|
||||
derivedStateOf {
|
||||
with(parseServerAddress(serverAddress.value)) {
|
||||
this?.valid == true
|
||||
}
|
||||
}
|
||||
}
|
||||
SectionView(
|
||||
stringResource(MR.strings.smp_servers_your_server_address).uppercase(),
|
||||
icon = painterResource(MR.images.ic_error),
|
||||
iconTint = if (!valid.value) MaterialTheme.colors.error else Color.Transparent,
|
||||
) {
|
||||
val testedPreviously = remember { mutableMapOf<String, Boolean?>() }
|
||||
TextEditor(
|
||||
serverAddress,
|
||||
Modifier.height(144.dp)
|
||||
)
|
||||
// TODO review
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { server.value.server }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
testedPreviously[server.value.server] = server.value.tested
|
||||
// server = server.copy(server = it, tested = testedPreviously[serverAddress.value])
|
||||
}
|
||||
}
|
||||
}
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
// UseServerSection(valid.value, testing, server, testServer, onUpdate, onDelete)
|
||||
|
||||
if (valid.value) {
|
||||
SectionDividerSpaced()
|
||||
SectionView(stringResource(MR.strings.smp_servers_add_to_another_device).uppercase()) {
|
||||
QRCode(serverAddress.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun serverProtocolAndOperator(
|
||||
server: UserServer,
|
||||
userServers: List<UserOperatorServers>
|
||||
): Pair<ServerProtocol, ServerOperator?>? {
|
||||
val serverAddress = parseServerAddress(server.server)
|
||||
return if (serverAddress != null) {
|
||||
val serverProtocol = serverAddress.serverProtocol
|
||||
val hostnames = serverAddress.hostnames
|
||||
val matchingOperator = userServers.mapNotNull { it.operator }.firstOrNull { op ->
|
||||
op.serverDomains.any { domain ->
|
||||
hostnames.any { hostname ->
|
||||
hostname.endsWith(domain)
|
||||
}
|
||||
}
|
||||
}
|
||||
Pair(serverProtocol, matchingOperator)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun addServer(
|
||||
server: UserServer,
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
serverErrors: MutableState<List<UserServersError>>,
|
||||
rhId: Long?,
|
||||
close: () -> Unit
|
||||
) {
|
||||
val result = serverProtocolAndOperator(server, userServers.value)
|
||||
if (result != null) {
|
||||
val (serverProtocol, matchingOperator) = result
|
||||
val operatorIndex = userServers.value.indexOfFirst { it.operator?.operatorId == matchingOperator?.operatorId }
|
||||
if (operatorIndex != -1) {
|
||||
// Create a mutable copy of the userServers list
|
||||
val updatedUserServers = userServers.value.toMutableList()
|
||||
val operatorServers = updatedUserServers[operatorIndex]
|
||||
|
||||
// Create a mutable copy of the smpServers or xftpServers and add the server
|
||||
when (serverProtocol) {
|
||||
ServerProtocol.SMP -> {
|
||||
val updatedSMPServers = operatorServers.smpServers.toMutableList()
|
||||
updatedSMPServers.add(server)
|
||||
updatedUserServers[operatorIndex] = operatorServers.copy(smpServers = updatedSMPServers)
|
||||
}
|
||||
ServerProtocol.XFTP -> {
|
||||
val updatedXFTPServers = operatorServers.xftpServers.toMutableList()
|
||||
updatedXFTPServers.add(server)
|
||||
updatedUserServers[operatorIndex] = operatorServers.copy(xftpServers = updatedXFTPServers)
|
||||
}
|
||||
}
|
||||
|
||||
userServers.value = updatedUserServers
|
||||
// TODO
|
||||
// validateServers(rhId, userServers, serverErrors)
|
||||
close()
|
||||
matchingOperator?.let { op ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = "Operator server",
|
||||
text = "Server added to operator ${op.tradeName}."
|
||||
)
|
||||
}
|
||||
} else { // Shouldn't happen
|
||||
close()
|
||||
AlertManager.shared.showAlertMsg(title = "Error adding server")
|
||||
}
|
||||
} else {
|
||||
close()
|
||||
if (server.server.trim().isNotEmpty()) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = "Invalid server address!",
|
||||
text = "Check server address and try again."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//@Composable
|
||||
//private fun UseServerSection(
|
||||
// valid: Boolean,
|
||||
// testing: MutableState<Boolean>,
|
||||
// server: UserServer
|
||||
//) {
|
||||
// SectionView(stringResource(MR.strings.smp_servers_use_server).uppercase()) {
|
||||
// SectionItemViewSpaceBetween(testServer, disabled = !valid || testing.value) {
|
||||
// Text(stringResource(MR.strings.smp_servers_test_server), color = if (valid && !testing.value) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary)
|
||||
// ShowTestStatus(server)
|
||||
// }
|
||||
//
|
||||
// val enabled = rememberUpdatedState(server.enabled)
|
||||
// PreferenceToggle(
|
||||
// stringResource(MR.strings.smp_servers_use_server_for_new_conn),
|
||||
// disabled = server.tested != true && !server.preset,
|
||||
// checked = enabled.value
|
||||
// ) {
|
||||
// onUpdate(server.copy(enabled = it))
|
||||
// }
|
||||
//
|
||||
// SectionItemView(onDelete, disabled = testing) {
|
||||
// Text(stringResource(MR.strings.smp_servers_delete_server), color = if (testing) MaterialTheme.colors.secondary else MaterialTheme.colors.error)
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
+119
-40
@@ -1,6 +1,7 @@
|
||||
package chat.simplex.common.views.usersettings.networkAndServers
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionCustomFooter
|
||||
import SectionDividerSpaced
|
||||
import SectionItemView
|
||||
import SectionTextFooter
|
||||
@@ -42,7 +43,7 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser
|
||||
testing.value ||
|
||||
servers.none { srv ->
|
||||
val address = parseServerAddress(srv.server)
|
||||
address != null && uniqueAddress(srv, address, servers)
|
||||
address != null
|
||||
} ||
|
||||
allServersDisabled.value
|
||||
}
|
||||
@@ -131,15 +132,6 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser
|
||||
Text(stringResource(MR.strings.smp_servers_scan_qr), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
val hasAllPresets = hasAllPresets(presetServers, servers, m)
|
||||
if (!hasAllPresets) {
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
servers = (servers + addAllPresets(rhId, presetServers, servers, m)).sortedByDescending { it.preset }
|
||||
}) {
|
||||
Text(stringResource(MR.strings.smp_servers_preset_add), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -177,13 +169,6 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ModalData.YourServersView(userServers: MutableState<List<UserOperatorServers>>, operatorIndex: Int) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.your_servers))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProtocolServersLayout(
|
||||
serverProtocol: ServerProtocol,
|
||||
@@ -269,11 +254,127 @@ private fun ProtocolServersLayout(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ModalData.YourServersView(
|
||||
currUserServers: MutableState<List<UserOperatorServers>>,
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
serverErrors: MutableState<List<UserServersError>>,
|
||||
operatorIndex: Int,
|
||||
rhId: Long?
|
||||
) {
|
||||
val testing = remember { mutableStateOf(false) }
|
||||
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.your_servers))
|
||||
YourServersViewLayout(currUserServers, userServers, serverErrors, operatorIndex, rhId, testing.value)
|
||||
if (testing.value) {
|
||||
DefaultProgressView(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun YourServersViewLayout(
|
||||
currUserServers: MutableState<List<UserOperatorServers>>,
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
serverErrors: MutableState<List<UserServersError>>,
|
||||
operatorIndex: Int,
|
||||
rhId: Long?,
|
||||
testing: Boolean
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val duplicateHosts = findDuplicateHosts(serverErrors.value)
|
||||
|
||||
Column {
|
||||
if (userServers.value[operatorIndex].smpServers.any { !it.deleted }) {
|
||||
SectionView(generalGetString(MR.strings.message_servers).uppercase()) {
|
||||
userServers.value[operatorIndex].smpServers.forEach { server ->
|
||||
SectionItemView {
|
||||
ProtocolServerView(
|
||||
srv = server,
|
||||
serverProtocol = ServerProtocol.SMP,
|
||||
duplicateHosts = duplicateHosts
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
val smpErrors = globalSMPServersError(serverErrors.value)
|
||||
if (smpErrors != null) {
|
||||
SectionCustomFooter {
|
||||
ServerErrorsView(smpErrors)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionDividerSpaced()
|
||||
|
||||
SectionView {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_add),
|
||||
stringResource(MR.strings.smp_servers_add),
|
||||
click = { showAddServerDialog(userServers, serverErrors, operatorIndex, rhId) },
|
||||
disabled = testing,
|
||||
textColor = if (testing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary,
|
||||
iconColor = if (testing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false)
|
||||
|
||||
SectionView {
|
||||
// TODO Test servers button
|
||||
SectionItemView { Text("Test servers") }
|
||||
}
|
||||
SectionDividerSpaced(maxBottomPadding = false)
|
||||
|
||||
SectionView {
|
||||
HowToButton()
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
fun showAddServerDialog(
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
serverErrors: MutableState<List<UserServersError>>,
|
||||
operatorIndex: Int,
|
||||
rhId: Long?
|
||||
) {
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = generalGetString(MR.strings.smp_servers_add),
|
||||
buttons = {
|
||||
Column {
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
ModalManager.start.showModalCloseable { close ->
|
||||
NewServerView(userServers, serverErrors, operatorIndex, rhId, close)
|
||||
}
|
||||
}) {
|
||||
Text(stringResource(MR.strings.smp_servers_enter_manually), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
if (appPlatform.isAndroid) {
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
ModalManager.start.showModalCloseable { close ->
|
||||
ScanProtocolServer(rhId) {
|
||||
close()
|
||||
// TODO reuse logic from NewServerView
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(stringResource(MR.strings.smp_servers_scan_qr), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProtocolServerView(serverProtocol: ServerProtocol, srv: UserServer, servers: List<UserServer>, disabled: Boolean) {
|
||||
val address = parseServerAddress(srv.server)
|
||||
when {
|
||||
address == null || !address.valid || address.serverProtocol != serverProtocol || !uniqueAddress(srv, address, servers) -> InvalidServer()
|
||||
address == null || !address.valid || address.serverProtocol != serverProtocol -> InvalidServer()
|
||||
!srv.enabled -> Icon(painterResource(MR.images.ic_do_not_disturb_on), null, tint = MaterialTheme.colors.secondary)
|
||||
else -> ShowTestStatus(srv)
|
||||
}
|
||||
@@ -321,28 +422,6 @@ fun InvalidServer() {
|
||||
Icon(painterResource(MR.images.ic_error), null, tint = MaterialTheme.colors.error)
|
||||
}
|
||||
|
||||
private fun uniqueAddress(s: UserServer, address: ServerAddress, servers: List<UserServer>): Boolean = servers.all { srv ->
|
||||
address.hostnames.all { host ->
|
||||
srv.id == s.id || !srv.server.contains(host)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasAllPresets(presetServers: List<UserServer>, servers: List<UserServer>, m: ChatModel): Boolean =
|
||||
presetServers.all { hasPreset(it, servers) } ?: true
|
||||
|
||||
private fun addAllPresets(rhId: Long?, presetServers: List<UserServer>, servers: List<UserServer>, m: ChatModel): List<UserServer> {
|
||||
val toAdd = ArrayList<UserServer>()
|
||||
for (srv in presetServers) {
|
||||
if (!hasPreset(srv, servers)) {
|
||||
toAdd.add(srv)
|
||||
}
|
||||
}
|
||||
return toAdd
|
||||
}
|
||||
|
||||
private fun hasPreset(srv: UserServer, servers: List<UserServer>): Boolean =
|
||||
servers.any { it.server == srv.server }
|
||||
|
||||
private suspend fun testServers(testing: MutableState<Boolean>, servers: List<UserServer>, m: ChatModel, onUpdated: (List<UserServer>) -> Unit) {
|
||||
val resetStatus = resetTestStatus(servers)
|
||||
onUpdated(resetStatus)
|
||||
|
||||
@@ -757,6 +757,7 @@
|
||||
<string name="smp_servers_test_some_failed">Some servers failed the test:</string>
|
||||
<string name="smp_servers_scan_qr">Scan server QR code</string>
|
||||
<string name="smp_servers_enter_manually">Enter server manually</string>
|
||||
<string name="smp_servers_new_server">New server</string>
|
||||
<string name="smp_servers_preset_server">Preset server</string>
|
||||
<string name="smp_servers_your_server">Your server</string>
|
||||
<string name="smp_servers_your_server_address">Your server address</string>
|
||||
|
||||
Reference in New Issue
Block a user