mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Merge branch 'master' into master-ghc8107
This commit is contained in:
+39
-1
@@ -8,10 +8,14 @@ import android.os.Build
|
||||
import android.view.*
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import chat.simplex.common.views.helpers.KeyboardState
|
||||
import chat.simplex.common.AppScreen
|
||||
import chat.simplex.common.ui.theme.SimpleXTheme
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import androidx.compose.ui.platform.LocalContext as LocalContext1
|
||||
import chat.simplex.res.MR
|
||||
|
||||
actual fun showToast(text: String, timeout: Long) = Toast.makeText(androidAppContext, text, Toast.LENGTH_SHORT).show()
|
||||
|
||||
@@ -71,3 +75,37 @@ actual fun hideKeyboard(view: Any?) {
|
||||
}
|
||||
|
||||
actual fun androidIsFinishingMainActivity(): Boolean = (mainActivity.get()?.isFinishing == true)
|
||||
|
||||
actual class GlobalExceptionsHandler: Thread.UncaughtExceptionHandler {
|
||||
actual override fun uncaughtException(thread: Thread, e: Throwable) {
|
||||
Log.e(TAG, "App crashed, thread name: " + thread.name + ", exception: " + e.stackTraceToString())
|
||||
if (ModalManager.start.hasModalsOpen()) {
|
||||
ModalManager.start.closeModal()
|
||||
} else if (chatModel.chatId.value != null) {
|
||||
// Since no modals are open, the problem is probably in ChatView
|
||||
chatModel.chatId.value = null
|
||||
chatModel.chatItems.clear()
|
||||
} else {
|
||||
// ChatList, nothing to do. Maybe to show other view except ChatList
|
||||
}
|
||||
chatModel.activeCall.value?.let {
|
||||
withBGApi {
|
||||
chatModel.callManager.endCall(it)
|
||||
}
|
||||
}
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.app_was_crashed),
|
||||
text = e.stackTraceToString()
|
||||
)
|
||||
//mainActivity.get()?.recreate()
|
||||
mainActivity.get()?.apply {
|
||||
window
|
||||
?.decorView
|
||||
?.findViewById<ViewGroup>(android.R.id.content)
|
||||
?.removeViewAt(0)
|
||||
setContent {
|
||||
AppScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,9 +42,11 @@ data class SettingsViewState(
|
||||
|
||||
@Composable
|
||||
fun AppScreen() {
|
||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
||||
Surface(color = MaterialTheme.colors.background) {
|
||||
MainScreen()
|
||||
SimpleXTheme {
|
||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
||||
Surface(color = MaterialTheme.colors.background) {
|
||||
MainScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -2,7 +2,6 @@ package chat.simplex.common.model
|
||||
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
@@ -68,6 +67,9 @@ object ChatModel {
|
||||
// set when app opened from external intent
|
||||
val clearOverlays = mutableStateOf<Boolean>(false)
|
||||
|
||||
// Only needed during onboarding when user skipped password setup (left as random password)
|
||||
val desktopOnboardingRandomPassword = mutableStateOf(false)
|
||||
|
||||
// set when app is opened via contact or invitation URI
|
||||
val appOpenUrl = mutableStateOf<URI?>(null)
|
||||
|
||||
|
||||
@@ -16,3 +16,11 @@ expect fun getKeyboardState(): State<KeyboardState>
|
||||
expect fun hideKeyboard(view: Any?)
|
||||
|
||||
expect fun androidIsFinishingMainActivity(): Boolean
|
||||
|
||||
fun registerGlobalErrorHandler() {
|
||||
Thread.setDefaultUncaughtExceptionHandler(GlobalExceptionsHandler())
|
||||
}
|
||||
|
||||
expect class GlobalExceptionsHandler(): Thread.UncaughtExceptionHandler {
|
||||
override fun uncaughtException(thread: Thread, e: Throwable)
|
||||
}
|
||||
|
||||
+2
-2
@@ -207,12 +207,12 @@ fun createProfileInProfiles(chatModel: ChatModel, displayName: String, close: ()
|
||||
|
||||
fun createProfileOnboarding(chatModel: ChatModel, displayName: String, close: () -> Unit) {
|
||||
withApi {
|
||||
chatModel.controller.apiCreateActiveUser(
|
||||
chatModel.currentUser.value = chatModel.controller.apiCreateActiveUser(
|
||||
null, Profile(displayName.trim(), "", null)
|
||||
) ?: return@withApi
|
||||
val onboardingStage = chatModel.controller.appPrefs.onboardingStage
|
||||
if (chatModel.users.isEmpty()) {
|
||||
onboardingStage.set(if (appPlatform.isDesktop && chatModel.controller.appPrefs.initialRandomDBPassphrase.get()) {
|
||||
onboardingStage.set(if (appPlatform.isDesktop && chatModel.controller.appPrefs.initialRandomDBPassphrase.get() && !chatModel.desktopOnboardingRandomPassword.value) {
|
||||
OnboardingStage.Step2_5_SetupDatabasePassphrase
|
||||
} else {
|
||||
OnboardingStage.Step3_CreateSimpleXAddress
|
||||
|
||||
+59
-38
@@ -1,8 +1,11 @@
|
||||
package chat.simplex.common.views.helpers
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CornerSize
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -233,53 +236,71 @@ private fun alertTitle(title: String): (@Composable () -> Unit)? {
|
||||
|
||||
@Composable
|
||||
private fun AlertContent(text: String?, hostDevice: Pair<Long?, String>?, extraPadding: Boolean = false, content: @Composable (() -> Unit)) {
|
||||
Column(
|
||||
Modifier
|
||||
.padding(bottom = if (appPlatform.isDesktop) DEFAULT_PADDING else DEFAULT_PADDING_HALF)
|
||||
) {
|
||||
if (appPlatform.isDesktop) {
|
||||
HostDeviceTitle(hostDevice, extraPadding = extraPadding)
|
||||
} else {
|
||||
Spacer(Modifier.size(DEFAULT_PADDING_HALF))
|
||||
}
|
||||
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
|
||||
if (text != null) {
|
||||
Text(
|
||||
escapedHtmlToAnnotatedString(text, LocalDensity.current),
|
||||
Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING * 1.5f),
|
||||
fontSize = 16.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
BoxWithConstraints {
|
||||
Column(
|
||||
Modifier
|
||||
.padding(bottom = if (appPlatform.isDesktop) DEFAULT_PADDING else DEFAULT_PADDING_HALF)
|
||||
) {
|
||||
if (appPlatform.isDesktop) {
|
||||
HostDeviceTitle(hostDevice, extraPadding = extraPadding)
|
||||
} else {
|
||||
Spacer(Modifier.size(DEFAULT_PADDING_HALF))
|
||||
}
|
||||
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
|
||||
if (text != null) {
|
||||
Column(Modifier.heightIn(max = this@BoxWithConstraints.maxHeight * 0.7f)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
SelectionContainer {
|
||||
Text(
|
||||
escapedHtmlToAnnotatedString(text, LocalDensity.current),
|
||||
Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING * 1.5f),
|
||||
fontSize = 16.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
content()
|
||||
}
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AlertContent(text: AnnotatedString?, hostDevice: Pair<Long?, String>?, extraPadding: Boolean = false, content: @Composable (() -> Unit)) {
|
||||
Column(
|
||||
Modifier
|
||||
.padding(bottom = if (appPlatform.isDesktop) DEFAULT_PADDING else DEFAULT_PADDING_HALF)
|
||||
) {
|
||||
if (appPlatform.isDesktop) {
|
||||
HostDeviceTitle(hostDevice, extraPadding = extraPadding)
|
||||
} else {
|
||||
Spacer(Modifier.size(DEFAULT_PADDING_HALF))
|
||||
}
|
||||
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
|
||||
if (text != null) {
|
||||
Text(
|
||||
text,
|
||||
Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING * 1.5f),
|
||||
fontSize = 16.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
BoxWithConstraints {
|
||||
Column(
|
||||
Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(bottom = if (appPlatform.isDesktop) DEFAULT_PADDING else DEFAULT_PADDING_HALF)
|
||||
) {
|
||||
if (appPlatform.isDesktop) {
|
||||
HostDeviceTitle(hostDevice, extraPadding = extraPadding)
|
||||
} else {
|
||||
Spacer(Modifier.size(DEFAULT_PADDING_HALF))
|
||||
}
|
||||
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
|
||||
if (text != null) {
|
||||
Column(
|
||||
Modifier.heightIn(max = this@BoxWithConstraints.maxHeight * 0.7f)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
SelectionContainer {
|
||||
Text(
|
||||
text,
|
||||
Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING * 1.5f),
|
||||
fontSize = 16.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
content()
|
||||
}
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-4
@@ -19,8 +19,8 @@ import dev.icerock.moko.resources.compose.painterResource
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.*
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.views.database.PassphraseStrength
|
||||
import chat.simplex.common.views.database.validKey
|
||||
import chat.simplex.res.MR
|
||||
@@ -123,6 +123,7 @@ fun DefaultConfigurableTextField(
|
||||
isValid: (String) -> Boolean,
|
||||
keyboardActions: KeyboardActions = KeyboardActions(),
|
||||
keyboardType: KeyboardType = KeyboardType.Text,
|
||||
fontSize: TextUnit = 16.sp,
|
||||
dependsOn: State<Any?>? = null,
|
||||
) {
|
||||
var valid by remember { mutableStateOf(isValid(state.value.text)) }
|
||||
@@ -175,14 +176,14 @@ fun DefaultConfigurableTextField(
|
||||
textStyle = TextStyle.Default.copy(
|
||||
color = color,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp
|
||||
fontSize = fontSize
|
||||
),
|
||||
interactionSource = interactionSource,
|
||||
decorationBox = @Composable { innerTextField ->
|
||||
TextFieldDefaults.TextFieldDecorationBox(
|
||||
value = state.value.text,
|
||||
innerTextField = innerTextField,
|
||||
placeholder = { Text(placeholder, color = MaterialTheme.colors.secondary) },
|
||||
placeholder = { Text(placeholder, color = MaterialTheme.colors.secondary, fontSize = fontSize, maxLines = 1, overflow = TextOverflow.Ellipsis) },
|
||||
singleLine = true,
|
||||
enabled = enabled,
|
||||
isError = !valid,
|
||||
|
||||
+5
-2
@@ -20,6 +20,7 @@ fun <T> ExposedDropDownSetting(
|
||||
values: List<Pair<T, String>>,
|
||||
selection: State<T>,
|
||||
textColor: Color = MaterialTheme.colors.secondary,
|
||||
fontSize: TextUnit = 16.sp,
|
||||
label: String? = null,
|
||||
enabled: State<Boolean> = mutableStateOf(true),
|
||||
minWidth: Dp = 200.dp,
|
||||
@@ -43,7 +44,8 @@ fun <T> ExposedDropDownSetting(
|
||||
Modifier.widthIn(max = maxWidth),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = textColor
|
||||
color = textColor,
|
||||
fontSize = fontSize,
|
||||
)
|
||||
Spacer(Modifier.size(12.dp))
|
||||
Icon(
|
||||
@@ -69,6 +71,7 @@ fun <T> ExposedDropDownSetting(
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = if (isInDarkTheme()) MenuTextColorDark else Color.Black,
|
||||
fontSize = fontSize,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -91,6 +94,6 @@ fun <T> ExposedDropDownSettingRow(
|
||||
onSelected: (T) -> Unit
|
||||
) {
|
||||
SettingsActionItemWithContent(icon, title, iconColor = iconTint, disabled = !enabled.value) {
|
||||
ExposedDropDownSetting(values, selection ,textColor, label, enabled, minWidth, maxWidth, onSelected)
|
||||
ExposedDropDownSetting(values, selection ,textColor, label = label, enabled = enabled, minWidth = minWidth, maxWidth = maxWidth, onSelected = onSelected)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -73,7 +73,8 @@ private fun LinkAMobileLayout(
|
||||
}
|
||||
Box(Modifier.weight(0.7f)) {
|
||||
AddingMobileDevice(false, staleQrCode, connecting) {
|
||||
if (chatModel.remoteHosts.isEmpty()) {
|
||||
// currentRemoteHost will be set instantly but remoteHosts may be delayed
|
||||
if (chatModel.remoteHosts.isEmpty() && chatModel.currentRemoteHost.value == null) {
|
||||
chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
|
||||
} else {
|
||||
chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.OnboardingComplete)
|
||||
|
||||
+4
-6
@@ -1,10 +1,7 @@
|
||||
package chat.simplex.common.views.onboarding
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionItemView
|
||||
import SectionItemViewSpaceBetween
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
@@ -15,14 +12,12 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.key.*
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.*
|
||||
@@ -177,7 +172,10 @@ private fun SetupDatabasePassphraseLayout(
|
||||
}
|
||||
|
||||
Spacer(Modifier.weight(1f))
|
||||
SkipButton(progressIndicator.value, nextStep)
|
||||
SkipButton(progressIndicator.value) {
|
||||
chatModel.desktopOnboardingRandomPassword.value = true
|
||||
nextStep()
|
||||
}
|
||||
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
|
||||
+69
-59
@@ -174,8 +174,6 @@ private fun ConnectMobileViewLayout(
|
||||
sessionCode: String?,
|
||||
port: String?,
|
||||
staleQrCode: Boolean = false,
|
||||
editEnabled: Boolean = false,
|
||||
editClicked: () -> Unit = {},
|
||||
refreshQrCode: () -> Unit = {},
|
||||
UnderQrLayout: @Composable () -> Unit = {},
|
||||
) {
|
||||
@@ -201,16 +199,7 @@ private fun ConnectMobileViewLayout(
|
||||
}
|
||||
}
|
||||
SectionTextFooter(annotatedStringResource(MR.strings.open_on_mobile_and_scan_qr_code), 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))
|
||||
}
|
||||
}
|
||||
SectionTextFooter(annotatedStringResource(MR.strings.waiting_for_mobile_to_connect), textAlign = TextAlign.Center)
|
||||
|
||||
UnderQrLayout()
|
||||
|
||||
@@ -249,7 +238,9 @@ private fun ConnectMobileViewLayout(
|
||||
}
|
||||
}
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
if (invitation != null) {
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,10 +266,9 @@ private fun showAddingMobileDevice(connecting: MutableState<Boolean>) {
|
||||
|
||||
@Composable
|
||||
fun AddingMobileDevice(showTitle: Boolean, staleQrCode: MutableState<Boolean>, connecting: MutableState<Boolean>, close: () -> Unit) {
|
||||
val cachedR = remember { mutableStateOf<CR.RemoteHostStarted?>(null) }
|
||||
var cachedR by 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 = null,
|
||||
@@ -287,9 +277,9 @@ fun AddingMobileDevice(showTitle: Boolean, staleQrCode: MutableState<Boolean>, c
|
||||
port = if (customPort.value != cachedR.port) customPort.value else cachedR.rh?.bindPort_
|
||||
)
|
||||
if (r != null) {
|
||||
cachedR.value = r
|
||||
cachedR = r
|
||||
connecting.value = true
|
||||
customAddress.value = cachedR.address
|
||||
customAddress.value = cachedR.addresses.firstOrNull()
|
||||
customPort.value = cachedR.port
|
||||
chatModel.remoteHostPairing.value = null to RemoteHostSessionState.Starting
|
||||
}
|
||||
@@ -307,23 +297,20 @@ 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 = cachedR.invitation,
|
||||
invitation = cachedR?.invitation,
|
||||
deviceName = remoteDeviceName,
|
||||
sessionCode = cachedSessionCode,
|
||||
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 },
|
||||
port = cachedR?.ctrlPort,
|
||||
staleQrCode = staleQrCode.value || (cachedR.address != customAddress.value && customAddress.value != null) || cachedR.port != customPort.value,
|
||||
refreshQrCode = {
|
||||
withBGApi {
|
||||
if (chatController.stopRemoteHost(null)) {
|
||||
startRemoteHost()
|
||||
staleQrCode.value = false
|
||||
editing = false
|
||||
}
|
||||
}
|
||||
},
|
||||
UnderQrLayout = { UnderQrLayout(editing, cachedR, customAddress, customPort) }
|
||||
UnderQrLayout = { UnderQrLayout(cachedR, customAddress, customPort) }
|
||||
)
|
||||
val oldRemoteHostId by remember { mutableStateOf(chatModel.currentRemoteHost.value?.remoteHostId) }
|
||||
LaunchedEffect(remember { chatModel.currentRemoteHost }.value) {
|
||||
@@ -353,10 +340,9 @@ 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) }
|
||||
var cachedR by 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,
|
||||
@@ -365,9 +351,9 @@ private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState
|
||||
port = if (customPort.value != cachedR.port) customPort.value else cachedR.rh?.bindPort_ ?: rh.bindPort_
|
||||
)
|
||||
if (r != null) {
|
||||
cachedR.value = r
|
||||
cachedR = r
|
||||
connecting.value = true
|
||||
customAddress.value = cachedR.address
|
||||
customAddress.value = cachedR.addresses.firstOrNull()
|
||||
customPort.value = cachedR.port
|
||||
chatModel.remoteHostPairing.value = null to RemoteHostSessionState.Starting
|
||||
}
|
||||
@@ -384,22 +370,19 @@ 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 = cachedR.invitation,
|
||||
invitation = cachedR?.invitation,
|
||||
deviceName = pairing.value?.first?.hostDeviceName ?: rh.hostDeviceName,
|
||||
sessionCode = cachedSessionCode,
|
||||
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 },
|
||||
port = cachedR?.ctrlPort,
|
||||
staleQrCode = (cachedR.address != customAddress.value && customAddress.value != null) || cachedR.port != customPort.value,
|
||||
refreshQrCode = {
|
||||
withBGApi {
|
||||
if (chatController.stopRemoteHost(rh.remoteHostId)) {
|
||||
startRemoteHost()
|
||||
editing = false
|
||||
}
|
||||
}
|
||||
},
|
||||
UnderQrLayout = { UnderQrLayout(editing, cachedR, customAddress, customPort) }
|
||||
UnderQrLayout = { UnderQrLayout(cachedR, customAddress, customPort) }
|
||||
)
|
||||
LaunchedEffect(remember { chatModel.currentRemoteHost }.value) {
|
||||
if (cachedR.remoteHostId != null && chatModel.currentRemoteHost.value?.remoteHostId == cachedR.remoteHostId) {
|
||||
@@ -453,48 +436,75 @@ 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) {
|
||||
private fun UnderQrLayout(cachedR: CR.RemoteHostStarted?, customAddress: MutableState<RemoteCtrlAddress?>, customPort: MutableState<Int?>) {
|
||||
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) {
|
||||
if (cachedR.addresses.size > 1) {
|
||||
ExposedDropDownSetting(
|
||||
cachedR.addresses.map { it to it.address + " (${it.`interface`})" },
|
||||
customAddress,
|
||||
textColor = MaterialTheme.colors.onBackground,
|
||||
fontSize = 14.sp,
|
||||
minWidth = 250.dp,
|
||||
maxWidth = with(LocalDensity.current) { 250.sp.toDp() },
|
||||
enabled = remember { mutableStateOf(cachedR.addresses.size > 1) },
|
||||
onSelected = {
|
||||
customAddress.value = it
|
||||
}
|
||||
)
|
||||
val portUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||
mutableStateOf(TextFieldValue((customPort.value ?: cachedR.port!!).toString()))
|
||||
}
|
||||
Spacer(Modifier.width(DEFAULT_PADDING))
|
||||
} else {
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Text(customAddress.value?.address + " (${customAddress.value?.`interface`})", fontSize = 14.sp, color = MaterialTheme.colors.onBackground)
|
||||
}
|
||||
val portUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||
mutableStateOf(TextFieldValue((customPort.value ?: cachedR.port!!).toString()))
|
||||
}
|
||||
Spacer(Modifier.width(DEFAULT_PADDING))
|
||||
Box {
|
||||
DefaultConfigurableTextField(
|
||||
portUnsaved,
|
||||
stringResource(MR.strings.port_verb),
|
||||
modifier = Modifier.widthIn(max = 100.dp),
|
||||
isValid = { validPort(it) && it.toInt() > 1023 },
|
||||
stringResource(MR.strings.random_port),
|
||||
modifier = Modifier.widthIn(max = 132.dp),
|
||||
isValid = { (validPort(it) && it.toInt() > 1023) || it.isBlank() },
|
||||
keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done) }),
|
||||
keyboardType = KeyboardType.Number,
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { portUnsaved.value.text }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
if (validPort(it) && it.toInt() > 1023) {
|
||||
customPort.value = it.toInt()
|
||||
}
|
||||
if (validPort(portUnsaved.value.text) && portUnsaved.value.text.toInt() > 1023) {
|
||||
Icon(painterResource(MR.images.ic_edit), stringResource(MR.strings.edit_verb), Modifier.padding(end = 56.dp).size(16.dp).align(Alignment.CenterEnd), tint = MaterialTheme.colors.secondary)
|
||||
IconButton(::showOpenPortAlert, Modifier.align(Alignment.TopEnd).padding(top = 2.dp)) {
|
||||
Icon(painterResource(MR.images.ic_info), null, tint = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { portUnsaved.value.text }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
if (validPort(it) && it.toInt() > 1023) {
|
||||
customPort.value = it.toInt()
|
||||
} else {
|
||||
customPort.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyChangeEffect(customPort.value) {
|
||||
if (customPort.value != null) {
|
||||
portUnsaved.value = portUnsaved.value.copy(text = customPort.value.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
private fun showOpenPortAlert() {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.open_port_in_firewall_title),
|
||||
text = generalGetString(MR.strings.open_port_in_firewall_desc),
|
||||
)
|
||||
}
|
||||
|
||||
private val CR.RemoteHostStarted?.rh: RemoteHostInfo? get() = this?.remoteHost_
|
||||
private val CR.RemoteHostStarted?.remoteHostId: Long? get() = this?.remoteHost_?.remoteHostId
|
||||
private val CR.RemoteHostStarted?.address: RemoteCtrlAddress? get() = this?.localAddrs?.firstOrNull()
|
||||
private val CR.RemoteHostStarted?.addresses: List<RemoteCtrlAddress> get() =
|
||||
(if (controller.appPrefs.developerTools.get() || this?.localAddrs?.indexOfFirst { it.address == "127.0.0.1" } == 0) this?.localAddrs else this?.localAddrs?.filterNot { it.address == "127.0.0.1" }) ?: emptyList()
|
||||
private val CR.RemoteHostStarted?.port: Int? get() = this?.ctrlPort?.toIntOrNull()
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<string name="opening_database">Opening database…</string>
|
||||
<string name="non_content_uri_alert_title">Invalid file path</string>
|
||||
<string name="non_content_uri_alert_text">You shared an invalid file path. Report the issue to the app developers.</string>
|
||||
<string name="app_was_crashed">View crashed</string>
|
||||
|
||||
<!-- Server info - ChatModel.kt -->
|
||||
<string name="server_connected">connected</string>
|
||||
@@ -1665,7 +1666,7 @@
|
||||
<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="waiting_for_mobile_to_connect_on_port"><![CDATA[Waiting for mobile to connect on port <i>%s</i>]]></string>
|
||||
<string name="waiting_for_mobile_to_connect">Waiting for mobile to connect:</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>
|
||||
@@ -1693,6 +1694,9 @@
|
||||
<string name="not_compatible">Not compatible!</string>
|
||||
<string name="refresh_qr_code">Refresh</string>
|
||||
<string name="no_connected_mobile">No connected mobile</string>
|
||||
<string name="random_port">Random</string>
|
||||
<string name="open_port_in_firewall_title">Open port in firewall</string>
|
||||
<string name="open_port_in_firewall_desc">To allow a mobile app to connect to the desktop, open this port in your firewall, if you have it enabled</string>
|
||||
|
||||
<!-- Under development -->
|
||||
<string name="in_developing_title">Coming soon!</string>
|
||||
|
||||
@@ -1581,7 +1581,7 @@
|
||||
<string name="v5_4_better_groups_descr">Schnellerer Gruppenbeitritt und zuverlässigere Nachrichtenzustellung.</string>
|
||||
<string name="linked_mobiles">Verknüpfte Mobiltelefone</string>
|
||||
<string name="this_device_name">Dieser Gerätename</string>
|
||||
<string name="waiting_for_mobile_to_connect_on_port"><![CDATA[Auf die Mobiltelefonverbindung über Port <i>%s</i> warten]]></string>
|
||||
<string name="waiting_for_mobile_to_connect">Auf die Mobiltelefonverbindung warten:</string>
|
||||
<string name="loading_remote_file_title">Laden der Datei</string>
|
||||
<string name="link_a_mobile">Zu einem Mobiltelefon verbinden</string>
|
||||
<string name="settings_section_title_use_from_desktop">Vom Desktop aus nutzen</string>
|
||||
|
||||
@@ -1486,7 +1486,7 @@
|
||||
<string name="desktop_device">Bureau</string>
|
||||
<string name="connected_to_desktop">Connecté au bureau</string>
|
||||
<string name="this_device_name">Ce nom d\'appareil</string>
|
||||
<string name="waiting_for_mobile_to_connect_on_port"><![CDATA[En attente d\'une connexion mobile sur le port <i>%s</i>]]></string>
|
||||
<string name="waiting_for_mobile_to_connect">En attente d\'une connexion mobile:</string>
|
||||
<string name="loading_remote_file_title">Chargement du fichier</string>
|
||||
<string name="connecting_to_desktop">Connexion au bureau</string>
|
||||
<string name="desktop_devices">Appareils de bureau</string>
|
||||
|
||||
@@ -1517,6 +1517,6 @@
|
||||
<string name="v5_4_more_things_descr">- avvisa facoltativamente i contatti eliminati.
|
||||
\n- nomi del profilo con spazi.
|
||||
\n- e molto altro!</string>
|
||||
<string name="waiting_for_mobile_to_connect_on_port"><![CDATA[In attesa che il cellulare si connetta alla porta <i>%s</i>]]></string>
|
||||
<string name="waiting_for_mobile_to_connect">In attesa che il cellulare si connette:</string>
|
||||
<string name="group_member_role_author">autore</string>
|
||||
</resources>
|
||||
@@ -1516,7 +1516,7 @@
|
||||
\n- en meer!</string>
|
||||
<string name="remote_host_was_disconnected_toast"><![CDATA[Mobiele verbinding <b>%s</b> is verbroken]]></string>
|
||||
<string name="group_member_role_author">auteur</string>
|
||||
<string name="waiting_for_mobile_to_connect_on_port"><![CDATA[Wachten tot mobiel verbinding maakt op poort <i>%s</i>]]></string>
|
||||
<string name="waiting_for_mobile_to_connect">Wachten tot mobiel verbinding maakt:</string>
|
||||
<string name="multicast_connect_automatically">Automatisch verbinden</string>
|
||||
<string name="waiting_for_desktop">Wachten op desktop…</string>
|
||||
<string name="found_desktop">Desktop gevonden</string>
|
||||
|
||||
@@ -1496,7 +1496,7 @@
|
||||
<string name="v5_4_better_groups_descr">Szybsze dołączenie i bardziej niezawodne wiadomości.</string>
|
||||
<string name="linked_mobiles">Połączone telefony</string>
|
||||
<string name="this_device_name">Nazwa tego urządzenia</string>
|
||||
<string name="waiting_for_mobile_to_connect_on_port"><![CDATA[Oczekiwanie na połączenie telefonu na port <i>%s</i>]]></string>
|
||||
<string name="waiting_for_mobile_to_connect">Oczekiwanie na połączenie telefonu:</string>
|
||||
<string name="loading_remote_file_title">Ładowanie pliku</string>
|
||||
<string name="found_desktop">Znaleziono komputer</string>
|
||||
<string name="desktop_devices">Urządzenia komputerowe</string>
|
||||
|
||||
@@ -1601,7 +1601,7 @@
|
||||
<string name="verify_connection">Проверить соединение</string>
|
||||
<string name="multicast_connect_automatically">Соединяться автоматически</string>
|
||||
<string name="waiting_for_desktop">Ожидается подключение…</string>
|
||||
<string name="waiting_for_mobile_to_connect_on_port"><![CDATA[Ожидается подключение мобильного через порт <i>%s</i>]]></string>
|
||||
<string name="waiting_for_mobile_to_connect">Ожидается подключение мобильного:</string>
|
||||
<string name="found_desktop">Компьютер найден</string>
|
||||
<string name="not_compatible">Несовместимая версия!</string>
|
||||
<string name="group_member_role_author">автор</string>
|
||||
|
||||
@@ -1517,7 +1517,7 @@
|
||||
<string name="v5_4_more_things_descr">- 可选择通知已删除的联系人。
|
||||
\n- 带空格的个人资料名称。
|
||||
\n- 以及更多!</string>
|
||||
<string name="waiting_for_mobile_to_connect_on_port"><![CDATA[正等待移动设备在端口 <i>%s</i> 进行连接]]></string>
|
||||
<string name="waiting_for_mobile_to_connect">正等待移动设备 进行连接:</string>
|
||||
<string name="group_member_role_author">作者</string>
|
||||
<string name="multicast_connect_automatically">自动连接</string>
|
||||
<string name="waiting_for_desktop">等待桌面中…</string>
|
||||
|
||||
+97
-52
@@ -9,30 +9,73 @@ import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.awt.ComposeWindow
|
||||
import androidx.compose.ui.input.key.*
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.*
|
||||
import chat.simplex.common.model.ChatController
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.DEFAULT_START_MODAL_WIDTH
|
||||
import chat.simplex.common.ui.theme.SimpleXTheme
|
||||
import chat.simplex.common.views.TerminalView
|
||||
import chat.simplex.common.views.helpers.FileDialogChooser
|
||||
import chat.simplex.common.views.helpers.escapedHtmlToAnnotatedString
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.*
|
||||
import java.awt.event.WindowEvent
|
||||
import java.awt.event.WindowFocusListener
|
||||
import java.io.File
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
val simplexWindowState = SimplexWindowState()
|
||||
|
||||
fun showApp() = application {
|
||||
fun showApp() {
|
||||
val closedByError = mutableStateOf(true)
|
||||
while (closedByError.value) {
|
||||
application(exitProcessOnExit = false) {
|
||||
CompositionLocalProvider(
|
||||
LocalWindowExceptionHandlerFactory provides WindowExceptionHandlerFactory { window ->
|
||||
WindowExceptionHandler { e ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.app_was_crashed),
|
||||
text = e.stackTraceToString()
|
||||
)
|
||||
Log.e(TAG, "App crashed, thread name: " + Thread.currentThread().name + ", exception: " + e.stackTraceToString())
|
||||
window.dispatchEvent(WindowEvent(window, WindowEvent.WINDOW_CLOSING))
|
||||
closedByError.value = true
|
||||
// If the left side of screen has open modal, it's probably caused the crash
|
||||
if (ModalManager.start.hasModalsOpen()) {
|
||||
ModalManager.start.closeModal()
|
||||
} else if (ModalManager.center.hasModalsOpen() || ModalManager.end.hasModalsOpen()) {
|
||||
ModalManager.center.closeModal()
|
||||
ModalManager.end.closeModal()
|
||||
// Better to not close fullscreen since it can contain passcode
|
||||
} else {
|
||||
// The last possible cause that can be closed
|
||||
chatModel.chatId.value = null
|
||||
chatModel.chatItems.clear()
|
||||
}
|
||||
chatModel.activeCall.value?.let {
|
||||
withBGApi {
|
||||
chatModel.callManager.endCall(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
AppWindow(closedByError)
|
||||
}
|
||||
}
|
||||
}
|
||||
exitProcess(0)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ApplicationScope.AppWindow(closedByError: MutableState<Boolean>) {
|
||||
// Creates file if not exists; comes with proper defaults
|
||||
val state = getStoredWindowState()
|
||||
|
||||
val windowState: WindowState = rememberWindowState(
|
||||
placement = WindowPlacement.Floating,
|
||||
width = state.width.dp,
|
||||
@@ -46,59 +89,62 @@ fun showApp() = application {
|
||||
windowState.size.width.value,
|
||||
windowState.size.height.value
|
||||
) {
|
||||
storeWindowState(WindowPositionSize(
|
||||
x = windowState.position.x.value.toInt(),
|
||||
y = windowState.position.y.value.toInt(),
|
||||
width = windowState.size.width.value.toInt(),
|
||||
height = windowState.size.height.value.toInt()
|
||||
))
|
||||
storeWindowState(
|
||||
WindowPositionSize(
|
||||
x = windowState.position.x.value.toInt(),
|
||||
y = windowState.position.y.value.toInt(),
|
||||
width = windowState.size.width.value.toInt(),
|
||||
height = windowState.size.height.value.toInt()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
simplexWindowState.windowState = windowState
|
||||
// Reload all strings in all @Composable's after language change at runtime
|
||||
if (remember { ChatController.appPrefs.appLanguage.state }.value != "") {
|
||||
Window(state = windowState, onCloseRequest = ::exitApplication, onKeyEvent = {
|
||||
Window(state = windowState, onCloseRequest = { closedByError.value = false; exitApplication() }, onKeyEvent = {
|
||||
if (it.key == Key.Escape && it.type == KeyEventType.KeyUp) {
|
||||
simplexWindowState.backstack.lastOrNull()?.invoke() != null
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}, title = "SimpleX") {
|
||||
SimpleXTheme {
|
||||
AppScreen()
|
||||
if (simplexWindowState.openDialog.isAwaiting) {
|
||||
FileDialogChooser(
|
||||
title = "SimpleX",
|
||||
isLoad = true,
|
||||
params = simplexWindowState.openDialog.params,
|
||||
onResult = {
|
||||
simplexWindowState.openDialog.onResult(it.firstOrNull())
|
||||
}
|
||||
)
|
||||
}
|
||||
simplexWindowState.window = window
|
||||
AppScreen()
|
||||
if (simplexWindowState.openDialog.isAwaiting) {
|
||||
FileDialogChooser(
|
||||
title = "SimpleX",
|
||||
isLoad = true,
|
||||
params = simplexWindowState.openDialog.params,
|
||||
onResult = {
|
||||
simplexWindowState.openDialog.onResult(it.firstOrNull())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (simplexWindowState.openMultipleDialog.isAwaiting) {
|
||||
FileDialogChooser(
|
||||
title = "SimpleX",
|
||||
isLoad = true,
|
||||
params = simplexWindowState.openMultipleDialog.params,
|
||||
onResult = {
|
||||
simplexWindowState.openMultipleDialog.onResult(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
if (simplexWindowState.openMultipleDialog.isAwaiting) {
|
||||
FileDialogChooser(
|
||||
title = "SimpleX",
|
||||
isLoad = true,
|
||||
params = simplexWindowState.openMultipleDialog.params,
|
||||
onResult = {
|
||||
simplexWindowState.openMultipleDialog.onResult(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (simplexWindowState.saveDialog.isAwaiting) {
|
||||
FileDialogChooser(
|
||||
title = "SimpleX",
|
||||
isLoad = false,
|
||||
params = simplexWindowState.saveDialog.params,
|
||||
onResult = { simplexWindowState.saveDialog.onResult(it.firstOrNull()) }
|
||||
)
|
||||
}
|
||||
val toasts = remember { simplexWindowState.toasts }
|
||||
val toast = toasts.firstOrNull()
|
||||
if (toast != null) {
|
||||
if (simplexWindowState.saveDialog.isAwaiting) {
|
||||
FileDialogChooser(
|
||||
title = "SimpleX",
|
||||
isLoad = false,
|
||||
params = simplexWindowState.saveDialog.params,
|
||||
onResult = { simplexWindowState.saveDialog.onResult(it.firstOrNull()) }
|
||||
)
|
||||
}
|
||||
val toasts = remember { simplexWindowState.toasts }
|
||||
val toast = toasts.firstOrNull()
|
||||
if (toast != null) {
|
||||
SimpleXTheme {
|
||||
Box(Modifier.fillMaxSize().padding(bottom = 20.dp), contentAlignment = Alignment.BottomCenter) {
|
||||
Text(
|
||||
escapedHtmlToAnnotatedString(toast.first, LocalDensity.current),
|
||||
@@ -107,11 +153,11 @@ fun showApp() = application {
|
||||
style = MaterialTheme.typography.body1
|
||||
)
|
||||
}
|
||||
// Shows toast in insertion order with preferred delay per toast. New one will be shown once previous one expires
|
||||
LaunchedEffect(toast, toasts.size) {
|
||||
delay(toast.second)
|
||||
simplexWindowState.toasts.removeFirst()
|
||||
}
|
||||
}
|
||||
// Shows toast in insertion order with preferred delay per toast. New one will be shown once previous one expires
|
||||
LaunchedEffect(toast, toasts.size) {
|
||||
delay(toast.second)
|
||||
simplexWindowState.toasts.removeFirst()
|
||||
}
|
||||
}
|
||||
var windowFocused by remember { simplexWindowState.windowFocused }
|
||||
@@ -160,6 +206,7 @@ class SimplexWindowState {
|
||||
val saveDialog = DialogState<File?>()
|
||||
val toasts = mutableStateListOf<Pair<String, Long>>()
|
||||
var windowFocused = mutableStateOf(true)
|
||||
var window: ComposeWindow? = null
|
||||
}
|
||||
|
||||
data class DialogParams(
|
||||
@@ -188,7 +235,5 @@ class DialogState<T> {
|
||||
@Preview
|
||||
@Composable
|
||||
fun AppPreview() {
|
||||
SimpleXTheme {
|
||||
AppScreen()
|
||||
}
|
||||
AppScreen()
|
||||
}
|
||||
|
||||
+6
@@ -19,3 +19,9 @@ actual fun getKeyboardState(): State<KeyboardState> = remember { mutableStateOf(
|
||||
actual fun hideKeyboard(view: Any?) {}
|
||||
|
||||
actual fun androidIsFinishingMainActivity(): Boolean = false
|
||||
|
||||
actual class GlobalExceptionsHandler: Thread.UncaughtExceptionHandler {
|
||||
actual override fun uncaughtException(thread: Thread, e: Throwable) {
|
||||
Log.e(TAG, "App crashed, thread name: " + thread.name + ", exception: " + e.stackTraceToString())
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -6,6 +6,7 @@ import androidx.compose.runtime.Composable
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.model.SharedPreference
|
||||
import chat.simplex.common.model.User
|
||||
import chat.simplex.common.platform.chatModel
|
||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
@@ -14,7 +15,7 @@ import dev.icerock.moko.resources.compose.painterResource
|
||||
actual fun OnboardingActionButton(user: User?, onboardingStage: SharedPreference<OnboardingStage>, onclick: (() -> Unit)?) {
|
||||
if (user == null) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(DEFAULT_PADDING * 2.5f)) {
|
||||
OnboardingActionButton(MR.strings.link_a_mobile, onboarding = if (controller.appPrefs.initialRandomDBPassphrase.get()) OnboardingStage.Step2_5_SetupDatabasePassphrase else OnboardingStage.LinkAMobile, true, icon = painterResource(MR.images.ic_smartphone_300), onclick = onclick)
|
||||
OnboardingActionButton(MR.strings.link_a_mobile, onboarding = if (controller.appPrefs.initialRandomDBPassphrase.get() && !chatModel.desktopOnboardingRandomPassword.value) OnboardingStage.Step2_5_SetupDatabasePassphrase else OnboardingStage.LinkAMobile, true, icon = painterResource(MR.images.ic_smartphone_300), onclick = onclick)
|
||||
OnboardingActionButton(MR.strings.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, true, icon = painterResource(MR.images.ic_desktop), onclick = onclick)
|
||||
}
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user