android: UI for remote connections (#3395)
* android: UI for remote connections * camera permissions * eol --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
parent
f9e5a56e1a
commit
8f0538e756
@ -16,6 +16,8 @@ import kotlin.random.Random
|
||||
|
||||
actual val appPlatform = AppPlatform.ANDROID
|
||||
|
||||
actual val deviceName = android.os.Build.MODEL
|
||||
|
||||
var isAppOnForeground: Boolean = false
|
||||
|
||||
@Suppress("ConstantLocale")
|
||||
|
@ -38,7 +38,7 @@ actual class RecorderNative: RecorderInterface {
|
||||
rec.setAudioSamplingRate(16000)
|
||||
rec.setAudioEncodingBitRate(32000)
|
||||
rec.setMaxDuration(MAX_VOICE_MILLIS_FOR_SENDING)
|
||||
val fileToSave = File.createTempFile(generateNewFileName("voice", "${RecorderInterface.extension}_"), ".tmp", tmpDir)
|
||||
val fileToSave = File.createTempFile(generateNewFileName("voice", "${RecorderInterface.extension}_", tmpDir), ".tmp", tmpDir)
|
||||
fileToSave.deleteOnExit()
|
||||
val path = fileToSave.absolutePath
|
||||
filePath = path
|
||||
|
@ -1,16 +0,0 @@
|
||||
package chat.simplex.common.views.chat
|
||||
|
||||
import android.Manifest
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import chat.simplex.common.views.chat.ScanCodeLayout
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
|
||||
@Composable
|
||||
actual fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) {
|
||||
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
|
||||
LaunchedEffect(Unit) {
|
||||
cameraPermissionState.launchPermissionRequest()
|
||||
}
|
||||
ScanCodeLayout(verifyCode, close)
|
||||
}
|
@ -295,7 +295,7 @@ actual suspend fun saveTempImageUncompressed(image: ImageBitmap, asPng: Boolean)
|
||||
return try {
|
||||
val ext = if (asPng) "png" else "jpg"
|
||||
tmpDir.mkdir()
|
||||
return File(tmpDir.absolutePath + File.separator + generateNewFileName("IMG", ext)).apply {
|
||||
return File(tmpDir.absolutePath + File.separator + generateNewFileName("IMG", ext, tmpDir)).apply {
|
||||
outputStream().use { out ->
|
||||
image.asAndroidBitmap().compress(if (asPng) Bitmap.CompressFormat.PNG else Bitmap.CompressFormat.JPEG, 85, out)
|
||||
out.flush()
|
||||
|
@ -1,5 +1,6 @@
|
||||
package chat.simplex.common.views.newchat
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.util.Log
|
||||
import android.view.ViewGroup
|
||||
@ -19,6 +20,7 @@ import boofcv.android.ConvertCameraImage
|
||||
import boofcv.factory.fiducial.FactoryFiducial
|
||||
import boofcv.struct.image.GrayU8
|
||||
import chat.simplex.common.platform.TAG
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import java.util.concurrent.*
|
||||
|
||||
@ -26,6 +28,10 @@ import java.util.concurrent.*
|
||||
|
||||
@Composable
|
||||
actual fun QRCodeScanner(onBarcode: (String) -> Unit) {
|
||||
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
|
||||
LaunchedEffect(Unit) {
|
||||
cameraPermissionState.launchPermissionRequest()
|
||||
}
|
||||
val context = LocalContext.current
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
var preview by remember { mutableStateOf<Preview?>(null) }
|
||||
|
@ -27,6 +27,7 @@ import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.json.*
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.net.URLDecoder
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import java.util.*
|
||||
@ -111,6 +112,7 @@ object ChatModel {
|
||||
val remoteHosts = mutableStateListOf<RemoteHostInfo>()
|
||||
val currentRemoteHost = mutableStateOf<RemoteHostInfo?>(null)
|
||||
val newRemoteHostPairing = mutableStateOf<Pair<RemoteHostInfo?, RemoteHostSessionState>?>(null)
|
||||
val remoteCtrlSession = mutableStateOf<RemoteCtrlSession?>(null)
|
||||
|
||||
fun getUser(userId: Long): User? = if (currentUser.value?.userId == userId) {
|
||||
currentUser.value
|
||||
@ -598,7 +600,7 @@ object ChatModel {
|
||||
terminalItems.add(item)
|
||||
}
|
||||
|
||||
fun connectedToRemote(): Boolean = currentRemoteHost.value != null
|
||||
fun connectedToRemote(): Boolean = currentRemoteHost.value != null || remoteCtrlSession.value?.active == true
|
||||
}
|
||||
|
||||
enum class ChatType(val type: String) {
|
||||
@ -2347,7 +2349,7 @@ data class CryptoFile(
|
||||
companion object {
|
||||
fun plain(f: String): CryptoFile = CryptoFile(f, null)
|
||||
|
||||
fun desktopPlain(f: URI): CryptoFile = CryptoFile(f.rawPath, null)
|
||||
fun desktopPlain(f: URI): CryptoFile = CryptoFile(URLDecoder.decode(f.rawPath, "UTF-8"), null)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2907,8 +2909,18 @@ enum class NotificationPreviewMode {
|
||||
data class RemoteCtrlSession(
|
||||
val ctrlAppInfo: CtrlAppInfo,
|
||||
val appVersion: String,
|
||||
val sessionState: RemoteCtrlSessionState
|
||||
)
|
||||
val sessionState: UIRemoteCtrlSessionState
|
||||
) {
|
||||
val active: Boolean
|
||||
get () = sessionState is UIRemoteCtrlSessionState.Connected
|
||||
|
||||
val sessionCode: String?
|
||||
get() = when (val s = sessionState) {
|
||||
is UIRemoteCtrlSessionState.PendingConfirmation -> s.sessionCode
|
||||
is UIRemoteCtrlSessionState.Connected -> s.sessionCode
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class RemoteCtrlSessionState {
|
||||
@ -2917,3 +2929,10 @@ sealed class RemoteCtrlSessionState {
|
||||
@Serializable @SerialName("pendingConfirmation") data class PendingConfirmation(val sessionCode: String): RemoteCtrlSessionState()
|
||||
@Serializable @SerialName("connected") data class Connected(val sessionCode: String): RemoteCtrlSessionState()
|
||||
}
|
||||
|
||||
sealed class UIRemoteCtrlSessionState {
|
||||
@Serializable @SerialName("starting") object Starting: UIRemoteCtrlSessionState()
|
||||
@Serializable @SerialName("connecting") data class Connecting(val remoteCtrl_: RemoteCtrlInfo? = null): UIRemoteCtrlSessionState()
|
||||
@Serializable @SerialName("pendingConfirmation") data class PendingConfirmation(val remoteCtrl_: RemoteCtrlInfo? = null, val sessionCode: String): UIRemoteCtrlSessionState()
|
||||
@Serializable @SerialName("connected") data class Connected(val remoteCtrl: RemoteCtrlInfo, val sessionCode: String): UIRemoteCtrlSessionState()
|
||||
}
|
||||
|
@ -24,7 +24,6 @@ import kotlinx.serialization.*
|
||||
import kotlinx.serialization.builtins.MapSerializer
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.json.*
|
||||
import java.io.File
|
||||
import java.util.Date
|
||||
|
||||
typealias ChatCtrl = Long
|
||||
@ -167,7 +166,11 @@ class AppPreferences {
|
||||
val whatsNewVersion = mkStrPreference(SHARED_PREFS_WHATS_NEW_VERSION, null)
|
||||
val lastMigratedVersionCode = mkIntPreference(SHARED_PREFS_LAST_MIGRATED_VERSION_CODE, 0)
|
||||
val customDisappearingMessageTime = mkIntPreference(SHARED_PREFS_CUSTOM_DISAPPEARING_MESSAGE_TIME, 300)
|
||||
val deviceNameForRemoteAccess = mkStrPreference(SHARED_PREFS_DEVICE_NAME_FOR_REMOTE_ACCESS, "Desktop")
|
||||
val deviceNameForRemoteAccess = mkStrPreference(SHARED_PREFS_DEVICE_NAME_FOR_REMOTE_ACCESS, deviceName)
|
||||
|
||||
val confirmRemoteSessions = mkBoolPreference(SHARED_PREFS_CONFIRM_REMOTE_SESSIONS, false)
|
||||
val connectRemoteViaMulticast = mkBoolPreference(SHARED_PREFS_CONNECT_REMOTE_VIA_MULTICAST, false)
|
||||
val offerRemoteMulticast = mkBoolPreference(SHARED_PREFS_OFFER_REMOTE_MULTICAST, true)
|
||||
|
||||
private fun mkIntPreference(prefName: String, default: Int) =
|
||||
SharedPreference(
|
||||
@ -309,6 +312,9 @@ class AppPreferences {
|
||||
private const val SHARED_PREFS_LAST_MIGRATED_VERSION_CODE = "LastMigratedVersionCode"
|
||||
private const val SHARED_PREFS_CUSTOM_DISAPPEARING_MESSAGE_TIME = "CustomDisappearingMessageTime"
|
||||
private const val SHARED_PREFS_DEVICE_NAME_FOR_REMOTE_ACCESS = "DeviceNameForRemoteAccess"
|
||||
private const val SHARED_PREFS_CONFIRM_REMOTE_SESSIONS = "ConfirmRemoteSessions"
|
||||
private const val SHARED_PREFS_CONNECT_REMOTE_VIA_MULTICAST = "ConnectRemoteViaMulticast"
|
||||
private const val SHARED_PREFS_OFFER_REMOTE_MULTICAST = "OfferRemoteMulticast"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1430,18 +1436,23 @@ object ChatController {
|
||||
|
||||
suspend fun getRemoteFile(rhId: Long, file: RemoteFile): Boolean = sendCommandOkResp(CC.GetRemoteFile(rhId, file))
|
||||
|
||||
suspend fun connectRemoteCtrl(invitation: String): SomeRemoteCtrl? {
|
||||
val r = sendCmd(CC.ConnectRemoteCtrl(invitation))
|
||||
if (r is CR.RemoteCtrlConnecting) return SomeRemoteCtrl(r.remoteCtrl_, r.ctrlAppInfo, r.appVersion)
|
||||
apiErrorAlert("connectRemoteCtrl", generalGetString(MR.strings.error_alert_title), r)
|
||||
return null
|
||||
suspend fun connectRemoteCtrl(desktopAddress: String): Pair<SomeRemoteCtrl?, CR.ChatCmdError?> {
|
||||
val r = sendCmd(CC.ConnectRemoteCtrl(desktopAddress))
|
||||
if (r is CR.RemoteCtrlConnecting) return SomeRemoteCtrl(r.remoteCtrl_, r.ctrlAppInfo, r.appVersion) to null
|
||||
else if (r is CR.ChatCmdError) return null to r
|
||||
else throw Exception("connectRemoteCtrl error: ${r.responseType} ${r.details}")
|
||||
}
|
||||
|
||||
suspend fun findKnownRemoteCtrl(): Boolean = sendCommandOkResp(CC.FindKnownRemoteCtrl())
|
||||
|
||||
suspend fun confirmRemoteCtrl(rhId: Long): Boolean = sendCommandOkResp(CC.ConfirmRemoteCtrl(rhId))
|
||||
suspend fun confirmRemoteCtrl(rcId: Long): Boolean = sendCommandOkResp(CC.ConfirmRemoteCtrl(rcId))
|
||||
|
||||
suspend fun verifyRemoteCtrlSession(sessionCode: String): Boolean = sendCommandOkResp(CC.VerifyRemoteCtrlSession(sessionCode))
|
||||
suspend fun verifyRemoteCtrlSession(sessionCode: String): RemoteCtrlInfo? {
|
||||
val r = sendCmd(CC.VerifyRemoteCtrlSession(sessionCode))
|
||||
if (r is CR.RemoteCtrlConnected) return r.remoteCtrl
|
||||
apiErrorAlert("verifyRemoteCtrlSession", generalGetString(MR.strings.error_alert_title), r)
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun listRemoteCtrls(): List<RemoteCtrlInfo>? {
|
||||
val r = sendCmd(CC.ListRemoteCtrls())
|
||||
@ -1843,6 +1854,22 @@ object ChatController {
|
||||
chatModel.newRemoteHostPairing.value = null
|
||||
switchUIRemoteHost(null)
|
||||
}
|
||||
is CR.RemoteCtrlFound -> {
|
||||
// TODO multicast
|
||||
Log.d(TAG, "RemoteCtrlFound: ${r.remoteCtrl}")
|
||||
}
|
||||
is CR.RemoteCtrlSessionCode -> {
|
||||
val state = UIRemoteCtrlSessionState.PendingConfirmation(remoteCtrl_ = r.remoteCtrl_, sessionCode = r.sessionCode)
|
||||
chatModel.remoteCtrlSession.value = chatModel.remoteCtrlSession.value?.copy(sessionState = state)
|
||||
}
|
||||
is CR.RemoteCtrlConnected -> {
|
||||
// TODO currently it is returned in response to command, so it is redundant
|
||||
val state = UIRemoteCtrlSessionState.Connected(remoteCtrl = r.remoteCtrl, sessionCode = chatModel.remoteCtrlSession.value?.sessionCode ?: "")
|
||||
chatModel.remoteCtrlSession.value = chatModel.remoteCtrlSession.value?.copy(sessionState = state)
|
||||
}
|
||||
is CR.RemoteCtrlStopped -> {
|
||||
switchToLocalSession()
|
||||
}
|
||||
else ->
|
||||
Log.d(TAG , "unsupported event: ${r.responseType}")
|
||||
}
|
||||
@ -1866,6 +1893,23 @@ object ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
fun switchToLocalSession() {
|
||||
val m = chatModel
|
||||
m.remoteCtrlSession.value = null
|
||||
withBGApi {
|
||||
val users = listUsers()
|
||||
m.users.clear()
|
||||
m.users.addAll(users)
|
||||
getUserChatData()
|
||||
val statuses = apiGetNetworkStatuses()
|
||||
if (statuses != null) {
|
||||
chatModel.networkStatuses.clear()
|
||||
val ss = statuses.associate { it.agentConnId to it.networkStatus }.toMap()
|
||||
chatModel.networkStatuses.putAll(ss)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun activeUser(rhId: Long?, user: UserLike): Boolean =
|
||||
rhId == chatModel.currentRemoteHost.value?.remoteHostId && user.userId == chatModel.currentUser.value?.userId
|
||||
|
||||
@ -3474,7 +3518,10 @@ data class RemoteCtrlInfo (
|
||||
val remoteCtrlId: Long,
|
||||
val ctrlDeviceName: String,
|
||||
val sessionState: RemoteCtrlSessionState?
|
||||
)
|
||||
) {
|
||||
val deviceViewName: String
|
||||
get() = ctrlDeviceName.ifEmpty { remoteCtrlId.toString() }
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class RemoteHostInfo(
|
||||
@ -4558,6 +4605,7 @@ sealed class AgentErrorType {
|
||||
is SMP -> "SMP ${smpErr.string}"
|
||||
// is NTF -> "NTF ${ntfErr.string}"
|
||||
is XFTP -> "XFTP ${xftpErr.string}"
|
||||
is RCP -> "RCP ${rcpErr.string}"
|
||||
is BROKER -> "BROKER ${brokerErr.string}"
|
||||
is AGENT -> "AGENT ${agentErr.string}"
|
||||
is INTERNAL -> "INTERNAL $internalErr"
|
||||
@ -4568,6 +4616,7 @@ sealed class AgentErrorType {
|
||||
@Serializable @SerialName("SMP") class SMP(val smpErr: SMPErrorType): AgentErrorType()
|
||||
// @Serializable @SerialName("NTF") class NTF(val ntfErr: SMPErrorType): AgentErrorType()
|
||||
@Serializable @SerialName("XFTP") class XFTP(val xftpErr: XFTPErrorType): AgentErrorType()
|
||||
@Serializable @SerialName("XFTP") class RCP(val rcpErr: RCErrorType): AgentErrorType()
|
||||
@Serializable @SerialName("BROKER") class BROKER(val brokerAddress: String, val brokerErr: BrokerErrorType): AgentErrorType()
|
||||
@Serializable @SerialName("AGENT") class AGENT(val agentErr: SMPAgentError): AgentErrorType()
|
||||
@Serializable @SerialName("INTERNAL") class INTERNAL(val internalErr: String): AgentErrorType()
|
||||
@ -4738,6 +4787,38 @@ sealed class XFTPErrorType {
|
||||
@Serializable @SerialName("INTERNAL") object INTERNAL: XFTPErrorType()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class RCErrorType {
|
||||
val string: String get() = when (this) {
|
||||
is INTERNAL -> "INTERNAL $internalErr"
|
||||
is IDENTITY -> "IDENTITY"
|
||||
is NO_LOCAL_ADDRESS -> "NO_LOCAL_ADDRESS"
|
||||
is TLS_START_FAILED -> "TLS_START_FAILED"
|
||||
is EXCEPTION -> "EXCEPTION $EXCEPTION"
|
||||
is CTRL_AUTH -> "CTRL_AUTH"
|
||||
is CTRL_NOT_FOUND -> "CTRL_NOT_FOUND"
|
||||
is CTRL_ERROR -> "CTRL_ERROR $ctrlErr"
|
||||
is VERSION -> "VERSION"
|
||||
is ENCRYPT -> "ENCRYPT"
|
||||
is DECRYPT -> "DECRYPT"
|
||||
is BLOCK_SIZE -> "BLOCK_SIZE"
|
||||
is SYNTAX -> "SYNTAX $syntaxErr"
|
||||
}
|
||||
@Serializable @SerialName("internal") data class INTERNAL(val internalErr: String): RCErrorType()
|
||||
@Serializable @SerialName("identity") object IDENTITY: RCErrorType()
|
||||
@Serializable @SerialName("noLocalAddress") object NO_LOCAL_ADDRESS: RCErrorType()
|
||||
@Serializable @SerialName("tlsStartFailed") object TLS_START_FAILED: RCErrorType()
|
||||
@Serializable @SerialName("exception") data class EXCEPTION(val exception: String): RCErrorType()
|
||||
@Serializable @SerialName("ctrlAuth") object CTRL_AUTH: RCErrorType()
|
||||
@Serializable @SerialName("ctrlNotFound") object CTRL_NOT_FOUND: RCErrorType()
|
||||
@Serializable @SerialName("ctrlError") data class CTRL_ERROR(val ctrlErr: String): RCErrorType()
|
||||
@Serializable @SerialName("version") object VERSION: RCErrorType()
|
||||
@Serializable @SerialName("encrypt") object ENCRYPT: RCErrorType()
|
||||
@Serializable @SerialName("decrypt") object DECRYPT: RCErrorType()
|
||||
@Serializable @SerialName("blockSize") object BLOCK_SIZE: RCErrorType()
|
||||
@Serializable @SerialName("syntax") data class SYNTAX(val syntaxErr: String): RCErrorType()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class ArchiveError {
|
||||
val string: String get() = when (this) {
|
||||
@ -4772,22 +4853,21 @@ sealed class RemoteHostError {
|
||||
sealed class RemoteCtrlError {
|
||||
val string: String get() = when (this) {
|
||||
is Inactive -> "inactive"
|
||||
is BadState -> "badState"
|
||||
is Busy -> "busy"
|
||||
is Timeout -> "timeout"
|
||||
is Disconnected -> "disconnected"
|
||||
is ConnectionLost -> "connectionLost"
|
||||
is CertificateExpired -> "certificateExpired"
|
||||
is CertificateUntrusted -> "certificateUntrusted"
|
||||
is BadFingerprint -> "badFingerprint"
|
||||
is BadInvitation -> "badInvitation"
|
||||
is BadVersion -> "badVersion"
|
||||
}
|
||||
@Serializable @SerialName("inactive") object Inactive: RemoteCtrlError()
|
||||
@Serializable @SerialName("badState") object BadState: RemoteCtrlError()
|
||||
@Serializable @SerialName("busy") object Busy: RemoteCtrlError()
|
||||
@Serializable @SerialName("timeout") object Timeout: RemoteCtrlError()
|
||||
@Serializable @SerialName("disconnected") class Disconnected(val remoteCtrlId: Long, val reason: String): RemoteCtrlError()
|
||||
@Serializable @SerialName("connectionLost") class ConnectionLost(val remoteCtrlId: Long, val reason: String): RemoteCtrlError()
|
||||
@Serializable @SerialName("certificateExpired") class CertificateExpired(val remoteCtrlId: Long): RemoteCtrlError()
|
||||
@Serializable @SerialName("certificateUntrusted") class CertificateUntrusted(val remoteCtrlId: Long): RemoteCtrlError()
|
||||
@Serializable @SerialName("badFingerprint") object BadFingerprint: RemoteCtrlError()
|
||||
@Serializable @SerialName("badInvitation") object BadInvitation: RemoteCtrlError()
|
||||
@Serializable @SerialName("badVersion") data class BadVersion(val appVersion: String): RemoteCtrlError()
|
||||
//@Serializable @SerialName("protocolError") data class ProtocolError(val protocolError: RemoteProtocolError): RemoteCtrlError()
|
||||
}
|
||||
|
||||
enum class NotificationsMode() {
|
||||
|
@ -18,6 +18,8 @@ enum class AppPlatform {
|
||||
|
||||
expect val appPlatform: AppPlatform
|
||||
|
||||
expect val deviceName: String
|
||||
|
||||
val appVersionInfo: Pair<String, Int?> = if (appPlatform == AppPlatform.ANDROID)
|
||||
BuildConfigCommon.ANDROID_VERSION_NAME to BuildConfigCommon.ANDROID_VERSION_CODE
|
||||
else
|
||||
|
@ -11,10 +11,7 @@ import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
|
||||
@Composable
|
||||
expect fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit)
|
||||
|
||||
@Composable
|
||||
fun ScanCodeLayout(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) {
|
||||
fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
|
@ -26,6 +26,7 @@ import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.remote.ConnectDesktopView
|
||||
import chat.simplex.common.views.remote.connectMobileDevice
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
@ -42,6 +43,7 @@ fun UserPicker(
|
||||
showSettings: Boolean = true,
|
||||
showCancel: Boolean = false,
|
||||
cancelClicked: () -> Unit = {},
|
||||
useFromDesktopClicked: () -> Unit = {},
|
||||
settingsClicked: () -> Unit = {},
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
@ -203,6 +205,15 @@ fun UserPicker(
|
||||
Divider(Modifier.requiredHeight(1.dp))
|
||||
}
|
||||
}
|
||||
if (appPlatform.isAndroid) {
|
||||
UseFromDesktopPickerItem {
|
||||
ModalManager.start.showCustomModal { close ->
|
||||
ConnectDesktopView(close)
|
||||
}
|
||||
userPickerState.value = AnimatedViewState.GONE
|
||||
}
|
||||
Divider(Modifier.requiredHeight(1.dp))
|
||||
}
|
||||
if (showSettings) {
|
||||
SettingsPickerItem(settingsClicked)
|
||||
}
|
||||
@ -363,6 +374,16 @@ fun LocalDeviceRow(active: Boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UseFromDesktopPickerItem(onClick: () -> Unit) {
|
||||
SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) {
|
||||
val text = generalGetString(MR.strings.settings_section_title_use_from_desktop).lowercase().capitalize(Locale.current)
|
||||
Icon(painterResource(MR.images.ic_desktop), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
||||
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
|
||||
Text(text, color = if (isInDarkTheme()) MenuTextColorDark else Color.Black)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsPickerItem(onClick: () -> Unit) {
|
||||
SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) {
|
||||
|
@ -52,6 +52,14 @@ fun annotatedStringResource(id: StringResource): AnnotatedString {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun annotatedStringResource(id: StringResource, vararg args: Any?): AnnotatedString {
|
||||
val density = LocalDensity.current
|
||||
return remember(id) {
|
||||
escapedHtmlToAnnotatedString(id.localized().format(args), density)
|
||||
}
|
||||
}
|
||||
|
||||
// maximum image file size to be auto-accepted
|
||||
const val MAX_IMAGE_SIZE: Long = 261_120 // 255KB
|
||||
const val MAX_IMAGE_SIZE_AUTO_RCV: Long = MAX_IMAGE_SIZE * 2
|
||||
|
@ -0,0 +1,472 @@
|
||||
package chat.simplex.common.views.remote
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionDividerSpaced
|
||||
import SectionItemView
|
||||
import SectionItemViewLongClickable
|
||||
import SectionSpacer
|
||||
import SectionView
|
||||
import TextIconSpaced
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.switchToLocalSession
|
||||
import chat.simplex.common.model.ChatModel.connectedToRemote
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF
|
||||
import chat.simplex.common.views.chat.item.ItemAction
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.newchat.QRCodeScanner
|
||||
import chat.simplex.common.views.usersettings.PreferenceToggle
|
||||
import chat.simplex.common.views.usersettings.SettingsActionItem
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
|
||||
@Composable
|
||||
fun ConnectDesktopView(close: () -> Unit) {
|
||||
val deviceName = remember { controller.appPrefs.deviceNameForRemoteAccess.state }
|
||||
val closeWithAlert = {
|
||||
if (!connectedToRemote()) {
|
||||
close()
|
||||
} else {
|
||||
showDisconnectDesktopAlert(close)
|
||||
}
|
||||
}
|
||||
ModalView(close = closeWithAlert) {
|
||||
ConnectDesktopLayout(
|
||||
deviceName = deviceName.value!!,
|
||||
)
|
||||
}
|
||||
val ntfModeService = remember { chatModel.controller.appPrefs.notificationsMode.get() == NotificationsMode.SERVICE }
|
||||
DisposableEffect(Unit) {
|
||||
withBGApi {
|
||||
if (!ntfModeService) platform.androidServiceStart()
|
||||
}
|
||||
onDispose {
|
||||
if (!ntfModeService) platform.androidServiceSafeStop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConnectDesktopLayout(deviceName: String) {
|
||||
val sessionAddress = remember { mutableStateOf("") }
|
||||
val remoteCtrls = remember { mutableStateListOf<RemoteCtrlInfo>() }
|
||||
val session = remember { chatModel.remoteCtrlSession }.value
|
||||
Column(
|
||||
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
if (session != null) {
|
||||
when (session.sessionState) {
|
||||
is UIRemoteCtrlSessionState.Starting -> ConnectingDesktop(session, null)
|
||||
is UIRemoteCtrlSessionState.Connecting -> ConnectingDesktop(session, session.sessionState.remoteCtrl_)
|
||||
is UIRemoteCtrlSessionState.PendingConfirmation -> {
|
||||
if (controller.appPrefs.confirmRemoteSessions.get() || session.sessionState.remoteCtrl_ == null) {
|
||||
VerifySession(session, session.sessionState.remoteCtrl_, session.sessionCode!!, remoteCtrls)
|
||||
} else {
|
||||
ConnectingDesktop(session, session.sessionState.remoteCtrl_)
|
||||
LaunchedEffect(Unit) {
|
||||
verifyDesktopSessionCode(remoteCtrls, session.sessionCode!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is UIRemoteCtrlSessionState.Connected -> ActiveSession(session, session.sessionState.remoteCtrl)
|
||||
}
|
||||
} else {
|
||||
ConnectDesktop(deviceName, remoteCtrls, sessionAddress)
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
setDeviceName(deviceName)
|
||||
updateRemoteCtrls(remoteCtrls)
|
||||
onDispose {
|
||||
if (chatModel.remoteCtrlSession.value != null) {
|
||||
disconnectDesktop()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConnectDesktop(deviceName: String, remoteCtrls: SnapshotStateList<RemoteCtrlInfo>, sessionAddress: MutableState<String>) {
|
||||
AppBarTitle(stringResource(MR.strings.connect_to_desktop))
|
||||
SectionView(stringResource(MR.strings.this_device_name).uppercase()) {
|
||||
DevicesView(deviceName, remoteCtrls) {
|
||||
if (it != "") {
|
||||
setDeviceName(it)
|
||||
controller.appPrefs.deviceNameForRemoteAccess.set(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
ScanDesktopAddressView(sessionAddress)
|
||||
if (controller.appPrefs.developerTools.get()) {
|
||||
SectionSpacer()
|
||||
DesktopAddressView(sessionAddress)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConnectingDesktop(session: RemoteCtrlSession, rc: RemoteCtrlInfo?) {
|
||||
AppBarTitle(stringResource(MR.strings.connecting_to_desktop))
|
||||
SectionView(stringResource(MR.strings.connecting_to_desktop).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
CtrlDeviceNameText(session, rc)
|
||||
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
|
||||
CtrlDeviceVersionText(session)
|
||||
}
|
||||
|
||||
if (session.sessionCode != null) {
|
||||
SectionSpacer()
|
||||
SectionView(stringResource(MR.strings.session_code).uppercase()) {
|
||||
SessionCodeText(session.sessionCode!!)
|
||||
}
|
||||
}
|
||||
|
||||
SectionSpacer()
|
||||
|
||||
SectionView {
|
||||
DisconnectButton(::disconnectDesktop)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VerifySession(session: RemoteCtrlSession, rc: RemoteCtrlInfo?, sessCode: String, remoteCtrls: SnapshotStateList<RemoteCtrlInfo>) {
|
||||
AppBarTitle(stringResource(MR.strings.verify_connection))
|
||||
SectionView(stringResource(MR.strings.connected_to_desktop).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
CtrlDeviceNameText(session, rc)
|
||||
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
|
||||
CtrlDeviceVersionText(session)
|
||||
}
|
||||
|
||||
SectionSpacer()
|
||||
|
||||
SectionView(stringResource(MR.strings.verify_code_with_desktop).uppercase()) {
|
||||
SessionCodeText(sessCode)
|
||||
}
|
||||
|
||||
SectionSpacer()
|
||||
|
||||
SectionItemView({ verifyDesktopSessionCode(remoteCtrls, sessCode) }) {
|
||||
Icon(painterResource(MR.images.ic_check), generalGetString(MR.strings.confirm_verb), tint = MaterialTheme.colors.secondary)
|
||||
TextIconSpaced(false)
|
||||
Text(generalGetString(MR.strings.confirm_verb))
|
||||
}
|
||||
|
||||
SectionView {
|
||||
DisconnectButton(::disconnectDesktop)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CtrlDeviceNameText(session: RemoteCtrlSession, rc: RemoteCtrlInfo?) {
|
||||
val newDesktop = annotatedStringResource(MR.strings.new_desktop)
|
||||
val text = remember(rc) {
|
||||
var t = AnnotatedString(rc?.deviceViewName ?: session.ctrlAppInfo.deviceName)
|
||||
if (rc == null) {
|
||||
t = t + AnnotatedString(" ") + newDesktop
|
||||
}
|
||||
t
|
||||
}
|
||||
Text(text)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CtrlDeviceVersionText(session: RemoteCtrlSession) {
|
||||
val thisDeviceVersion = annotatedStringResource(MR.strings.this_device_version, session.appVersion)
|
||||
val text = remember(session) {
|
||||
val v = AnnotatedString(session.ctrlAppInfo.appVersionRange.maxVersion)
|
||||
var t = AnnotatedString("v$v")
|
||||
if (v.text != session.appVersion) {
|
||||
t = t + AnnotatedString(" ") + thisDeviceVersion
|
||||
}
|
||||
t
|
||||
}
|
||||
Text(text)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ActiveSession(session: RemoteCtrlSession, rc: RemoteCtrlInfo) {
|
||||
AppBarTitle(stringResource(MR.strings.connected_to_desktop))
|
||||
SectionView(stringResource(MR.strings.connected_desktop).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
Text(rc.deviceViewName)
|
||||
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
|
||||
CtrlDeviceVersionText(session)
|
||||
}
|
||||
|
||||
if (session.sessionCode != null) {
|
||||
SectionSpacer()
|
||||
SectionView(stringResource(MR.strings.session_code).uppercase()) {
|
||||
SessionCodeText(session.sessionCode!!)
|
||||
}
|
||||
}
|
||||
|
||||
SectionSpacer()
|
||||
|
||||
SectionView {
|
||||
DisconnectButton(::disconnectDesktop)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SessionCodeText(code: String) {
|
||||
SelectionContainer {
|
||||
Text(
|
||||
code.substring(0, 23),
|
||||
Modifier.padding(start = DEFAULT_PADDING, top = 5.dp, end = DEFAULT_PADDING, bottom = 10.dp),
|
||||
style = TextStyle(fontFamily = FontFamily.Monospace, fontSize = 16.sp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DevicesView(deviceName: String, remoteCtrls: SnapshotStateList<RemoteCtrlInfo>, updateDeviceName: (String) -> Unit) {
|
||||
DeviceNameField(deviceName) { updateDeviceName(it) }
|
||||
if (remoteCtrls.isNotEmpty()) {
|
||||
SectionItemView({ ModalManager.start.showModal { LinkedDesktopsView(remoteCtrls) } }) {
|
||||
Text(generalGetString(MR.strings.linked_desktops))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScanDesktopAddressView(sessionAddress: MutableState<String>) {
|
||||
SectionView(stringResource(MR.strings.scan_qr_code_from_desktop).uppercase()) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(ratio = 1F)
|
||||
.padding(DEFAULT_PADDING)
|
||||
) {
|
||||
QRCodeScanner { text ->
|
||||
sessionAddress.value = text
|
||||
processDesktopQRCode(sessionAddress, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DesktopAddressView(sessionAddress: MutableState<String>) {
|
||||
val clipboard = LocalClipboardManager.current
|
||||
SectionView(stringResource(MR.strings.desktop_address).uppercase()) {
|
||||
if (sessionAddress.value.isEmpty()) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_content_paste),
|
||||
stringResource(MR.strings.paste_desktop_address),
|
||||
disabled = !clipboard.hasText(),
|
||||
click = {
|
||||
sessionAddress.value = clipboard.getText()?.text ?: ""
|
||||
},
|
||||
)
|
||||
} else {
|
||||
Row(Modifier.padding(horizontal = DEFAULT_PADDING).fillMaxWidth()) {
|
||||
val state = remember {
|
||||
mutableStateOf(TextFieldValue(sessionAddress.value))
|
||||
}
|
||||
DefaultBasicTextField(
|
||||
Modifier.fillMaxWidth(),
|
||||
state,
|
||||
color = MaterialTheme.colors.secondary,
|
||||
) {
|
||||
state.value = it
|
||||
}
|
||||
KeyChangeEffect(state.value.text) {
|
||||
if (state.value.text.isNotEmpty()) {
|
||||
sessionAddress.value = state.value.text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_wifi),
|
||||
stringResource(MR.strings.connect_to_desktop),
|
||||
disabled = sessionAddress.value.isEmpty(),
|
||||
click = {
|
||||
connectDesktopAddress(sessionAddress, sessionAddress.value)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LinkedDesktopsView(remoteCtrls: SnapshotStateList<RemoteCtrlInfo>) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.linked_desktops))
|
||||
SectionView(stringResource(MR.strings.desktop_devices).uppercase()) {
|
||||
remoteCtrls.forEach { rc ->
|
||||
val showMenu = rememberSaveable { mutableStateOf(false) }
|
||||
SectionItemViewLongClickable(click = {}, longClick = { showMenu.value = true }) {
|
||||
RemoteCtrl(rc)
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
ItemAction(stringResource(MR.strings.delete_verb), painterResource(MR.images.ic_delete), color = Color.Red) {
|
||||
unlinkDesktop(remoteCtrls, rc)
|
||||
showMenu.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
|
||||
SectionView(stringResource(MR.strings.linked_desktop_options).uppercase()) {
|
||||
PreferenceToggle(stringResource(MR.strings.verify_connections), remember { controller.appPrefs.confirmRemoteSessions.state }.value) {
|
||||
controller.appPrefs.confirmRemoteSessions.set(it)
|
||||
}
|
||||
PreferenceToggle(stringResource(MR.strings.discover_on_network), remember { controller.appPrefs.connectRemoteViaMulticast.state }.value && false) {
|
||||
controller.appPrefs.confirmRemoteSessions.set(it)
|
||||
}
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RemoteCtrl(rc: RemoteCtrlInfo) {
|
||||
Text(rc.deviceViewName)
|
||||
}
|
||||
|
||||
private fun setDeviceName(name: String) {
|
||||
withBGApi {
|
||||
controller.setLocalDeviceName(name)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateRemoteCtrls(remoteCtrls: SnapshotStateList<RemoteCtrlInfo>) {
|
||||
withBGApi {
|
||||
val res = controller.listRemoteCtrls()
|
||||
if (res != null) {
|
||||
remoteCtrls.clear()
|
||||
remoteCtrls.addAll(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun processDesktopQRCode(sessionAddress: MutableState<String>, resp: String) {
|
||||
connectDesktopAddress(sessionAddress, resp)
|
||||
}
|
||||
|
||||
private fun connectDesktopAddress(sessionAddress: MutableState<String>, addr: String) {
|
||||
withBGApi {
|
||||
val res = controller.connectRemoteCtrl(desktopAddress = addr)
|
||||
if (res.first != null) {
|
||||
val (rc_, ctrlAppInfo, v) = res.first!!
|
||||
sessionAddress.value = ""
|
||||
chatModel.remoteCtrlSession.value = RemoteCtrlSession(
|
||||
ctrlAppInfo = ctrlAppInfo,
|
||||
appVersion = v,
|
||||
sessionState = UIRemoteCtrlSessionState.Connecting(remoteCtrl_ = rc_)
|
||||
)
|
||||
} else {
|
||||
val e = res.second ?: return@withBGApi
|
||||
when {
|
||||
e.chatError is ChatError.ChatErrorRemoteCtrl && e.chatError.remoteCtrlError is RemoteCtrlError.BadInvitation -> showBadInvitationErrorAlert()
|
||||
e.chatError is ChatError.ChatErrorChat && e.chatError.errorType is ChatErrorType.CommandError -> showBadInvitationErrorAlert()
|
||||
e.chatError is ChatError.ChatErrorRemoteCtrl && e.chatError.remoteCtrlError is RemoteCtrlError.BadVersion -> showBadVersionAlert(v = e.chatError.remoteCtrlError.appVersion)
|
||||
e.chatError is ChatError.ChatErrorAgent && e.chatError.agentError is AgentErrorType.RCP && e.chatError.agentError.rcpErr is RCErrorType.VERSION -> showBadVersionAlert(v = null)
|
||||
e.chatError is ChatError.ChatErrorAgent && e.chatError.agentError is AgentErrorType.RCP && e.chatError.agentError.rcpErr is RCErrorType.CTRL_AUTH -> showDesktopDisconnectedErrorAlert()
|
||||
else -> {
|
||||
val errMsg = "${e.responseType}: ${e.details}"
|
||||
Log.e(TAG, "bad response: $errMsg")
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), errMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyDesktopSessionCode(remoteCtrls: SnapshotStateList<RemoteCtrlInfo>, sessCode: String) {
|
||||
withBGApi {
|
||||
val rc = controller.verifyRemoteCtrlSession(sessCode)
|
||||
if (rc != null) {
|
||||
chatModel.remoteCtrlSession.value = chatModel.remoteCtrlSession.value?.copy(sessionState = UIRemoteCtrlSessionState.Connected(remoteCtrl = rc, sessionCode = sessCode))
|
||||
}
|
||||
updateRemoteCtrls(remoteCtrls)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DisconnectButton(onClick: () -> Unit) {
|
||||
SectionItemView(onClick) {
|
||||
Icon(painterResource(MR.images.ic_close), generalGetString(MR.strings.disconnect_remote_host), tint = MaterialTheme.colors.secondary)
|
||||
TextIconSpaced(false)
|
||||
Text(generalGetString(MR.strings.disconnect_remote_host))
|
||||
}
|
||||
}
|
||||
|
||||
private fun disconnectDesktop(close: (() -> Unit)? = null) {
|
||||
withBGApi {
|
||||
controller.stopRemoteCtrl()
|
||||
switchToLocalSession()
|
||||
close?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
private fun unlinkDesktop(remoteCtrls: SnapshotStateList<RemoteCtrlInfo>, rc: RemoteCtrlInfo) {
|
||||
withBGApi {
|
||||
controller.deleteRemoteCtrl(rc.remoteCtrlId)
|
||||
remoteCtrls.removeAll { it.remoteCtrlId == rc.remoteCtrlId }
|
||||
}
|
||||
}
|
||||
|
||||
private fun showUnlinkDesktopAlert(remoteCtrls: SnapshotStateList<RemoteCtrlInfo>, rc: RemoteCtrlInfo) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.unlink_desktop_question),
|
||||
confirmText = generalGetString(MR.strings.unlink_desktop),
|
||||
destructive = true,
|
||||
onConfirm = {
|
||||
unlinkDesktop(remoteCtrls, rc)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun showDisconnectDesktopAlert(close: (() -> Unit)?) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.disconnect_desktop_question),
|
||||
text = generalGetString(MR.strings.only_one_device_can_work_at_the_same_time),
|
||||
confirmText = generalGetString(MR.strings.disconnect_remote_host),
|
||||
destructive = true,
|
||||
onConfirm = { disconnectDesktop(close) }
|
||||
)
|
||||
}
|
||||
|
||||
private fun showBadInvitationErrorAlert() {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.bad_desktop_address),
|
||||
)
|
||||
}
|
||||
|
||||
private fun showBadVersionAlert(v: String?) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.desktop_incompatible_version),
|
||||
text = generalGetString(MR.strings.desktop_app_version_is_incompatible).format(v ?: "")
|
||||
)
|
||||
}
|
||||
|
||||
private fun showDesktopDisconnectedErrorAlert() {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.desktop_connection_terminated),
|
||||
)
|
||||
}
|
@ -141,7 +141,7 @@ fun ConnectMobileLayout(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DeviceNameField(
|
||||
fun DeviceNameField(
|
||||
initialValue: String,
|
||||
onChange: (String) -> Unit
|
||||
) {
|
||||
|
@ -29,6 +29,7 @@ import chat.simplex.common.views.database.DatabaseView
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.onboarding.SimpleXInfo
|
||||
import chat.simplex.common.views.onboarding.WhatsNewView
|
||||
import chat.simplex.common.views.remote.ConnectDesktopView
|
||||
import chat.simplex.common.views.remote.ConnectMobileView
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.launch
|
||||
@ -158,6 +159,8 @@ fun SettingsLayout(
|
||||
ChatPreferencesItem(showCustomModal, stopped = stopped)
|
||||
if (appPlatform.isDesktop) {
|
||||
SettingsActionItem(painterResource(MR.images.ic_smartphone), stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles), showModal { ConnectMobileView(it) }, disabled = stopped, extraPadding = true)
|
||||
} else {
|
||||
SettingsActionItem(painterResource(MR.images.ic_desktop), stringResource(MR.strings.settings_section_title_use_from_desktop), showCustomModal{ it, close -> ConnectDesktopView(close) }, disabled = stopped, extraPadding = true)
|
||||
}
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
|
@ -954,6 +954,7 @@
|
||||
<string name="settings_section_title_calls">CALLS</string>
|
||||
<string name="settings_section_title_incognito">Incognito mode</string>
|
||||
<string name="settings_section_title_experimenta">EXPERIMENTAL</string>
|
||||
<string name="settings_section_title_use_from_desktop">Use from desktop</string>
|
||||
|
||||
<!-- DatabaseView.kt -->
|
||||
<string name="your_chat_database">Your chat database</string>
|
||||
@ -1636,6 +1637,7 @@
|
||||
<string name="verify_connection">Verify connection</string>
|
||||
<string name="verify_code_on_mobile">Verify code on mobile</string>
|
||||
<string name="this_device_name">This device name</string>
|
||||
<string name="this_device_version"><![CDATA[<i>(this device v%s)</i>]]></string>
|
||||
<string name="connected_mobile">Connected mobile</string>
|
||||
<string name="connected_to_mobile">Connected to mobile</string>
|
||||
<string name="enter_this_device_name">Enter this device name…</string>
|
||||
@ -1644,8 +1646,32 @@
|
||||
<string name="this_device">This device</string>
|
||||
<string name="devices">Devices</string>
|
||||
<string name="new_mobile_device">New mobile device</string>
|
||||
<string name="unlink_desktop_question">Unlink desktop?</string>
|
||||
<string name="unlink_desktop">Unlink</string>
|
||||
<string name="disconnect_remote_host">Disconnect</string>
|
||||
<string name="disconnect_desktop_question">Disconnect desktop?</string>
|
||||
<string name="only_one_device_can_work_at_the_same_time">Only one device can work at the same time</string>
|
||||
<string name="open_on_mobile_and_scan_qr_code"><![CDATA[Open <i>Use from desktop</i> in mobile app and scan QR code]]></string>
|
||||
<string name="bad_desktop_address">Bad desktop address</string>
|
||||
<string name="desktop_incompatible_version">Incompatible version</string>
|
||||
<string name="desktop_app_version_is_incompatible">Desktop app version %s is not compatible with this app.</string>
|
||||
<string name="desktop_connection_terminated">Connection terminated</string>
|
||||
<string name="session_code">Session code</string>
|
||||
<string name="connecting_to_desktop">Connecting to desktop</string>
|
||||
<string name="connect_to_desktop">Connect to desktop</string>
|
||||
<string name="connected_to_desktop">Connected to desktop</string>
|
||||
<string name="connected_desktop">Connected desktop</string>
|
||||
<string name="verify_code_with_desktop">Verify code with desktop</string>
|
||||
<string name="new_desktop"><![CDATA[<i>(new)</i>]]></string>
|
||||
<string name="linked_desktops">Linked desktops</string>
|
||||
<string name="desktop_devices">Desktop devices</string>
|
||||
<string name="linked_desktop_options">Linked desktop options</string>
|
||||
<string name="scan_qr_code_from_desktop">Scan QR code from desktop</string>
|
||||
<string name="desktop_address">Desktop address</string>
|
||||
<string name="verify_connections">Verify connections</string>
|
||||
<string name="discover_on_network">Discover on network</string>
|
||||
<string name="paste_desktop_address">Paste desktop address</string>
|
||||
<string name="desktop_device">Desktop</string>
|
||||
|
||||
<!-- Under development -->
|
||||
<string name="in_developing_title">Coming soon!</string>
|
||||
|
@ -2,11 +2,15 @@ package chat.simplex.common.platform
|
||||
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.views.call.RcvCallInvitation
|
||||
import chat.simplex.common.views.helpers.generalGetString
|
||||
import chat.simplex.common.views.helpers.withBGApi
|
||||
import java.util.*
|
||||
import chat.simplex.res.MR
|
||||
|
||||
actual val appPlatform = AppPlatform.DESKTOP
|
||||
|
||||
actual val deviceName = generalGetString(MR.strings.desktop_device)
|
||||
|
||||
@Suppress("ConstantLocale")
|
||||
val defaultLocale: Locale = Locale.getDefault()
|
||||
|
||||
|
@ -1,8 +0,0 @@
|
||||
package chat.simplex.common.views.chat
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable
|
||||
actual fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) {
|
||||
ScanCodeLayout(verifyCode, close)
|
||||
}
|
Loading…
Reference in New Issue
Block a user