multiplatform: split common/android/desktop (#2672)

* multiplatform: relocated code to its new place

* code becomes better

* renamed file

* fixes for BASE64 and images, and changes for appFileUri

* different Base64 for both platforms

* fix file saving on long click

* platformCallbacks refactoring

* renamed callbacks to platform

* eol

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
Stanislav DmitrenkoandEvgeny Poberezkin authored and GitHub committed 2023-07-12 14:42:10 +01:00
1 parent ff7c22e114
commit 38f40fec3d
251 files changed
+4290 -2469

No files matched your search

@@ -0,0 +1,127 @@
package chat.simplex.common
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.*
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.ui.theme.SimpleXTheme
import chat.simplex.common.views.helpers.FileDialogChooser
import kotlinx.coroutines.*
import java.awt.event.WindowEvent
import java.awt.event.WindowFocusListener
import java.io.File
val simplexWindowState = SimplexWindowState()
fun showApp() = application {
val windowState = rememberWindowState(placement = WindowPlacement.Floating)
Window(state = windowState, onCloseRequest = ::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,
onResult = {
simplexWindowState.openDialog.onResult(it)
}
)
}
if (simplexWindowState.saveDialog.isAwaiting) {
FileDialogChooser(
title = "SimpleX",
isLoad = false,
onResult = { simplexWindowState.saveDialog.onResult(it) }
)
}
val toasts = remember { simplexWindowState.toasts }
val toast = toasts.firstOrNull()
if (toast != null) {
Box(Modifier.fillMaxSize().padding(bottom = 20.dp), contentAlignment = Alignment.BottomCenter) {
Text(
toast.first,
Modifier.background(MaterialTheme.colors.primary, RoundedCornerShape(100)).padding(vertical = 5.dp, horizontal = 10.dp),
color = MaterialTheme.colors.onPrimary,
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()
}
}
}
var windowFocused by remember { mutableStateOf(true) }
LaunchedEffect(windowFocused) {
val delay = ChatController.appPrefs.laLockDelay.get()
if (!windowFocused && ChatModel.performLA.value && delay > 0) {
delay(delay * 1000L)
// Trigger auth state check when delay ends (and if it ends)
AppLock.recheckAuthState()
}
}
LaunchedEffect(Unit) {
window.addWindowFocusListener(object : WindowFocusListener {
override fun windowGainedFocus(p0: WindowEvent?) {
windowFocused = true
AppLock.recheckAuthState()
}
override fun windowLostFocus(p0: WindowEvent?) {
windowFocused = false
AppLock.appWasHidden()
}
})
}
}
}
class SimplexWindowState {
val backstack = mutableStateListOf<() -> Unit>()
val openDialog = DialogState<File?>()
val saveDialog = DialogState<File?>()
val toasts = mutableStateListOf<Pair<String, Long>>()
}
class DialogState<T> {
private var onResult: CompletableDeferred<T>? by mutableStateOf(null)
val isAwaiting get() = onResult != null
suspend fun awaitResult(): T {
onResult = CompletableDeferred()
val result = onResult!!.await()
onResult = null
return result
}
fun onResult(result: T) = onResult!!.complete(result)
}
@Preview
@Composable
fun AppPreview() {
SimpleXTheme {
AppScreen()
}
}
/** Needed for [chat.simplex.common.platform.Files] to get path to jar file */
class DesktopApp()
@@ -0,0 +1,67 @@
package chat.simplex.common.platform
import chat.simplex.common.DesktopApp
import chat.simplex.common.model.*
import chat.simplex.common.views.call.RcvCallInvitation
import chat.simplex.common.views.helpers.withBGApi
import java.io.*
import java.nio.file.*
import java.nio.file.attribute.BasicFileAttributes
import java.util.*
actual val appPlatform = AppPlatform.DESKTOP
@Suppress("ConstantLocale")
val defaultLocale: Locale = Locale.getDefault()
fun initApp() {
ntfManager = object : NtfManager() { // LALAL
override fun notifyContactConnected(user: User, contact: Contact) {}
override fun notifyContactRequestReceived(user: User, cInfo: ChatInfo.ContactRequest) {}
override fun notifyMessageReceived(user: User, cInfo: ChatInfo, cItem: ChatItem) {}
override fun notifyCallInvitation(invitation: RcvCallInvitation) {}
override fun hasNotificationsForChat(chatId: String): Boolean = false
override fun cancelNotificationsForChat(chatId: String) {}
override fun displayNotification(user: User, chatId: String, displayName: String, msgText: String, image: String?, actions: List<NotificationAction>) {}
override fun createNtfChannelsMaybeShowAlert() {}
override fun cancelCallNotification() {}
override fun cancelAllNotifications() {}
}
applyAppLocale()
withBGApi {
initChatController()
runMigrations()
}
}
private fun applyAppLocale() {
val lang = ChatController.appPrefs.appLanguage.get()
if (lang == null || lang == Locale.getDefault().language) return
Locale.setDefault(Locale.forLanguageTag(lang))
}
@Suppress("UnsafeDynamicallyLoadedCode")
actual fun initHaskell() {
val libApp = "libapp-lib.${desktopPlatform.libExtension}"
val tmpDir = Files.createTempDirectory("simplex-native-libs").toFile()
tmpDir.deleteOnExit()
copyResources(desktopPlatform.libPath, tmpDir.toPath())
System.load(File(tmpDir, libApp).absolutePath)
initHS()
}
private fun copyResources(from: String, to: Path) {
val resource = DesktopApp::class.java.getResource("")!!.toURI()
val fileSystem = FileSystems.newFileSystem(resource, emptyMap<String, String>())
val resPath = fileSystem.getPath(from)
Files.walkFileTree(resPath, object: SimpleFileVisitor<Path>() {
override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult {
Files.createDirectories(to.resolve(resPath.relativize(dir).toString()))
return FileVisitResult.CONTINUE
}
override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult {
Files.copy(file, to.resolve(resPath.relativize(file).toString()), StandardCopyOption.REPLACE_EXISTING)
return FileVisitResult.CONTINUE
}
})
}
@@ -0,0 +1,14 @@
package chat.simplex.common.platform
import androidx.compose.runtime.*
import chat.simplex.common.simplexWindowState
@Composable
actual fun BackHandler(enabled: Boolean, onBack: () -> Unit) {
DisposableEffect(enabled) {
if (enabled) simplexWindowState.backstack.add(onBack)
onDispose {
simplexWindowState.backstack.remove(onBack)
}
}
}
@@ -0,0 +1,15 @@
package chat.simplex.common.platform
actual val cryptor: CryptorInterface = object : CryptorInterface {
override fun decryptData(data: ByteArray, iv: ByteArray, alias: String): String? {
return String(data) // LALAL
}
override fun encryptText(text: String, alias: String): Pair<ByteArray, ByteArray> {
return text.toByteArray() to text.toByteArray() // LALAL
}
override fun deleteKey(alias: String) {
// LALAL
}
}
@@ -0,0 +1,42 @@
package chat.simplex.common.platform
import androidx.compose.runtime.*
import chat.simplex.common.DesktopApp
import chat.simplex.common.simplexWindowState
import java.io.*
import java.net.URI
private fun applicationParentPath(): String = try {
DesktopApp::class.java.protectionDomain!!.codeSource.location.toURI().path
.replaceAfterLast("/", "")
.replaceAfterLast(File.separator, "")
.replace("/", File.separator)
} catch (e: Exception) {
"./"
}
actual val dataDir: File = File(desktopPlatform.configPath)
actual val tmpDir: File = File(System.getProperty("java.io.tmpdir"))
actual val cacheDir: File = tmpDir
@Composable
actual fun rememberFileChooserLauncher(getContent: Boolean, onResult: (URI?) -> Unit): FileChooserLauncher =
remember { FileChooserLauncher(getContent, onResult) }
actual class FileChooserLauncher actual constructor() {
var getContent: Boolean = false
lateinit var onResult: (URI?) -> Unit
constructor(getContent: Boolean, onResult: (URI?) -> Unit): this() {
this.getContent = getContent
this.onResult = onResult
}
actual suspend fun launch(input: String) {
val res = if (getContent) simplexWindowState.openDialog.awaitResult() else simplexWindowState.saveDialog.awaitResult()
onResult(if (!getContent && input.isNotEmpty() && res != null) File(res, input).toURI() else res?.toURI())
}
}
actual fun URI.inputStream(): InputStream? = File(URI("file:" + toString().removePrefix("file:"))).inputStream()
actual fun URI.outputStream(): OutputStream = File(URI("file:" + toString().removePrefix("file:"))).outputStream()
@@ -0,0 +1,129 @@
package chat.simplex.common.platform
import androidx.compose.ui.graphics.*
import boofcv.io.image.ConvertBufferedImage
import boofcv.struct.image.GrayU8
import chat.simplex.res.MR
import org.jetbrains.skia.Image
import java.awt.RenderingHints
import java.awt.image.BufferedImage
import java.io.*
import java.net.URI
import java.util.*
import javax.imageio.ImageIO
import kotlin.math.sqrt
private fun errorBitmap(): ImageBitmap =
ImageIO.read(ByteArrayInputStream(Base64.getDecoder().decode("iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAKVJREFUeF7t1kENACEUQ0FQhnVQ9lfGO+xggITQdvbMzArPey+8fa3tAfwAEdABZQspQStgBssEcgAIkSAJkiAJljtEgiRIgmUCSZAESZAESZAEyx0iQRIkwTKBJEiCv5fgvTd1wDmn7QAP4AeIgA4oW0gJWgEzWCZwbQ7gAA7ggLKFOIADOKBMIAeAEAmSIAmSYLlDJEiCJFgmkARJkARJ8N8S/ADTZUewBvnTOQAAAABJRU5ErkJggg=="))).toComposeImageBitmap()
actual fun base64ToBitmap(base64ImageString: String): ImageBitmap {
val imageString = base64ImageString
.removePrefix("data:image/png;base64,")
.removePrefix("data:image/jpg;base64,")
return try {
ImageIO.read(ByteArrayInputStream(Base64.getDecoder().decode(imageString))).toComposeImageBitmap()
} catch (e: IOException) {
Log.e(TAG, "base64ToBitmap error: $e")
errorBitmap()
}
}
actual fun resizeImageToStrSize(image: ImageBitmap, maxDataSize: Long): String {
var img = image
var str = compressImageStr(img)
while (str.length > maxDataSize) {
val ratio = sqrt(str.length.toDouble() / maxDataSize.toDouble())
val clippedRatio = kotlin.math.min(ratio, 2.0)
val width = (img.width.toDouble() / clippedRatio).toInt()
val height = img.height * width / img.width
img = img.scale(width, height)
str = compressImageStr(img)
}
return str
}
actual fun resizeImageToDataSize(image: ImageBitmap, usePng: Boolean, maxDataSize: Long): ByteArrayOutputStream = TODO()
actual fun cropToSquare(image: ImageBitmap): ImageBitmap {
var xOffset = 0
var yOffset = 0
val side = kotlin.math.min(image.height, image.width)
if (image.height < image.width) {
xOffset = (image.width - side) / 2
} else {
yOffset = (image.height - side) / 2
}
// LALAL MAKE REAL CROP
return image
}
actual fun compressImageStr(bitmap: ImageBitmap): String {
val usePng = bitmap.hasAlpha
val ext = if (usePng) "png" else "jpg"
return try {
val encoded = Base64.getEncoder().encodeToString(compressImageData(bitmap, usePng).toByteArray())
"data:image/$ext;base64,$encoded"
} catch (e: IOException) {
Log.e(TAG, "resizeImageToStrSize error: $e")
throw e
}
}
actual fun compressImageData(bitmap: ImageBitmap, usePng: Boolean): ByteArrayOutputStream {
val stream = ByteArrayOutputStream()
stream.use { s -> ImageIO.write(bitmap.toAwtImage(), if (usePng) "png" else "jpg", s) }
// MAKE REAL COMPRESSION
//bitmap.compress(if (!usePng) Bitmap.CompressFormat.JPEG else Bitmap.CompressFormat.PNG, 85, stream)
return stream
}
actual fun GrayU8.toImageBitmap(): ImageBitmap = ConvertBufferedImage.extractBuffered(this).toComposeImageBitmap()
actual fun ImageBitmap.addLogo(): ImageBitmap {
val radius = (width * 0.16f).toInt()
val logoSize = (width * 0.24).toInt()
val logo: BufferedImage = MR.images.icon_foreground_common.image
val original = toAwtImage()
val withLogo = BufferedImage(width, height, original.type)
val g = withLogo.createGraphics()
g.setRenderingHint(
RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BILINEAR
)
g.drawImage(original, 0, 0, width, height, 0, 0, original.width, original.height, null)
g.fillRoundRect(width / 2 - radius / 2, height / 2 - radius / 2, radius, radius, radius, radius)
g.drawImage(logo, (width - logoSize) / 2, (height - logoSize) / 2, logoSize, logoSize, null)
g.dispose()
return withLogo.toComposeImageBitmap()
}
actual fun ImageBitmap.scale(width: Int, height: Int): ImageBitmap {
val original = toAwtImage()
val resized = BufferedImage(width, height, original.type)
val g = resized.createGraphics()
g.setRenderingHint(
RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BILINEAR
)
g.drawImage(original, 0, 0, width, height, 0, 0, original.width, original.height, null)
g.dispose()
return resized.toComposeImageBitmap()
}
// LALAL
actual fun isImage(uri: URI): Boolean {
val path = uri.path.lowercase()
return path.endsWith(".gif") ||
path.endsWith(".webp") ||
path.endsWith(".png") ||
path.endsWith(".jpg") ||
path.endsWith(".jpeg")
}
actual fun isAnimImage(uri: URI, drawable: Any?): Boolean {
val path = uri.path.lowercase()
return path.endsWith(".gif") || path.endsWith(".webp")
}
@Suppress("NewApi")
actual fun loadImageBitmap(inputStream: InputStream): ImageBitmap =
Image.makeFromEncoded(inputStream.readAllBytes()).toComposeImageBitmap()
@@ -0,0 +1,8 @@
package chat.simplex.common.platform
actual object Log {
actual fun d(tag: String, text: String) = println("D: $text")
actual fun e(tag: String, text: String) = println("E: $text")
actual fun i(tag: String, text: String) = println("I: $text")
actual fun w(tag: String, text: String) = println("W: $text")
}
@@ -0,0 +1,15 @@
package chat.simplex.common.platform
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
actual fun Modifier.navigationBarsWithImePadding(): Modifier = this
@Composable
actual fun ProvideWindowInsets(
consumeWindowInsets: Boolean,
windowInsetsAnimationsEnabled: Boolean,
content: @Composable () -> Unit
) {
content()
}
@@ -0,0 +1,5 @@
package chat.simplex.common.platform
import chat.simplex.common.model.NotificationsMode
actual fun allowedToShowNotification(): Boolean = true
@@ -0,0 +1,28 @@
package chat.simplex.common.platform
import java.io.File
import java.util.*
private val home = System.getProperty("user.home")
val desktopPlatform = detectDesktopPlatform()
enum class DesktopPlatform(val libPath: String, val libExtension: String, val configPath: String) {
LINUX_X86_64("/libs/linux-x86_64", "so", "$home/.config/simplex"),
LINUX_AARCH64("/libs/aarch64", "so", "$home/.config/simplex"),
WINDOWS_X86_64("/libs/windows-x86_64", "dll", System.getenv("AppData") + File.separator + "simplex"),
MAC_X86_64("/libs/mac-x86_64", "dylib", "$home/.config/simplex"),
MAC_AARCH64("/libs/mac-aarch64", "dylib", "$home/.config/simplex");
}
private fun detectDesktopPlatform(): DesktopPlatform {
val os = System.getProperty("os.name", "generic").lowercase(Locale.ENGLISH)
val arch = System.getProperty("os.arch")
return when {
os == "linux" && (arch.contains("x86") || arch == "amd64") -> DesktopPlatform.LINUX_X86_64
os == "linux" && arch == "aarch64" -> DesktopPlatform.LINUX_AARCH64
os.contains("windows") && (arch.contains("x86") || arch == "amd64") -> DesktopPlatform.WINDOWS_X86_64
os.contains("mac") && arch.contains("x86") -> DesktopPlatform.MAC_X86_64
os.contains("mac") && arch.contains("aarch64") -> DesktopPlatform.MAC_AARCH64
else -> TODO("Currently, your processor's architecture ($arch) or os ($os) are unsupported. Please, contact us")
}
}
@@ -0,0 +1,107 @@
package chat.simplex.common.platform
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.*
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import chat.simplex.common.views.chat.*
import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.delay
import kotlin.math.min
@Composable
actual fun PlatformTextField(
composeState: MutableState<ComposeState>,
textStyle: MutableState<TextStyle>,
showDeleteTextButton: MutableState<Boolean>,
userIsObserver: Boolean,
onMessageChange: (String) -> Unit
) {
val cs = composeState.value
val focusRequester = remember { FocusRequester() }
val keyboard = LocalSoftwareKeyboardController.current
val padding = PaddingValues(12.dp, 7.dp, 45.dp, 0.dp)
LaunchedEffect(cs.contextItem) {
if (cs.contextItem !is ComposeContextItem.QuotedItem) return@LaunchedEffect
// In replying state
focusRequester.requestFocus()
delay(50)
keyboard?.show()
}
val isRtl = remember(cs.message) { isRtl(cs.message.subSequence(0, min(50, cs.message.length))) }
BasicTextField(
value = cs.message,
onValueChange = {
if (!composeState.value.inProgress && !(composeState.value.preview is ComposePreview.VoicePreview && it != "")) {
onMessageChange(it)
}
},
textStyle = textStyle.value,
maxLines = 16,
keyboardOptions = KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences,
autoCorrect = true
),
modifier = Modifier.padding(vertical = 4.dp).focusRequester(focusRequester),
cursorBrush = SolidColor(MaterialTheme.colors.secondary),
decorationBox = { innerTextField ->
Surface(
shape = RoundedCornerShape(18.dp),
border = BorderStroke(1.dp, MaterialTheme.colors.secondary)
) {
Row(
Modifier.background(MaterialTheme.colors.background),
verticalAlignment = Alignment.Bottom
) {
CompositionLocalProvider(
LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LocalLayoutDirection.current
) {
Box(
Modifier
.weight(1f)
.padding(horizontal = 12.dp)
.padding(top = 4.dp)
.padding(bottom = 6.dp)
) {
innerTextField()
}
}
}
}
}
)
showDeleteTextButton.value = cs.message.split("\n").size >= 4 && !cs.inProgress
if (composeState.value.preview is ComposePreview.VoicePreview) {
ComposeOverlay(MR.strings.voice_message_send_text, textStyle, padding)
} else if (userIsObserver) {
ComposeOverlay(MR.strings.you_are_observer, textStyle, padding)
}
}
@Composable
private fun ComposeOverlay(textId: StringResource, textStyle: MutableState<TextStyle>, padding: PaddingValues) {
Text(
generalGetString(textId),
Modifier.padding(padding),
color = MaterialTheme.colors.secondary,
style = textStyle.value.copy(fontStyle = FontStyle.Italic)
)
}
@@ -0,0 +1,53 @@
package chat.simplex.common.platform
import androidx.compose.runtime.MutableState
import chat.simplex.common.model.ChatItem
import kotlinx.coroutines.CoroutineScope
actual class RecorderNative: RecorderInterface {
override fun start(onProgressUpdate: (position: Int?, finished: Boolean) -> Unit): String {
/*LALAL*/
return ""
}
override fun stop(): Int {
/*LALAL*/
return 0
}
}
actual object AudioPlayer: AudioPlayerInterface {
override fun play(filePath: String?, audioPlaying: MutableState<Boolean>, progress: MutableState<Int>, duration: MutableState<Int>, resetOnEnd: Boolean) {
/*LALAL*/
}
override fun stop() {
/*LALAL*/
}
override fun stop(item: ChatItem) {
/*LALAL*/
}
override fun stop(fileName: String?) {
TODO("Not yet implemented")
}
override fun pause(audioPlaying: MutableState<Boolean>, pro: MutableState<Int>) {
TODO("Not yet implemented")
}
override fun seekTo(ms: Int, pro: MutableState<Int>, filePath: String?) {
/*LALAL*/
}
override fun duration(filePath: String): Int? {
/*LALAL*/
return null
}
}
actual object SoundPlayer: SoundPlayerInterface {
override fun start(scope: CoroutineScope, sound: Boolean) { /*LALAL*/ }
override fun stop() { /*LALAL*/ }
}
@@ -0,0 +1,58 @@
package chat.simplex.common.platform
import androidx.compose.runtime.*
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.russhwolf.settings.*
import dev.icerock.moko.resources.StringResource
import dev.icerock.moko.resources.desc.desc
import java.io.File
import java.util.*
@Composable
actual fun font(name: String, res: String, weight: FontWeight, style: FontStyle): Font =
androidx.compose.ui.text.platform.Font("MR/fonts/$res.ttf", weight, style)
actual fun StringResource.localized(): String = desc().toString()
actual fun isInNightMode() = false
private val settingsFile =
File(desktopPlatform.configPath + File.separator + "settings.properties")
.also { it.parentFile.mkdirs() }
private val settingsThemesFile =
File(desktopPlatform.configPath + File.separator + "themes.properties")
.also { it.parentFile.mkdirs() }
private val settingsProps =
Properties()
.also { try { it.load(settingsFile.reader()) } catch (e: Exception) { Properties() } }
private val settingsThemesProps =
Properties()
.also { try { it.load(settingsThemesFile.reader()) } catch (e: Exception) { Properties() } }
actual val settings: Settings = PropertiesSettings(settingsProps) { settingsProps.store(settingsFile.writer(), "") }
actual val settingsThemes: Settings = PropertiesSettings(settingsThemesProps) { settingsThemesProps.store(settingsThemesFile.writer(), "") }
actual fun screenOrientation(): ScreenOrientation = ScreenOrientation.UNDEFINED
@Composable // LALAL
actual fun screenWidth(): Dp {
return java.awt.Toolkit.getDefaultToolkit().screenSize.width.dp.also { println("LALAL $it") }
/*var width by remember { mutableStateOf(java.awt.Toolkit.getDefaultToolkit().screenSize.width.also { println("LALAL $it") }) }
SideEffect {
if (width != java.awt.Toolkit.getDefaultToolkit().screenSize.width)
width = java.awt.Toolkit.getDefaultToolkit().screenSize.width
}
return width*/
}// LALAL java.awt.Desktop.getDesktop()
actual fun isRtl(text: CharSequence): Boolean {
if (text.isEmpty()) return false
return text.any { char ->
val dir = Character.getDirectionality(char)
dir == Character.DIRECTIONALITY_RIGHT_TO_LEFT || dir == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC
}
}
@@ -0,0 +1,31 @@
package chat.simplex.common.platform
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.UriHandler
import androidx.compose.ui.text.AnnotatedString
import chat.simplex.common.views.helpers.withApi
import java.io.File
import java.net.URI
import java.net.URLEncoder
import chat.simplex.res.MR
actual fun UriHandler.sendEmail(subject: String, body: CharSequence) {
val subjectEncoded = URLEncoder.encode(subject, "UTF-8").replace("+", "%20")
val bodyEncoded = URLEncoder.encode(subject, "UTF-8").replace("+", "%20")
openUri("mailto:?subject=$subjectEncoded&body=$bodyEncoded")
}
actual fun ClipboardManager.shareText(text: String) {
setText(AnnotatedString(text))
showToast(MR.strings.copied.localized())
}
actual fun shareFile(text: String, filePath: String) {
withApi {
FileChooserLauncher(false) { to: URI? ->
if (to != null) {
copyFileToFile(File(filePath), to) {}
}
}.launch(filePath)
}
}
@@ -0,0 +1,19 @@
package chat.simplex.common.platform
import androidx.compose.runtime.*
import chat.simplex.common.simplexWindowState
import chat.simplex.common.views.helpers.KeyboardState
actual fun showToast(text: String, timeout: Long) {
simplexWindowState.toasts.add(text to timeout)
}
@Composable
actual fun LockToCurrentOrientationUntilDispose() {}
@Composable
actual fun LocalMultiplatformView(): Any? = null
@Composable
actual fun getKeyboardState(): State<KeyboardState> = remember { mutableStateOf(KeyboardState.Opened) }
actual fun hideKeyboard(view: Any?) {}
@@ -0,0 +1,50 @@
package chat.simplex.common.platform
import androidx.compose.runtime.MutableState
import androidx.compose.ui.graphics.ImageBitmap
import java.net.URI
actual class VideoPlayer: VideoPlayerInterface {
actual companion object {
actual fun getOrCreate(
uri: URI,
gallery: Boolean,
defaultPreview: ImageBitmap,
defaultDuration: Long,
soundEnabled: Boolean
): VideoPlayer = VideoPlayer()
actual fun enableSound(enable: Boolean, fileName: String?, gallery: Boolean): Boolean { TODO() }
actual fun release(uri: URI, gallery: Boolean, remove: Boolean) { TODO() }
actual fun stopAll() { /*LALAL*/ }
actual fun releaseAll() { /*LALAL*/ }
}
override val soundEnabled: MutableState<Boolean>
get() = TODO("Not yet implemented")
override val brokenVideo: MutableState<Boolean>
get() = TODO("Not yet implemented")
override val videoPlaying: MutableState<Boolean>
get() = TODO("Not yet implemented")
override val progress: MutableState<Long>
get() = TODO("Not yet implemented")
override val duration: MutableState<Long>
get() = TODO("Not yet implemented")
override val preview: MutableState<ImageBitmap>
get() = TODO("Not yet implemented")
override fun stop() {
TODO("Not yet implemented")
}
override fun play(resetOnEnd: Boolean) {
TODO("Not yet implemented")
}
override fun enableSound(enable: Boolean): Boolean {
TODO("Not yet implemented")
}
override fun release(remove: Boolean) {
TODO("Not yet implemented")
}
}
@@ -0,0 +1,14 @@
package chat.simplex.common.ui.theme
import androidx.compose.ui.text.font.*
import androidx.compose.ui.text.platform.Font
import chat.simplex.res.MR
actual val Inter: FontFamily = FontFamily(
Font(MR.fonts.Inter.regular.file),
Font(MR.fonts.Inter.italic.file, style = FontStyle.Italic),
Font(MR.fonts.Inter.bold.file, FontWeight.Bold),
Font(MR.fonts.Inter.semibold.file, FontWeight.SemiBold),
Font(MR.fonts.Inter.medium.file, FontWeight.Medium),
Font(MR.fonts.Inter.light.file, FontWeight.Light)
)
@@ -0,0 +1,8 @@
package chat.simplex.common.views.call
import androidx.compose.runtime.Composable
@Composable
actual fun ActiveCallView() {
// LALAL
}
@@ -0,0 +1,24 @@
package chat.simplex.common.views.chat
import androidx.compose.runtime.*
import chat.simplex.common.views.helpers.AttachmentOption
import java.net.URI
@Composable
actual fun AttachmentSelection(
composeState: MutableState<ComposeState>,
attachmentOption: MutableState<AttachmentOption?>,
processPickedFile: (URI?, String?) -> Unit,
processPickedMedia: (List<URI>, String?) -> Unit
) {
LaunchedEffect(attachmentOption.value) {
when (attachmentOption.value) {
AttachmentOption.CameraPhoto -> {}
AttachmentOption.GalleryImage -> {}
AttachmentOption.GalleryVideo -> {}
AttachmentOption.File -> {}
else -> {}
}
attachmentOption.value = null
}
}
@@ -0,0 +1,8 @@
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)
}
@@ -0,0 +1,11 @@
package chat.simplex.common.views.chat
import androidx.compose.runtime.Composable
@Composable
actual fun allowedToRecordVoiceByPlatform(): Boolean = true
@Composable
actual fun VoiceButtonWithoutPermissionByPlatform() {
VoiceButtonWithoutPermission {}
}
@@ -0,0 +1,28 @@
package chat.simplex.common.views.chat.item
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
import chat.simplex.common.model.CIFile
import chat.simplex.common.platform.*
import chat.simplex.common.views.helpers.ModalManager
import java.net.URI
@Composable
actual fun SimpleAndAnimatedImageView(
uri: URI,
imageBitmap: ImageBitmap,
file: CIFile?,
imageProvider: () -> ImageGalleryProvider,
ImageView: @Composable (painter: Painter, onClick: () -> Unit) -> Unit
) {
// LALAL make it animated too
ImageView(imageBitmap.toAwtImage().toPainter()) {
if (getLoadedFilePath(file) != null) {
ModalManager.shared.showCustomModal(animated = false) { close ->
ImageFullScreenView(imageProvider, close)
}
}
}
}
@@ -0,0 +1,23 @@
package chat.simplex.common.views.chat.item
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import chat.simplex.common.platform.VideoPlayer
@Composable
actual fun PlayerView(player: VideoPlayer, width: Dp, onClick: () -> Unit, onLongClick: () -> Unit, stop: () -> Unit) {
/* LALAL */
}
@Composable
actual fun LocalWindowWidth(): Dp {
return with(LocalDensity.current) { (java.awt.Window.getWindows().find { it.isActive }?.width ?: 0).toDp() }
/*val density = LocalDensity.current
var width by remember { mutableStateOf(with(density) { (java.awt.Window.getWindows().find { it.isActive }?.width ?: 0).toDp() }) }
SideEffect {
if (width != with(density) { (java.awt.Window.getWindows().find { it.isActive }?.width ?: 0).toDp() })
width = with(density) { (java.awt.Window.getWindows().find { it.isActive }?.width ?: 0).toDp() }
}
return width.also { println("LALAL $it") }*/
}
@@ -0,0 +1,23 @@
package chat.simplex.common.views.chat.item
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import chat.simplex.common.model.ChatItem
import chat.simplex.common.model.MsgContent
import chat.simplex.common.platform.FileChooserLauncher
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
@Composable
actual fun SaveContentItemAction(cItem: ChatItem, saveFileLauncher: FileChooserLauncher, showMenu: MutableState<Boolean>) {
ItemAction(stringResource(MR.strings.save_verb), painterResource(MR.images.ic_download), onClick = {
when (cItem.content.msgContent) {
is MsgContent.MCImage -> saveImage(getAppFileUri(cItem.file?.fileName ?: return@ItemAction))
is MsgContent.MCFile, is MsgContent.MCVoice, is MsgContent.MCVideo -> withApi { saveFileLauncher.launch(cItem.file?.fileName ?: "") }
else -> {}
}
showMenu.value = false
})
}
@@ -0,0 +1,28 @@
package chat.simplex.common.views.chat.item
import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.layout.ContentScale
import chat.simplex.common.platform.VideoPlayer
import chat.simplex.common.views.helpers.getBitmapFromUri
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import java.net.URI
@Composable
actual fun FullScreenImageView(modifier: Modifier, uri: URI, imageBitmap: ImageBitmap) {
Image(
getBitmapFromUri(uri, false) ?: MR.images.decentralized.image.toComposeImageBitmap(),
contentDescription = stringResource(MR.strings.image_descr),
contentScale = ContentScale.Fit,
modifier = modifier,
)
}
@Composable
actual fun FullScreenVideoView(player: VideoPlayer, modifier: Modifier) {
}
@@ -0,0 +1,119 @@
package chat.simplex.common.views.helpers
import androidx.compose.runtime.*
import androidx.compose.ui.input.key.*
import androidx.compose.ui.window.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.awt.FileDialog
import java.io.File
import java.util.*
import javax.swing.JFileChooser
import javax.swing.filechooser.FileFilter
import javax.swing.filechooser.FileNameExtensionFilter
@Composable
actual fun DefaultDialog(
onDismissRequest: () -> Unit,
content: @Composable () -> Unit
) {
Dialog(
undecorated = true,
title = "",
onCloseRequest = onDismissRequest,
onPreviewKeyEvent = { event ->
if (event.key == Key.Escape && event.type == KeyEventType.KeyUp) {
onDismissRequest(); true
} else false
}
) {
content()
}
}
@Composable
fun FrameWindowScope.FileDialogChooser(
title: String,
isLoad: Boolean,
extensions: List<FileFilter> = emptyList(),
onResult: (result: File?) -> Unit
) {
if (isLinux()) {
FileDialogChooserMultiple(title, isLoad, extensions) { onResult(it.firstOrNull()) }
} else {
FileDialogAwt(title, isLoad, onResult)
}
}
@Composable
fun FrameWindowScope.FileDialogChooserMultiple(
title: String,
isLoad: Boolean,
extensions: List<FileFilter> = emptyList(),
onResult: (result: List<File>) -> Unit
) {
val scope = rememberCoroutineScope()
DisposableEffect(Unit) {
val job = scope.launch(Dispatchers.Main) {
val fileChooser = JFileChooser()
fileChooser.dialogTitle = title
fileChooser.isMultiSelectionEnabled = isLoad
fileChooser.isAcceptAllFileFilterUsed = extensions.isEmpty()
extensions.forEach { fileChooser.addChoosableFileFilter(it) }
val returned = if (isLoad) {
fileChooser.showOpenDialog(window)
} else {
fileChooser.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
fileChooser.showSaveDialog(window)
}
val result = when (returned) {
JFileChooser.APPROVE_OPTION -> {
if (isLoad) {
fileChooser.selectedFiles.filter { it.canRead() }
} else {
if (!fileChooser.fileFilter.accept(fileChooser.selectedFile)) {
val ext = (fileChooser.fileFilter as FileNameExtensionFilter).extensions[0]
fileChooser.selectedFile = File(fileChooser.selectedFile.absolutePath + ".$ext")
}
listOf(fileChooser.selectedFile)
}
}
else -> listOf();
}
onResult(result)
}
onDispose {
job.cancel()
}
}
}
/*
* Has graphic glitches on many Linux distributions, so use only on non-Linux systems
* */
@Composable
private fun FrameWindowScope.FileDialogAwt(
title: String,
isLoad: Boolean,
onResult: (result: File?) -> Unit
) = AwtWindow(
create = {
object: FileDialog(window, "Choose a file", if (isLoad) LOAD else SAVE) {
override fun setVisible(value: Boolean) {
super.setVisible(value)
if (value) {
if (file != null) {
onResult(File(directory).resolve(file))
} else {
onResult(null)
}
}
}
}.apply {
this.title = title
}
},
dispose = FileDialog::dispose
)
fun isLinux(): Boolean = System.getProperty("os.name", "generic").lowercase(Locale.ENGLISH) == "linux"
@@ -0,0 +1,34 @@
package chat.simplex.common.views.helpers
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
actual interface DefaultExposedDropdownMenuBoxScope {
@Composable
actual fun DefaultExposedDropdownMenu(
expanded: Boolean,
onDismissRequest: () -> Unit,
modifier: Modifier,
content: @Composable ColumnScope.() -> Unit
) {
Column {
content()
}
}
}
@Composable
actual fun DefaultExposedDropdownMenuBox(
expanded: Boolean,
onExpandedChange: (Boolean) -> Unit,
modifier: Modifier,
content: @Composable DefaultExposedDropdownMenuBoxScope.() -> Unit
) {
if (expanded) {
val obj = remember { object : DefaultExposedDropdownMenuBoxScope {} }
obj.content()
}
}
@@ -0,0 +1,57 @@
package chat.simplex.common.views.helpers
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.ImageBitmap
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.unit.dp
import chat.simplex.common.platform.rememberFileChooserLauncher
import chat.simplex.res.MR
import chat.simplex.common.views.newchat.ActionButton
import java.net.URI
@Composable
actual fun GetImageBottomSheet(
imageBitmap: MutableState<URI?>,
onImageChange: (ImageBitmap) -> Unit,
hideBottomSheet: () -> Unit
) {
Box(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.onFocusChanged { focusState ->
if (!focusState.hasFocus) hideBottomSheet()
}
) {
Row(
Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 30.dp),
horizontalArrangement = Arrangement.SpaceEvenly
) {
val processPickedImage = { uri: URI? ->
if (uri != null) {
val bitmap = getBitmapFromUri(uri)
if (bitmap != null) {
imageBitmap.value = uri
onImageChange(bitmap)
}
hideBottomSheet()
}
}
val pickImageLauncher = rememberFileChooserLauncher(true, processPickedImage)
// LALAL
/*ActionButton(null, stringResource(MR.strings.use_camera_button), icon = painterResource(MR.images.ic_photo_camera)) {
hideBottomSheet()
}*/
ActionButton(null, stringResource(MR.strings.from_gallery_button), icon = painterResource(MR.images.ic_image)) {
// LALAL support providing file extensions
withApi { pickImageLauncher.launch("") }
}
}
}
}
@@ -0,0 +1,16 @@
package chat.simplex.common.views.helpers
import chat.simplex.common.views.usersettings.LAMode
actual fun authenticate(
promptTitle: String,
promptSubtitle: String,
selfDestruct: Boolean,
usingLAMode: LAMode,
completed: (LAResult) -> Unit
) {
when (usingLAMode) {
LAMode.PASSCODE -> authenticateWithPasscode(promptTitle, promptSubtitle, selfDestruct, completed)
else -> {}
}
}
@@ -0,0 +1,72 @@
package chat.simplex.common.views.helpers
import androidx.compose.ui.graphics.*
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.Density
import chat.simplex.common.model.CIFile
import chat.simplex.common.platform.*
import chat.simplex.common.simplexWindowState
import java.io.File
import java.net.URI
import java.util.*
import javax.imageio.ImageIO
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.io.path.toPath
// LALAL MAKE REALLY ANNOTATED STRING FROM HTML
actual fun escapedHtmlToAnnotatedString(text: String, density: Density): AnnotatedString {
return AnnotatedString(text)
}
actual fun getAppFileUri(fileName: String): URI =
URI("file:" + getAppFilesDirectory() + File.separator + fileName)
actual fun getLoadedImage(file: CIFile?): ImageBitmap? {
val filePath = getLoadedFilePath(file)
return if (filePath != null) {
val uri = getAppFileUri(filePath.substringAfterLast(File.separator))
getBitmapFromUri(uri, false)
} else {
null
}
}
actual fun getFileName(uri: URI): String? = uri.toPath().toFile().name
actual fun getAppFilePath(uri: URI): String? = getFilesDirectory() + File.separator + "app_files"
actual fun getFileSize(uri: URI): Long? = uri.toPath().toFile().length()
actual fun getBitmapFromUri(uri: URI, withAlertOnException: Boolean): ImageBitmap? =
ImageIO.read(uri.inputStream()).toComposeImageBitmap()
// LALAL implement to support animated drawable
actual fun getDrawableFromUri(uri: URI, withAlertOnException: Boolean): Any? = null
actual suspend fun saveTempImageUncompressed(image: ImageBitmap, asPng: Boolean): File? {
val file = simplexWindowState.saveDialog.awaitResult()
return if (file != null) {
try {
val ext = if (asPng) "png" else "jpg"
val newFile = File(file.absolutePath + File.separator + generateNewFileName("IMG", ext))
// LALAL FILE IS EMPTY
ImageIO.write(image.toAwtImage(), ext.uppercase(), newFile.outputStream())
newFile
} catch (e: Exception) {
Log.e(TAG, "Util.kt saveTempImageUncompressed error: ${e.message}")
null
}
} else null
}
actual fun getBitmapFromVideo(uri: URI, timestamp: Long?, random: Boolean): VideoPlayerInterface.PreviewAndDuration {
// LALAL
return VideoPlayerInterface.PreviewAndDuration(preview = null, timestamp = 0L, duration = 0L)
}
@OptIn(ExperimentalEncodingApi::class)
actual fun ByteArray.toBase64StringForPassphrase(): String = Base64.encode(this)
@OptIn(ExperimentalEncodingApi::class)
actual fun String.toByteArrayFromBase64ForPassphrase(): ByteArray = Base64.decode(this)
@@ -0,0 +1,18 @@
package chat.simplex.common.views.newchat
import androidx.compose.ui.graphics.*
actual fun ImageBitmap.replaceColor(from: Int, to: Int): ImageBitmap {
val image = toAwtImage()
val pixels = IntArray(width * height)
image.getRGB(0, 0, width, height, pixels, 0, image.width)
var i = 0
while (i < pixels.size) {
if (pixels[i] == from) {
pixels[i] = to
}
i++
}
image.setRGB(0, 0, width, height, pixels, 0, image.width)
return image.toComposeImageBitmap()
}
@@ -0,0 +1,8 @@
package chat.simplex.common.views.newchat
import androidx.compose.runtime.*
@Composable
actual fun QRCodeScanner(onBarcode: (String) -> Unit) {
//LALAL
}
@@ -0,0 +1,12 @@
package chat.simplex.common.views.newchat
import androidx.compose.runtime.Composable
import chat.simplex.common.model.ChatModel
@Composable
actual fun ScanToConnectView(chatModel: ChatModel, close: () -> Unit) {
ConnectContactLayout(
chatModelIncognito = chatModel.incognito.value,
close = close
)
}
@@ -0,0 +1,6 @@
package chat.simplex.common.views.onboarding
import androidx.compose.runtime.Composable
@Composable
actual fun SetNotificationsModeAdditions() {}
@@ -0,0 +1,70 @@
package chat.simplex.common.views.usersettings
import SectionBottomSpacer
import SectionDividerSpaced
import SectionView
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import chat.simplex.common.model.ChatModel
import chat.simplex.common.model.SharedPreference
import chat.simplex.common.platform.defaultLocale
import chat.simplex.common.ui.theme.ThemeColor
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.usersettings.AppearanceScope.ColorEditor
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.delay
import java.util.Locale
@Composable
actual fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)) {
AppearanceScope.AppearanceLayout(
m.controller.appPrefs.appLanguage,
m.controller.appPrefs.systemDarkTheme,
showSettingsModal = showSettingsModal,
editColor = { name, initialColor ->
ModalManager.shared.showModalCloseable { close ->
ColorEditor(name, initialColor, close)
}
},
)
}
@Composable
fun AppearanceScope.AppearanceLayout(
languagePref: SharedPreference<String?>,
systemDarkTheme: SharedPreference<String?>,
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
editColor: (ThemeColor, Color) -> Unit,
) {
Column(
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
) {
AppBarTitle(stringResource(MR.strings.appearance_settings))
SectionView(stringResource(MR.strings.settings_section_title_language), padding = PaddingValues()) {
val state = rememberSaveable { mutableStateOf(languagePref.get() ?: "system") }
LangSelector(state) {
state.value = it
withApi {
delay(200)
if (it == "system") {
languagePref.set(null)
Locale.setDefault(defaultLocale)
} else {
languagePref.set(it)
Locale.setDefault(Locale.forLanguageTag(it))
}
}
}
}
SectionDividerSpaced(maxTopPadding = true)
ThemesSection(systemDarkTheme, showSettingsModal, editColor)
SectionBottomSpacer()
}
}
@@ -0,0 +1,17 @@
package chat.simplex.common.views.usersettings
import SectionView
import androidx.compose.runtime.Composable
import chat.simplex.common.model.ChatModel
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.stringResource
@Composable
actual fun PrivacyDeviceSection(
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
setPerformLA: (Boolean) -> Unit,
) {
SectionView(stringResource(MR.strings.settings_section_title_device)) {
ChatLockItem(showSettingsModal, setPerformLA)
}
}
@@ -0,0 +1,9 @@
package chat.simplex.common.views.usersettings
import androidx.compose.runtime.Composable
import chat.simplex.common.model.ServerCfg
@Composable
actual fun ScanProtocolServer(onNext: (ServerCfg) -> Unit) {
ScanProtocolServerLayout(onNext)
}
@@ -0,0 +1,21 @@
package chat.simplex.common.views.usersettings
import SectionView
import androidx.compose.runtime.Composable
import chat.simplex.common.model.ChatModel
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
@Composable
actual fun SettingsSectionApp(
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
showVersion: () -> Unit,
withAuth: (title: String, desc: String, block: () -> Unit) -> Unit
) {
SectionView(stringResource(MR.strings.settings_section_title_app)) {
SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(it, showCustomModal, withAuth) }, extraPadding = true)
AppVersionItem(showVersion)
}
}