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)
|
chatModel.remoteHosts.addAll(hosts)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun startRemoteHost(rhId: Long?, multicast: Boolean = true): Triple<RemoteHostInfo?, String, String>? {
|
suspend fun startRemoteHost(rhId: Long?, multicast: Boolean = true, address: RemoteCtrlAddress?, port: Int?): CR.RemoteHostStarted? {
|
||||||
val r = sendCmd(null, CC.StartRemoteHost(rhId, multicast))
|
val r = sendCmd(null, CC.StartRemoteHost(rhId, multicast, address, port))
|
||||||
if (r is CR.RemoteHostStarted) return Triple(r.remoteHost_, r.invitation, r.ctrlPort)
|
if (r is CR.RemoteHostStarted) return r
|
||||||
apiErrorAlert("startRemoteHost", generalGetString(MR.strings.error_alert_title), r)
|
apiErrorAlert("startRemoteHost", generalGetString(MR.strings.error_alert_title), r)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -2248,7 +2248,7 @@ sealed class CC {
|
|||||||
// Remote control
|
// Remote control
|
||||||
class SetLocalDeviceName(val displayName: String): CC()
|
class SetLocalDeviceName(val displayName: String): CC()
|
||||||
class ListRemoteHosts(): 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 SwitchRemoteHost (val remoteHostId: Long?): CC()
|
||||||
class StopRemoteHost(val remoteHostKey: Long?): CC()
|
class StopRemoteHost(val remoteHostKey: Long?): CC()
|
||||||
class DeleteRemoteHost(val remoteHostId: Long): CC()
|
class DeleteRemoteHost(val remoteHostId: Long): CC()
|
||||||
@ -2384,7 +2384,7 @@ sealed class CC {
|
|||||||
is CancelFile -> "/fcancel $fileId"
|
is CancelFile -> "/fcancel $fileId"
|
||||||
is SetLocalDeviceName -> "/set device name $displayName"
|
is SetLocalDeviceName -> "/set device name $displayName"
|
||||||
is ListRemoteHosts -> "/list remote hosts"
|
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 SwitchRemoteHost -> "/switch remote host " + if (remoteHostId == null) "local" else "$remoteHostId"
|
||||||
is StopRemoteHost -> "/stop remote host " + if (remoteHostKey == null) "new" else "$remoteHostKey"
|
is StopRemoteHost -> "/stop remote host " + if (remoteHostKey == null) "new" else "$remoteHostKey"
|
||||||
is DeleteRemoteHost -> "/delete remote host $remoteHostId"
|
is DeleteRemoteHost -> "/delete remote host $remoteHostId"
|
||||||
@ -3606,6 +3606,8 @@ data class RemoteHostInfo(
|
|||||||
val remoteHostId: Long,
|
val remoteHostId: Long,
|
||||||
val hostDeviceName: String,
|
val hostDeviceName: String,
|
||||||
val storePath: String,
|
val storePath: String,
|
||||||
|
val bindAddress_: RemoteCtrlAddress?,
|
||||||
|
val bindPort_: Int?,
|
||||||
val sessionState: RemoteHostSessionState?
|
val sessionState: RemoteHostSessionState?
|
||||||
) {
|
) {
|
||||||
val activeHost: Boolean
|
val activeHost: Boolean
|
||||||
@ -3614,6 +3616,12 @@ data class RemoteHostInfo(
|
|||||||
fun activeHost(): Boolean = chatModel.currentRemoteHost.value?.remoteHostId == remoteHostId
|
fun activeHost(): Boolean = chatModel.currentRemoteHost.value?.remoteHostId == remoteHostId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class RemoteCtrlAddress(
|
||||||
|
val address: String,
|
||||||
|
val `interface`: String
|
||||||
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
sealed class RemoteHostSessionState {
|
sealed class RemoteHostSessionState {
|
||||||
@Serializable @SerialName("starting") object Starting: RemoteHostSessionState()
|
@Serializable @SerialName("starting") object Starting: RemoteHostSessionState()
|
||||||
@ -3848,7 +3856,7 @@ sealed class CR {
|
|||||||
// remote events (desktop)
|
// remote events (desktop)
|
||||||
@Serializable @SerialName("remoteHostList") class RemoteHostList(val remoteHosts: List<RemoteHostInfo>): CR()
|
@Serializable @SerialName("remoteHostList") class RemoteHostList(val remoteHosts: List<RemoteHostInfo>): CR()
|
||||||
@Serializable @SerialName("currentRemoteHost") class CurrentRemoteHost(val remoteHost_: 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("remoteHostSessionCode") class RemoteHostSessionCode(val remoteHost_: RemoteHostInfo?, val sessionCode: String): CR()
|
||||||
@Serializable @SerialName("newRemoteHost") class NewRemoteHost(val remoteHost: RemoteHostInfo): CR()
|
@Serializable @SerialName("newRemoteHost") class NewRemoteHost(val remoteHost: RemoteHostInfo): CR()
|
||||||
@Serializable @SerialName("remoteHostConnected") class RemoteHostConnected(val remoteHost: RemoteHostInfo): CR()
|
@Serializable @SerialName("remoteHostConnected") class RemoteHostConnected(val remoteHost: RemoteHostInfo): CR()
|
||||||
|
@ -152,7 +152,6 @@ fun DefaultConfigurableTextField(
|
|||||||
BasicTextField(
|
BasicTextField(
|
||||||
value = state.value,
|
value = state.value,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
|
||||||
.background(colors.backgroundColor(enabled).value, shape)
|
.background(colors.backgroundColor(enabled).value, shape)
|
||||||
.indicatorLine(enabled, false, interactionSource, colors)
|
.indicatorLine(enabled, false, interactionSource, colors)
|
||||||
.defaultMinSize(
|
.defaultMinSize(
|
||||||
|
@ -10,72 +10,87 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.graphics.painter.Painter
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.*
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import chat.simplex.common.ui.theme.*
|
import chat.simplex.common.ui.theme.*
|
||||||
import chat.simplex.common.views.usersettings.SettingsActionItemWithContent
|
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
|
@Composable
|
||||||
fun <T> ExposedDropDownSettingRow(
|
fun <T> ExposedDropDownSettingRow(
|
||||||
title: String,
|
title: String,
|
||||||
values: List<Pair<T, String>>,
|
values: List<Pair<T, String>>,
|
||||||
selection: State<T>,
|
selection: State<T>,
|
||||||
|
textColor: Color = MaterialTheme.colors.secondary,
|
||||||
label: String? = null,
|
label: String? = null,
|
||||||
icon: Painter? = null,
|
icon: Painter? = null,
|
||||||
iconTint: Color = MaterialTheme.colors.secondary,
|
iconTint: Color = MaterialTheme.colors.secondary,
|
||||||
enabled: State<Boolean> = mutableStateOf(true),
|
enabled: State<Boolean> = mutableStateOf(true),
|
||||||
|
minWidth: Dp = 200.dp,
|
||||||
|
maxWidth: Dp = with(LocalDensity.current) { 180.sp.toDp() },
|
||||||
onSelected: (T) -> Unit
|
onSelected: (T) -> Unit
|
||||||
) {
|
) {
|
||||||
SettingsActionItemWithContent(icon, title, iconColor = iconTint, disabled = !enabled.value) {
|
SettingsActionItemWithContent(icon, title, iconColor = iconTint, disabled = !enabled.value) {
|
||||||
val expanded = remember { mutableStateOf(false) }
|
ExposedDropDownSetting(values, selection ,textColor, label, enabled, minWidth, maxWidth, onSelected)
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,7 +55,7 @@ fun annotatedStringResource(id: StringResource): AnnotatedString {
|
|||||||
@Composable
|
@Composable
|
||||||
fun annotatedStringResource(id: StringResource, vararg args: Any?): AnnotatedString {
|
fun annotatedStringResource(id: StringResource, vararg args: Any?): AnnotatedString {
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
return remember(id) {
|
return remember(id, args) {
|
||||||
escapedHtmlToAnnotatedString(id.localized().format(args = args), density)
|
escapedHtmlToAnnotatedString(id.localized().format(args = args), density)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import SectionView
|
|||||||
import TextIconSpaced
|
import TextIconSpaced
|
||||||
import androidx.compose.foundation.*
|
import androidx.compose.foundation.*
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
@ -17,6 +18,7 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.*
|
import androidx.compose.ui.text.font.*
|
||||||
import androidx.compose.ui.text.input.*
|
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.chatlist.*
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import chat.simplex.common.views.newchat.QRCode
|
import chat.simplex.common.views.newchat.QRCode
|
||||||
import chat.simplex.common.views.usersettings.PreferenceToggle
|
import chat.simplex.common.views.usersettings.*
|
||||||
import chat.simplex.common.views.usersettings.SettingsActionItemWithContent
|
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import dev.icerock.moko.resources.compose.painterResource
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
import dev.icerock.moko.resources.compose.stringResource
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ConnectMobileView() {
|
fun ConnectMobileView() {
|
||||||
@ -156,7 +158,7 @@ fun DeviceNameField(
|
|||||||
DefaultConfigurableTextField(
|
DefaultConfigurableTextField(
|
||||||
state = state,
|
state = state,
|
||||||
placeholder = generalGetString(MR.strings.enter_this_device_name),
|
placeholder = generalGetString(MR.strings.enter_this_device_name),
|
||||||
modifier = Modifier.padding(start = DEFAULT_PADDING),
|
modifier = Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING),
|
||||||
isValid = { true },
|
isValid = { true },
|
||||||
)
|
)
|
||||||
KeyChangeEffect(state.value) {
|
KeyChangeEffect(state.value) {
|
||||||
@ -172,7 +174,10 @@ private fun ConnectMobileViewLayout(
|
|||||||
sessionCode: String?,
|
sessionCode: String?,
|
||||||
port: String?,
|
port: String?,
|
||||||
staleQrCode: Boolean = false,
|
staleQrCode: Boolean = false,
|
||||||
refreshQrCode: () -> Unit = {}
|
editEnabled: Boolean = false,
|
||||||
|
editClicked: () -> Unit = {},
|
||||||
|
refreshQrCode: () -> Unit = {},
|
||||||
|
UnderQrLayout: @Composable () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
|
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.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) {
|
if (remember { controller.appPrefs.developerTools.state }.value) {
|
||||||
val clipboard = LocalClipboardManager.current
|
val clipboard = LocalClipboardManager.current
|
||||||
@ -259,14 +275,22 @@ private fun showAddingMobileDevice(connecting: MutableState<Boolean>) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AddingMobileDevice(showTitle: Boolean, staleQrCode: MutableState<Boolean>, connecting: MutableState<Boolean>, close: () -> Unit) {
|
fun AddingMobileDevice(showTitle: Boolean, staleQrCode: MutableState<Boolean>, connecting: MutableState<Boolean>, close: () -> Unit) {
|
||||||
val invitation = rememberSaveable { mutableStateOf<String?>(null) }
|
val cachedR = remember { mutableStateOf<CR.RemoteHostStarted?>(null) }
|
||||||
val port = rememberSaveable { mutableStateOf<String?>(null) }
|
val customAddress = rememberSaveable { mutableStateOf<RemoteCtrlAddress?>(null) }
|
||||||
|
val customPort = rememberSaveable { mutableStateOf<Int?>(null) }
|
||||||
|
var editing by rememberSaveable { mutableStateOf(false) }
|
||||||
val startRemoteHost = suspend {
|
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) {
|
if (r != null) {
|
||||||
|
cachedR.value = r
|
||||||
connecting.value = true
|
connecting.value = true
|
||||||
invitation.value = r.second
|
customAddress.value = cachedR.address
|
||||||
port.value = r.third
|
customPort.value = cachedR.port
|
||||||
chatModel.remoteHostPairing.value = null to RemoteHostSessionState.Starting
|
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
|
val remoteDeviceName = pairing.value?.first?.hostDeviceName
|
||||||
ConnectMobileViewLayout(
|
ConnectMobileViewLayout(
|
||||||
title = if (!showTitle) null else if (cachedSessionCode == null) stringResource(MR.strings.link_a_mobile) else stringResource(MR.strings.verify_connection),
|
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,
|
deviceName = remoteDeviceName,
|
||||||
sessionCode = cachedSessionCode,
|
sessionCode = cachedSessionCode,
|
||||||
port = port.value,
|
port = cachedR.value?.ctrlPort,
|
||||||
staleQrCode = staleQrCode.value,
|
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 = {
|
refreshQrCode = {
|
||||||
withBGApi {
|
withBGApi {
|
||||||
if (chatController.stopRemoteHost(null)) {
|
if (chatController.stopRemoteHost(null)) {
|
||||||
startRemoteHost()
|
startRemoteHost()
|
||||||
staleQrCode.value = false
|
staleQrCode.value = false
|
||||||
|
editing = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
UnderQrLayout = { UnderQrLayout(editing, cachedR, customAddress, customPort) }
|
||||||
)
|
)
|
||||||
val oldRemoteHostId by remember { mutableStateOf(chatModel.currentRemoteHost.value?.remoteHostId) }
|
val oldRemoteHostId by remember { mutableStateOf(chatModel.currentRemoteHost.value?.remoteHostId) }
|
||||||
LaunchedEffect(remember { chatModel.currentRemoteHost }.value) {
|
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>) {
|
private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState<Boolean>) {
|
||||||
ModalManager.start.showModalCloseable { close ->
|
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 pairing = remember { chatModel.remoteHostPairing }
|
||||||
val invitation = rememberSaveable { mutableStateOf<String?>(null) }
|
|
||||||
val port = rememberSaveable { mutableStateOf<String?>(null) }
|
|
||||||
val sessionCode = when (val state = pairing.value?.second) {
|
val sessionCode = when (val state = pairing.value?.second) {
|
||||||
is RemoteHostSessionState.PendingConfirmation -> state.sessionCode
|
is RemoteHostSessionState.PendingConfirmation -> state.sessionCode
|
||||||
else -> null
|
else -> null
|
||||||
@ -339,25 +384,25 @@ private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState
|
|||||||
}
|
}
|
||||||
ConnectMobileViewLayout(
|
ConnectMobileViewLayout(
|
||||||
title = if (cachedSessionCode == null) stringResource(MR.strings.scan_from_mobile) else stringResource(MR.strings.verify_connection),
|
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,
|
deviceName = pairing.value?.first?.hostDeviceName ?: rh.hostDeviceName,
|
||||||
sessionCode = cachedSessionCode,
|
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) {
|
LaunchedEffect(remember { chatModel.currentRemoteHost }.value) {
|
||||||
if (remoteHostId != null && chatModel.currentRemoteHost.value?.remoteHostId == remoteHostId) {
|
if (cachedR.remoteHostId != null && chatModel.currentRemoteHost.value?.remoteHostId == cachedR.remoteHostId) {
|
||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -367,10 +412,13 @@ private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
|
withBGApi {
|
||||||
|
startRemoteHost()
|
||||||
|
}
|
||||||
onDispose {
|
onDispose {
|
||||||
if (remoteHostId != null && chatModel.currentRemoteHost.value?.remoteHostId != remoteHostId) {
|
if (cachedR.remoteHostId != null && chatModel.currentRemoteHost.value?.remoteHostId != cachedR.remoteHostId) {
|
||||||
withBGApi {
|
withBGApi {
|
||||||
chatController.stopRemoteHost(remoteHostId)
|
chatController.stopRemoteHost(cachedR.remoteHostId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
chatModel.remoteHostPairing.value = null
|
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(
|
DefaultConfigurableTextField(
|
||||||
hostUnsaved,
|
hostUnsaved,
|
||||||
stringResource(MR.strings.host_verb),
|
stringResource(MR.strings.host_verb),
|
||||||
modifier = Modifier,
|
modifier = Modifier.fillMaxWidth(),
|
||||||
isValid = ::validHost,
|
isValid = ::validHost,
|
||||||
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
|
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
|
||||||
keyboardType = KeyboardType.Text,
|
keyboardType = KeyboardType.Text,
|
||||||
@ -315,7 +315,7 @@ fun SockProxySettings(m: ChatModel) {
|
|||||||
DefaultConfigurableTextField(
|
DefaultConfigurableTextField(
|
||||||
portUnsaved,
|
portUnsaved,
|
||||||
stringResource(MR.strings.port_verb),
|
stringResource(MR.strings.port_verb),
|
||||||
modifier = Modifier,
|
modifier = Modifier.fillMaxWidth(),
|
||||||
isValid = ::validPort,
|
isValid = ::validPort,
|
||||||
keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done); save() }),
|
keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done); save() }),
|
||||||
keyboardType = KeyboardType.Number,
|
keyboardType = KeyboardType.Number,
|
||||||
@ -428,7 +428,7 @@ private fun validHost(s: String): Boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// https://ihateregex.io/expr/port/
|
// 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})$")
|
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)
|
return s.isNotBlank() && s.matches(validPort)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user