desktop: allow changing listening ip and port of remote (#3498)
* desktop: allow changing listening ip and port of remote * remove empty lines --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
parent
9a1c7f41f7
commit
e1ff7c88d7
@ -1426,9 +1426,9 @@ object ChatController {
|
||||
chatModel.remoteHosts.addAll(hosts)
|
||||
}
|
||||
|
||||
suspend fun startRemoteHost(rhId: Long?, multicast: Boolean = true): Triple<RemoteHostInfo?, String, String>? {
|
||||
val r = sendCmd(null, CC.StartRemoteHost(rhId, multicast))
|
||||
if (r is CR.RemoteHostStarted) return Triple(r.remoteHost_, r.invitation, r.ctrlPort)
|
||||
suspend fun startRemoteHost(rhId: Long?, multicast: Boolean = true, address: RemoteCtrlAddress?, port: Int?): CR.RemoteHostStarted? {
|
||||
val r = sendCmd(null, CC.StartRemoteHost(rhId, multicast, address, port))
|
||||
if (r is CR.RemoteHostStarted) return r
|
||||
apiErrorAlert("startRemoteHost", generalGetString(MR.strings.error_alert_title), r)
|
||||
return null
|
||||
}
|
||||
@ -2248,7 +2248,7 @@ sealed class CC {
|
||||
// Remote control
|
||||
class SetLocalDeviceName(val displayName: String): CC()
|
||||
class ListRemoteHosts(): CC()
|
||||
class StartRemoteHost(val remoteHostId: Long?, val multicast: Boolean): CC()
|
||||
class StartRemoteHost(val remoteHostId: Long?, val multicast: Boolean, val address: RemoteCtrlAddress?, val port: Int?): CC()
|
||||
class SwitchRemoteHost (val remoteHostId: Long?): CC()
|
||||
class StopRemoteHost(val remoteHostKey: Long?): CC()
|
||||
class DeleteRemoteHost(val remoteHostId: Long): CC()
|
||||
@ -2384,7 +2384,7 @@ sealed class CC {
|
||||
is CancelFile -> "/fcancel $fileId"
|
||||
is SetLocalDeviceName -> "/set device name $displayName"
|
||||
is ListRemoteHosts -> "/list remote hosts"
|
||||
is StartRemoteHost -> "/start remote host " + if (remoteHostId == null) "new" else "$remoteHostId multicast=${onOff(multicast)}"
|
||||
is StartRemoteHost -> "/start remote host " + (if (remoteHostId == null) "new" else "$remoteHostId multicast=${onOff(multicast)}") + (if (address != null) " addr=${address.address} iface=${address.`interface`}" else "") + (if (port != null) " port=$port" else "")
|
||||
is SwitchRemoteHost -> "/switch remote host " + if (remoteHostId == null) "local" else "$remoteHostId"
|
||||
is StopRemoteHost -> "/stop remote host " + if (remoteHostKey == null) "new" else "$remoteHostKey"
|
||||
is DeleteRemoteHost -> "/delete remote host $remoteHostId"
|
||||
@ -3606,6 +3606,8 @@ data class RemoteHostInfo(
|
||||
val remoteHostId: Long,
|
||||
val hostDeviceName: String,
|
||||
val storePath: String,
|
||||
val bindAddress_: RemoteCtrlAddress?,
|
||||
val bindPort_: Int?,
|
||||
val sessionState: RemoteHostSessionState?
|
||||
) {
|
||||
val activeHost: Boolean
|
||||
@ -3614,6 +3616,12 @@ data class RemoteHostInfo(
|
||||
fun activeHost(): Boolean = chatModel.currentRemoteHost.value?.remoteHostId == remoteHostId
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class RemoteCtrlAddress(
|
||||
val address: String,
|
||||
val `interface`: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
sealed class RemoteHostSessionState {
|
||||
@Serializable @SerialName("starting") object Starting: RemoteHostSessionState()
|
||||
@ -3848,7 +3856,7 @@ sealed class CR {
|
||||
// remote events (desktop)
|
||||
@Serializable @SerialName("remoteHostList") class RemoteHostList(val remoteHosts: List<RemoteHostInfo>): CR()
|
||||
@Serializable @SerialName("currentRemoteHost") class CurrentRemoteHost(val remoteHost_: RemoteHostInfo?): CR()
|
||||
@Serializable @SerialName("remoteHostStarted") class RemoteHostStarted(val remoteHost_: RemoteHostInfo?, val invitation: String, val ctrlPort: String): CR()
|
||||
@Serializable @SerialName("remoteHostStarted") class RemoteHostStarted(val remoteHost_: RemoteHostInfo?, val invitation: String, val localAddrs: List<RemoteCtrlAddress>, val ctrlPort: String): CR()
|
||||
@Serializable @SerialName("remoteHostSessionCode") class RemoteHostSessionCode(val remoteHost_: RemoteHostInfo?, val sessionCode: String): CR()
|
||||
@Serializable @SerialName("newRemoteHost") class NewRemoteHost(val remoteHost: RemoteHostInfo): CR()
|
||||
@Serializable @SerialName("remoteHostConnected") class RemoteHostConnected(val remoteHost: RemoteHostInfo): CR()
|
||||
|
@ -152,7 +152,6 @@ fun DefaultConfigurableTextField(
|
||||
BasicTextField(
|
||||
value = state.value,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(colors.backgroundColor(enabled).value, shape)
|
||||
.indicatorLine(enabled, false, interactionSource, colors)
|
||||
.defaultMinSize(
|
||||
|
@ -10,72 +10,87 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.res.MR
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.usersettings.SettingsActionItemWithContent
|
||||
|
||||
@Composable
|
||||
fun <T> ExposedDropDownSetting(
|
||||
values: List<Pair<T, String>>,
|
||||
selection: State<T>,
|
||||
textColor: Color = MaterialTheme.colors.secondary,
|
||||
label: String? = null,
|
||||
enabled: State<Boolean> = mutableStateOf(true),
|
||||
minWidth: Dp = 200.dp,
|
||||
maxWidth: Dp = with(LocalDensity.current) { 180.sp.toDp() },
|
||||
onSelected: (T) -> Unit
|
||||
) {
|
||||
val expanded = remember { mutableStateOf(false) }
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded.value,
|
||||
onExpandedChange = {
|
||||
expanded.value = !expanded.value && enabled.value
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
Modifier.padding(start = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
Text(
|
||||
values.first { it.first == selection.value }.second + (if (label != null) " $label" else ""),
|
||||
Modifier.widthIn(max = maxWidth),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = textColor
|
||||
)
|
||||
Spacer(Modifier.size(12.dp))
|
||||
Icon(
|
||||
if (!expanded.value) painterResource(MR.images.ic_expand_more) else painterResource(MR.images.ic_expand_less),
|
||||
generalGetString(MR.strings.icon_descr_more_button),
|
||||
tint = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
DefaultExposedDropdownMenu(
|
||||
modifier = Modifier.widthIn(min = minWidth),
|
||||
expanded = expanded,
|
||||
) {
|
||||
values.forEach { selectionOption ->
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
onSelected(selectionOption.first)
|
||||
expanded.value = false
|
||||
},
|
||||
contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)
|
||||
) {
|
||||
Text(
|
||||
selectionOption.second + (if (label != null) " $label" else ""),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = if (isInDarkTheme()) MenuTextColorDark else Color.Black,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T> ExposedDropDownSettingRow(
|
||||
title: String,
|
||||
values: List<Pair<T, String>>,
|
||||
selection: State<T>,
|
||||
textColor: Color = MaterialTheme.colors.secondary,
|
||||
label: String? = null,
|
||||
icon: Painter? = null,
|
||||
iconTint: Color = MaterialTheme.colors.secondary,
|
||||
enabled: State<Boolean> = mutableStateOf(true),
|
||||
minWidth: Dp = 200.dp,
|
||||
maxWidth: Dp = with(LocalDensity.current) { 180.sp.toDp() },
|
||||
onSelected: (T) -> Unit
|
||||
) {
|
||||
SettingsActionItemWithContent(icon, title, iconColor = iconTint, disabled = !enabled.value) {
|
||||
val expanded = remember { mutableStateOf(false) }
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded.value,
|
||||
onExpandedChange = {
|
||||
expanded.value = !expanded.value && enabled.value
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
Modifier.padding(start = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
val maxWidth = with(LocalDensity.current) { 180.sp.toDp() }
|
||||
Text(
|
||||
values.first { it.first == selection.value }.second + (if (label != null) " $label" else ""),
|
||||
Modifier.widthIn(max = maxWidth),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
Spacer(Modifier.size(12.dp))
|
||||
Icon(
|
||||
if (!expanded.value) painterResource(MR.images.ic_expand_more) else painterResource(MR.images.ic_expand_less),
|
||||
generalGetString(MR.strings.icon_descr_more_button),
|
||||
tint = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
DefaultExposedDropdownMenu(
|
||||
modifier = Modifier.widthIn(min = 200.dp),
|
||||
expanded = expanded,
|
||||
) {
|
||||
values.forEach { selectionOption ->
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
onSelected(selectionOption.first)
|
||||
expanded.value = false
|
||||
},
|
||||
contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)
|
||||
) {
|
||||
Text(
|
||||
selectionOption.second + (if (label != null) " $label" else ""),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = if (isInDarkTheme()) MenuTextColorDark else Color.Black,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ExposedDropDownSetting(values, selection ,textColor, label, enabled, minWidth, maxWidth, onSelected)
|
||||
}
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ fun annotatedStringResource(id: StringResource): AnnotatedString {
|
||||
@Composable
|
||||
fun annotatedStringResource(id: StringResource, vararg args: Any?): AnnotatedString {
|
||||
val density = LocalDensity.current
|
||||
return remember(id) {
|
||||
return remember(id, args) {
|
||||
escapedHtmlToAnnotatedString(id.localized().format(args = args), density)
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import SectionView
|
||||
import TextIconSpaced
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
@ -17,6 +18,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.*
|
||||
import androidx.compose.ui.text.input.*
|
||||
@ -32,11 +34,11 @@ import chat.simplex.common.views.chat.item.ItemAction
|
||||
import chat.simplex.common.views.chatlist.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.newchat.QRCode
|
||||
import chat.simplex.common.views.usersettings.PreferenceToggle
|
||||
import chat.simplex.common.views.usersettings.SettingsActionItemWithContent
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
||||
@Composable
|
||||
fun ConnectMobileView() {
|
||||
@ -156,7 +158,7 @@ fun DeviceNameField(
|
||||
DefaultConfigurableTextField(
|
||||
state = state,
|
||||
placeholder = generalGetString(MR.strings.enter_this_device_name),
|
||||
modifier = Modifier.padding(start = DEFAULT_PADDING),
|
||||
modifier = Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING),
|
||||
isValid = { true },
|
||||
)
|
||||
KeyChangeEffect(state.value) {
|
||||
@ -172,7 +174,10 @@ private fun ConnectMobileViewLayout(
|
||||
sessionCode: String?,
|
||||
port: String?,
|
||||
staleQrCode: Boolean = false,
|
||||
refreshQrCode: () -> Unit = {}
|
||||
editEnabled: Boolean = false,
|
||||
editClicked: () -> Unit = {},
|
||||
refreshQrCode: () -> Unit = {},
|
||||
UnderQrLayout: @Composable () -> Unit = {},
|
||||
) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
|
||||
@ -196,7 +201,18 @@ private fun ConnectMobileViewLayout(
|
||||
}
|
||||
}
|
||||
SectionTextFooter(annotatedStringResource(MR.strings.open_on_mobile_and_scan_qr_code), textAlign = TextAlign.Center)
|
||||
SectionTextFooter(annotatedStringResource(MR.strings.waiting_for_mobile_to_connect_on_port, port), textAlign = TextAlign.Center)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
SectionTextFooter(annotatedStringResource(MR.strings.waiting_for_mobile_to_connect_on_port, port), textAlign = TextAlign.Center)
|
||||
if (editEnabled) {
|
||||
Spacer(Modifier.width(4.dp))
|
||||
IconButton(editClicked, Modifier.size(16.dp)) {
|
||||
Icon(painterResource(MR.images.ic_edit), stringResource(MR.strings.edit_verb), Modifier.size(16.dp), tint = MaterialTheme.colors.primary)
|
||||
}
|
||||
Spacer(Modifier.width(DEFAULT_PADDING))
|
||||
}
|
||||
}
|
||||
|
||||
UnderQrLayout()
|
||||
|
||||
if (remember { controller.appPrefs.developerTools.state }.value) {
|
||||
val clipboard = LocalClipboardManager.current
|
||||
@ -259,14 +275,22 @@ private fun showAddingMobileDevice(connecting: MutableState<Boolean>) {
|
||||
|
||||
@Composable
|
||||
fun AddingMobileDevice(showTitle: Boolean, staleQrCode: MutableState<Boolean>, connecting: MutableState<Boolean>, close: () -> Unit) {
|
||||
val invitation = rememberSaveable { mutableStateOf<String?>(null) }
|
||||
val port = rememberSaveable { mutableStateOf<String?>(null) }
|
||||
val cachedR = remember { mutableStateOf<CR.RemoteHostStarted?>(null) }
|
||||
val customAddress = rememberSaveable { mutableStateOf<RemoteCtrlAddress?>(null) }
|
||||
val customPort = rememberSaveable { mutableStateOf<Int?>(null) }
|
||||
var editing by rememberSaveable { mutableStateOf(false) }
|
||||
val startRemoteHost = suspend {
|
||||
val r = chatModel.controller.startRemoteHost(null, controller.appPrefs.offerRemoteMulticast.get())
|
||||
val r = chatModel.controller.startRemoteHost(
|
||||
rhId = null,
|
||||
multicast = controller.appPrefs.offerRemoteMulticast.get(),
|
||||
address = if (customAddress.value?.address != cachedR.address?.address) customAddress.value else cachedR.rh?.bindAddress_,
|
||||
port = if (customPort.value != cachedR.port) customPort.value else cachedR.rh?.bindPort_
|
||||
)
|
||||
if (r != null) {
|
||||
cachedR.value = r
|
||||
connecting.value = true
|
||||
invitation.value = r.second
|
||||
port.value = r.third
|
||||
customAddress.value = cachedR.address
|
||||
customPort.value = cachedR.port
|
||||
chatModel.remoteHostPairing.value = null to RemoteHostSessionState.Starting
|
||||
}
|
||||
}
|
||||
@ -283,19 +307,23 @@ fun AddingMobileDevice(showTitle: Boolean, staleQrCode: MutableState<Boolean>, c
|
||||
val remoteDeviceName = pairing.value?.first?.hostDeviceName
|
||||
ConnectMobileViewLayout(
|
||||
title = if (!showTitle) null else if (cachedSessionCode == null) stringResource(MR.strings.link_a_mobile) else stringResource(MR.strings.verify_connection),
|
||||
invitation = invitation.value,
|
||||
invitation = cachedR.invitation,
|
||||
deviceName = remoteDeviceName,
|
||||
sessionCode = cachedSessionCode,
|
||||
port = port.value,
|
||||
staleQrCode = staleQrCode.value,
|
||||
port = cachedR.value?.ctrlPort,
|
||||
staleQrCode = staleQrCode.value || (cachedR.address != customAddress.value && customAddress.value != null) || (cachedR.port != customPort.value && customPort.value != null),
|
||||
editEnabled = !editing && cachedR.addresses.isNotEmpty(),
|
||||
editClicked = { editing = true },
|
||||
refreshQrCode = {
|
||||
withBGApi {
|
||||
if (chatController.stopRemoteHost(null)) {
|
||||
startRemoteHost()
|
||||
staleQrCode.value = false
|
||||
editing = false
|
||||
}
|
||||
}
|
||||
},
|
||||
UnderQrLayout = { UnderQrLayout(editing, cachedR, customAddress, customPort) }
|
||||
)
|
||||
val oldRemoteHostId by remember { mutableStateOf(chatModel.currentRemoteHost.value?.remoteHostId) }
|
||||
LaunchedEffect(remember { chatModel.currentRemoteHost }.value) {
|
||||
@ -325,9 +353,26 @@ fun AddingMobileDevice(showTitle: Boolean, staleQrCode: MutableState<Boolean>, c
|
||||
|
||||
private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState<Boolean>) {
|
||||
ModalManager.start.showModalCloseable { close ->
|
||||
val cachedR = remember { mutableStateOf<CR.RemoteHostStarted?>(null) }
|
||||
val customAddress = rememberSaveable { mutableStateOf<RemoteCtrlAddress?>(null) }
|
||||
val customPort = rememberSaveable { mutableStateOf<Int?>(null) }
|
||||
var editing by rememberSaveable { mutableStateOf(false) }
|
||||
val startRemoteHost = suspend {
|
||||
val r = chatModel.controller.startRemoteHost(
|
||||
rhId = rh.remoteHostId,
|
||||
multicast = controller.appPrefs.offerRemoteMulticast.get(),
|
||||
address = if (customAddress.value?.address != cachedR.address?.address) customAddress.value else cachedR.rh?.bindAddress_ ?: rh.bindAddress_,
|
||||
port = if (customPort.value != cachedR.port) customPort.value else cachedR.rh?.bindPort_ ?: rh.bindPort_
|
||||
)
|
||||
if (r != null) {
|
||||
cachedR.value = r
|
||||
connecting.value = true
|
||||
customAddress.value = cachedR.address
|
||||
customPort.value = cachedR.port
|
||||
chatModel.remoteHostPairing.value = null to RemoteHostSessionState.Starting
|
||||
}
|
||||
}
|
||||
val pairing = remember { chatModel.remoteHostPairing }
|
||||
val invitation = rememberSaveable { mutableStateOf<String?>(null) }
|
||||
val port = rememberSaveable { mutableStateOf<String?>(null) }
|
||||
val sessionCode = when (val state = pairing.value?.second) {
|
||||
is RemoteHostSessionState.PendingConfirmation -> state.sessionCode
|
||||
else -> null
|
||||
@ -339,25 +384,25 @@ private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState
|
||||
}
|
||||
ConnectMobileViewLayout(
|
||||
title = if (cachedSessionCode == null) stringResource(MR.strings.scan_from_mobile) else stringResource(MR.strings.verify_connection),
|
||||
invitation = invitation.value,
|
||||
invitation = cachedR.invitation,
|
||||
deviceName = pairing.value?.first?.hostDeviceName ?: rh.hostDeviceName,
|
||||
sessionCode = cachedSessionCode,
|
||||
port = port.value
|
||||
port = cachedR.value?.ctrlPort,
|
||||
staleQrCode = (cachedR.address != customAddress.value && customAddress.value != null) || (cachedR.port != customPort.value && customPort.value != null),
|
||||
editEnabled = !editing && cachedR.addresses.isNotEmpty(),
|
||||
editClicked = { editing = true },
|
||||
refreshQrCode = {
|
||||
withBGApi {
|
||||
if (chatController.stopRemoteHost(rh.remoteHostId)) {
|
||||
startRemoteHost()
|
||||
editing = false
|
||||
}
|
||||
}
|
||||
},
|
||||
UnderQrLayout = { UnderQrLayout(editing, cachedR, customAddress, customPort) }
|
||||
)
|
||||
var remoteHostId by rememberSaveable { mutableStateOf<Long?>(null) }
|
||||
LaunchedEffect(Unit) {
|
||||
val r = chatModel.controller.startRemoteHost(rh.remoteHostId, controller.appPrefs.offerRemoteMulticast.get())
|
||||
if (r != null) {
|
||||
val (rh_, inv) = r
|
||||
connecting.value = true
|
||||
remoteHostId = rh_?.remoteHostId
|
||||
invitation.value = inv
|
||||
port.value = r.third
|
||||
chatModel.remoteHostPairing.value = null to RemoteHostSessionState.Starting
|
||||
}
|
||||
}
|
||||
LaunchedEffect(remember { chatModel.currentRemoteHost }.value) {
|
||||
if (remoteHostId != null && chatModel.currentRemoteHost.value?.remoteHostId == remoteHostId) {
|
||||
if (cachedR.remoteHostId != null && chatModel.currentRemoteHost.value?.remoteHostId == cachedR.remoteHostId) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
@ -367,10 +412,13 @@ private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState
|
||||
}
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
withBGApi {
|
||||
startRemoteHost()
|
||||
}
|
||||
onDispose {
|
||||
if (remoteHostId != null && chatModel.currentRemoteHost.value?.remoteHostId != remoteHostId) {
|
||||
if (cachedR.remoteHostId != null && chatModel.currentRemoteHost.value?.remoteHostId != cachedR.remoteHostId) {
|
||||
withBGApi {
|
||||
chatController.stopRemoteHost(remoteHostId)
|
||||
chatController.stopRemoteHost(cachedR.remoteHostId)
|
||||
}
|
||||
}
|
||||
chatModel.remoteHostPairing.value = null
|
||||
@ -403,3 +451,50 @@ private fun showConnectedMobileDevice(rh: RemoteHostInfo, disconnectHost: () ->
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UnderQrLayout(editing: Boolean, cachedR: State<CR.RemoteHostStarted?>, customAddress: MutableState<RemoteCtrlAddress?>, customPort: MutableState<Int?>) {
|
||||
if (editing) {
|
||||
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) {
|
||||
ExposedDropDownSetting(
|
||||
cachedR.addresses.map { it to it.address + " (${it.`interface`})" },
|
||||
customAddress,
|
||||
textColor = MaterialTheme.colors.onBackground,
|
||||
minWidth = 250.dp,
|
||||
maxWidth = with(LocalDensity.current) { 250.sp.toDp() },
|
||||
onSelected = {
|
||||
customAddress.value = it
|
||||
}
|
||||
)
|
||||
val portUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||
mutableStateOf(TextFieldValue((customPort.value ?: cachedR.port!!).toString()))
|
||||
}
|
||||
Spacer(Modifier.width(DEFAULT_PADDING))
|
||||
DefaultConfigurableTextField(
|
||||
portUnsaved,
|
||||
stringResource(MR.strings.port_verb),
|
||||
modifier = Modifier.widthIn(max = 100.dp),
|
||||
isValid = { validPort(it) && it.toInt() > 1023 },
|
||||
keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done) }),
|
||||
keyboardType = KeyboardType.Number,
|
||||
)
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { portUnsaved.value.text }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
if (validPort(it) && it.toInt() > 1023) {
|
||||
customPort.value = it.toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val State<CR.RemoteHostStarted?>.rh: RemoteHostInfo? get() = value?.remoteHost_
|
||||
private val State<CR.RemoteHostStarted?>.remoteHostId: Long? get() = value?.remoteHost_?.remoteHostId
|
||||
private val State<CR.RemoteHostStarted?>.invitation: String? get() = value?.invitation
|
||||
private val State<CR.RemoteHostStarted?>.address: RemoteCtrlAddress? get() = value?.localAddrs?.firstOrNull()
|
||||
private val State<CR.RemoteHostStarted?>.addresses: List<RemoteCtrlAddress> get() =
|
||||
(if (controller.appPrefs.developerTools.get()) value?.localAddrs else value?.localAddrs?.filterNot { it.address == "127.0.0.1" }) ?: emptyList()
|
||||
private val State<CR.RemoteHostStarted?>.port: Int? get() = value?.ctrlPort?.toIntOrNull()
|
||||
|
@ -305,7 +305,7 @@ fun SockProxySettings(m: ChatModel) {
|
||||
DefaultConfigurableTextField(
|
||||
hostUnsaved,
|
||||
stringResource(MR.strings.host_verb),
|
||||
modifier = Modifier,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isValid = ::validHost,
|
||||
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
|
||||
keyboardType = KeyboardType.Text,
|
||||
@ -315,7 +315,7 @@ fun SockProxySettings(m: ChatModel) {
|
||||
DefaultConfigurableTextField(
|
||||
portUnsaved,
|
||||
stringResource(MR.strings.port_verb),
|
||||
modifier = Modifier,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isValid = ::validPort,
|
||||
keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done); save() }),
|
||||
keyboardType = KeyboardType.Number,
|
||||
@ -428,7 +428,7 @@ private fun validHost(s: String): Boolean {
|
||||
}
|
||||
|
||||
// https://ihateregex.io/expr/port/
|
||||
private fun validPort(s: String): Boolean {
|
||||
fun validPort(s: String): Boolean {
|
||||
val validPort = Regex("^(6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4})$")
|
||||
return s.isNotBlank() && s.matches(validPort)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user