mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-23 10:43:05 +00:00
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:
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()
|
||||
+67
@@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
+14
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -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
|
||||
}
|
||||
}
|
||||
+42
@@ -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()
|
||||
+129
@@ -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()
|
||||
+8
@@ -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")
|
||||
}
|
||||
+15
@@ -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()
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package chat.simplex.common.platform
|
||||
|
||||
import chat.simplex.common.model.NotificationsMode
|
||||
|
||||
actual fun allowedToShowNotification(): Boolean = true
|
||||
+28
@@ -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")
|
||||
}
|
||||
}
|
||||
+107
@@ -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)
|
||||
)
|
||||
}
|
||||
+53
@@ -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*/ }
|
||||
}
|
||||
+58
@@ -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
|
||||
}
|
||||
}
|
||||
+31
@@ -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)
|
||||
}
|
||||
}
|
||||
+19
@@ -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?) {}
|
||||
+50
@@ -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")
|
||||
}
|
||||
}
|
||||
+14
@@ -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)
|
||||
)
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package chat.simplex.common.views.call
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable
|
||||
actual fun ActiveCallView() {
|
||||
// LALAL
|
||||
}
|
||||
+24
@@ -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
|
||||
}
|
||||
}
|
||||
+8
@@ -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)
|
||||
}
|
||||
+11
@@ -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 {}
|
||||
}
|
||||
+28
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
@@ -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") }*/
|
||||
}
|
||||
+23
@@ -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
|
||||
})
|
||||
}
|
||||
+28
@@ -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) {
|
||||
|
||||
}
|
||||
+119
@@ -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"
|
||||
+34
@@ -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()
|
||||
}
|
||||
}
|
||||
+57
@@ -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("") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
@@ -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 -> {}
|
||||
}
|
||||
}
|
||||
+72
@@ -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)
|
||||
+18
@@ -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()
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package chat.simplex.common.views.newchat
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
|
||||
@Composable
|
||||
actual fun QRCodeScanner(onBarcode: (String) -> Unit) {
|
||||
//LALAL
|
||||
}
|
||||
+12
@@ -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
|
||||
)
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package chat.simplex.common.views.onboarding
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable
|
||||
actual fun SetNotificationsModeAdditions() {}
|
||||
+70
@@ -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()
|
||||
}
|
||||
}
|
||||
+17
@@ -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)
|
||||
}
|
||||
}
|
||||
+9
@@ -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)
|
||||
}
|
||||
+21
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user