android, desktop: better handling of URI's (#3450)
This commit is contained in:
parent
8ce9dd7ab6
commit
f9b5c673c5
@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import com.google.accompanist.insets.navigationBarsWithImePadding
|
||||
import java.io.File
|
||||
|
||||
actual fun Modifier.navigationBarsWithImePadding(): Modifier = navigationBarsWithImePadding()
|
||||
|
||||
@ -19,7 +20,7 @@ actual fun ProvideWindowInsets(
|
||||
@Composable
|
||||
actual fun Modifier.desktopOnExternalDrag(
|
||||
enabled: Boolean,
|
||||
onFiles: (List<String>) -> Unit,
|
||||
onFiles: (List<File>) -> Unit,
|
||||
onImage: (Painter) -> Unit,
|
||||
onText: (String) -> Unit
|
||||
): Modifier = this
|
||||
|
@ -71,7 +71,7 @@ actual class VideoPlayer actual constructor(
|
||||
private fun start(seek: Long? = null, onProgressUpdate: (position: Long?, state: TrackState) -> Unit): Boolean {
|
||||
val filepath = getAppFilePath(uri)
|
||||
if (filepath == null || !File(filepath).exists()) {
|
||||
Log.e(TAG, "No such file: $uri")
|
||||
Log.e(TAG, "No such file: $filepath")
|
||||
brokenVideo.value = true
|
||||
return false
|
||||
}
|
||||
|
@ -27,7 +27,6 @@ import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.json.*
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.net.URLDecoder
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import java.util.*
|
||||
@ -2363,7 +2362,7 @@ data class CryptoFile(
|
||||
companion object {
|
||||
fun plain(f: String): CryptoFile = CryptoFile(f, null)
|
||||
|
||||
fun desktopPlain(f: URI): CryptoFile = CryptoFile(URLDecoder.decode(f.rawPath, "UTF-8"), null)
|
||||
fun desktopPlain(f: URI): CryptoFile = CryptoFile(f.toFile().absolutePath, null)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,8 @@ import chat.simplex.common.views.helpers.generalGetString
|
||||
import chat.simplex.res.MR
|
||||
import java.io.*
|
||||
import java.net.URI
|
||||
import java.net.URLDecoder
|
||||
import java.net.URLEncoder
|
||||
|
||||
expect val dataDir: File
|
||||
expect val tmpDir: File
|
||||
@ -28,6 +30,10 @@ expect val remoteHostsDir: File
|
||||
|
||||
expect fun desktopOpenDatabaseDir()
|
||||
|
||||
fun createURIFromPath(absolutePath: String): URI = URI.create(URLEncoder.encode(absolutePath, "UTF-8"))
|
||||
|
||||
fun URI.toFile(): File = File(URLDecoder.decode(rawPath, "UTF-8").removePrefix("file:"))
|
||||
|
||||
fun copyFileToFile(from: File, to: URI, finally: () -> Unit) {
|
||||
try {
|
||||
to.outputStream().use { stream ->
|
||||
|
@ -3,6 +3,7 @@ package chat.simplex.common.platform
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import java.io.File
|
||||
|
||||
expect fun Modifier.navigationBarsWithImePadding(): Modifier
|
||||
|
||||
@ -16,7 +17,7 @@ expect fun ProvideWindowInsets(
|
||||
@Composable
|
||||
expect fun Modifier.desktopOnExternalDrag(
|
||||
enabled: Boolean = true,
|
||||
onFiles: (List<String>) -> Unit = {},
|
||||
onFiles: (List<File>) -> Unit = {},
|
||||
onImage: (Painter) -> Unit = {},
|
||||
onText: (String) -> Unit = {}
|
||||
): Modifier
|
||||
|
@ -43,16 +43,13 @@ object VideoPlayerHolder {
|
||||
): VideoPlayer =
|
||||
players.getOrPut(uri to gallery) { VideoPlayer(uri, gallery, defaultPreview, defaultDuration, soundEnabled) }
|
||||
|
||||
fun enableSound(enable: Boolean, fileName: String?, gallery: Boolean): Boolean =
|
||||
player(fileName, gallery)?.enableSound(enable) == true
|
||||
|
||||
private fun player(fileName: String?, gallery: Boolean): VideoPlayer? {
|
||||
fileName ?: return null
|
||||
return players.values.firstOrNull { player -> player.uri.path?.endsWith(fileName) == true && player.gallery == gallery }
|
||||
private fun player(uri: URI?, gallery: Boolean): VideoPlayer? {
|
||||
uri ?: return null
|
||||
return players.values.firstOrNull { player -> player.uri == uri && player.gallery == gallery }
|
||||
}
|
||||
|
||||
fun release(uri: URI, gallery: Boolean, remove: Boolean) =
|
||||
player(uri.path, gallery)?.release(remove).run { }
|
||||
player(uri, gallery)?.release(remove).run { }
|
||||
|
||||
fun stopAll() {
|
||||
players.values.forEach { it.stop() }
|
||||
|
@ -501,7 +501,7 @@ fun ChatLayout(
|
||||
.fillMaxWidth()
|
||||
.desktopOnExternalDrag(
|
||||
enabled = !attachmentDisabled.value && rememberUpdatedState(chat.userCanSend).value,
|
||||
onFiles = { paths -> composeState.onFilesAttached(paths.map { URI.create(it) }) },
|
||||
onFiles = { paths -> composeState.onFilesAttached(paths.map { it.toURI() }) },
|
||||
onImage = {
|
||||
// TODO: file is not saved anywhere?!
|
||||
val tmpFile = File.createTempFile("image", ".bmp", tmpDir)
|
||||
|
@ -104,5 +104,5 @@ private fun fileFilterDescription(input: String): String = when(input) {
|
||||
else -> ""
|
||||
}
|
||||
|
||||
actual fun URI.inputStream(): InputStream? = File(URI("file:" + toString().removePrefix("file:"))).inputStream()
|
||||
actual fun URI.outputStream(): OutputStream = File(URI("file:" + toString().removePrefix("file:"))).outputStream()
|
||||
actual fun URI.inputStream(): InputStream? = toFile().inputStream()
|
||||
actual fun URI.outputStream(): OutputStream = toFile().outputStream()
|
||||
|
@ -157,7 +157,7 @@ actual fun ImageBitmap.scale(width: Int, height: Int): ImageBitmap {
|
||||
|
||||
// LALAL
|
||||
actual fun isImage(uri: URI): Boolean {
|
||||
val path = uri.path.lowercase()
|
||||
val path = uri.toFile().path.lowercase()
|
||||
return path.endsWith(".gif") ||
|
||||
path.endsWith(".webp") ||
|
||||
path.endsWith(".png") ||
|
||||
@ -166,7 +166,7 @@ actual fun isImage(uri: URI): Boolean {
|
||||
}
|
||||
|
||||
actual fun isAnimImage(uri: URI, drawable: Any?): Boolean {
|
||||
val path = uri.path.lowercase()
|
||||
val path = uri.toFile().path.lowercase()
|
||||
return path.endsWith(".gif") || path.endsWith(".webp")
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,8 @@ import androidx.compose.foundation.contextMenuOpenDetector
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.*
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
|
||||
actual fun Modifier.navigationBarsWithImePadding(): Modifier = this
|
||||
|
||||
@ -19,13 +21,15 @@ actual fun ProvideWindowInsets(
|
||||
@Composable
|
||||
actual fun Modifier.desktopOnExternalDrag(
|
||||
enabled: Boolean,
|
||||
onFiles: (List<String>) -> Unit,
|
||||
onFiles: (List<File>) -> Unit,
|
||||
onImage: (Painter) -> Unit,
|
||||
onText: (String) -> Unit
|
||||
): Modifier =
|
||||
onExternalDrag(enabled) {
|
||||
when(val data = it.dragData) {
|
||||
is DragData.FilesList -> onFiles(data.readFiles())
|
||||
// data.readFiles() returns filePath in URI format (where spaces replaces with %20). But it's an error-prone idea to work later
|
||||
// with such format when everywhere we use absolutePath in File() format
|
||||
is DragData.FilesList -> onFiles(data.readFiles().map { URI.create(it).toFile() })
|
||||
is DragData.Image -> onImage(data.readImage())
|
||||
is DragData.Text -> onText(data.readText())
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ actual object AudioPlayer: AudioPlayerInterface {
|
||||
|
||||
// Returns real duration of the track
|
||||
private fun start(fileSource: CryptoFile, seek: Int? = null, onProgressUpdate: (position: Int?, state: TrackState) -> Unit): Int? {
|
||||
val absoluteFilePath = if (fileSource.isAbsolutePath) fileSource.filePath else getAppFilePath(fileSource.filePath)
|
||||
val absoluteFilePath = if (fileSource.isAbsolutePath) fileSource.filePath else getAppFilePath(fileSource.filePath)
|
||||
if (!File(absoluteFilePath).exists()) {
|
||||
Log.e(TAG, "No such file: ${fileSource.filePath}")
|
||||
return null
|
||||
@ -46,16 +46,16 @@ actual object AudioPlayer: AudioPlayerInterface {
|
||||
VideoPlayerHolder.stopAll()
|
||||
RecorderInterface.stopRecording?.invoke()
|
||||
val current = currentlyPlaying.value
|
||||
if (current == null || current.first != fileSource) {
|
||||
if (current == null || current.first != fileSource || !player.status().isPlayable) {
|
||||
stopListener()
|
||||
player.stop()
|
||||
runCatching {
|
||||
if (fileSource.cryptoArgs != null) {
|
||||
val tmpFile = fileSource.createTmpFileIfNeeded()
|
||||
decryptCryptoFile(absoluteFilePath, fileSource.cryptoArgs, tmpFile.absolutePath)
|
||||
player.media().prepare(tmpFile.toURI().toString().replaceFirst("file:", "file://"))
|
||||
player.media().prepare(tmpFile.absolutePath)
|
||||
} else {
|
||||
player.media().prepare(File(absoluteFilePath).toURI().toString().replaceFirst("file:", "file://"))
|
||||
player.media().prepare(absoluteFilePath)
|
||||
}
|
||||
}.onFailure {
|
||||
Log.e(TAG, it.stackTraceToString())
|
||||
@ -171,7 +171,7 @@ actual object AudioPlayer: AudioPlayerInterface {
|
||||
var res: Int? = null
|
||||
try {
|
||||
val helperPlayer = AudioPlayerComponent().mediaPlayer()
|
||||
helperPlayer.media().startPaused(File(unencryptedFilePath).toURI().toString().replaceFirst("file:", "file://"))
|
||||
helperPlayer.media().startPaused(unencryptedFilePath)
|
||||
res = helperPlayer.duration
|
||||
helperPlayer.stop()
|
||||
helperPlayer.release()
|
||||
|
@ -4,6 +4,7 @@ import androidx.compose.ui.platform.ClipboardManager
|
||||
import androidx.compose.ui.platform.UriHandler
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.views.helpers.generalGetString
|
||||
import chat.simplex.common.views.helpers.withApi
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
@ -25,14 +26,16 @@ actual fun shareFile(text: String, fileSource: CryptoFile) {
|
||||
withApi {
|
||||
FileChooserLauncher(false) { to: URI? ->
|
||||
if (to != null) {
|
||||
val absolutePath = if (fileSource.isAbsolutePath) fileSource.filePath else getAppFilePath(fileSource.filePath)
|
||||
if (fileSource.cryptoArgs != null) {
|
||||
try {
|
||||
decryptCryptoFile(getAppFilePath(fileSource.filePath), fileSource.cryptoArgs, to.path)
|
||||
decryptCryptoFile(absolutePath, fileSource.cryptoArgs, to.toFile().absolutePath)
|
||||
showToast(generalGetString(MR.strings.file_saved))
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to decrypt crypto file: " + e.stackTraceToString())
|
||||
}
|
||||
} else {
|
||||
copyFileToFile(File(fileSource.filePath), to) {}
|
||||
copyFileToFile(File(absolutePath), to) {}
|
||||
}
|
||||
}
|
||||
}.launch(fileSource.filePath)
|
||||
|
@ -52,7 +52,7 @@ actual class VideoPlayer actual constructor(
|
||||
private fun start(seek: Long? = null, onProgressUpdate: (position: Long?, state: TrackState) -> Unit): Boolean {
|
||||
val filepath = getAppFilePath(uri)
|
||||
if (filepath == null || !File(filepath).exists()) {
|
||||
Log.e(TAG, "No such file: $uri")
|
||||
Log.e(TAG, "No such file: $filepath")
|
||||
brokenVideo.value = true
|
||||
return false
|
||||
}
|
||||
@ -62,10 +62,9 @@ actual class VideoPlayer actual constructor(
|
||||
}
|
||||
AudioPlayer.stop()
|
||||
VideoPlayerHolder.stopAll()
|
||||
val playerFilePath = uri.toString().replaceFirst("file:", "file://")
|
||||
if (listener.value == null) {
|
||||
runCatching {
|
||||
player.media().prepare(playerFilePath)
|
||||
player.media().prepare(uri.toFile().absolutePath)
|
||||
if (seek != null) {
|
||||
player.seekTo(seek.toInt())
|
||||
}
|
||||
@ -217,12 +216,12 @@ actual class VideoPlayer actual constructor(
|
||||
suspend fun getBitmapFromVideo(defaultPreview: ImageBitmap?, uri: URI?, withAlertOnException: Boolean = true): VideoPlayerInterface.PreviewAndDuration = withContext(playerThread.asCoroutineDispatcher()) {
|
||||
val mediaComponent = getOrCreateHelperPlayer()
|
||||
val player = mediaComponent.mediaPlayer()
|
||||
if (uri == null || !File(uri.rawPath).exists()) {
|
||||
if (uri == null || !uri.toFile().exists()) {
|
||||
if (withAlertOnException) showVideoDecodingException()
|
||||
|
||||
return@withContext VideoPlayerInterface.PreviewAndDuration(preview = defaultPreview, timestamp = 0L, duration = 0L)
|
||||
}
|
||||
player.media().startPaused(uri.toString().replaceFirst("file:", "file://"))
|
||||
player.media().startPaused(uri.toFile().absolutePath)
|
||||
val start = System.currentTimeMillis()
|
||||
var snap: BufferedImage? = null
|
||||
while (snap == null && start + 5000 > System.currentTimeMillis()) {
|
||||
|
@ -3,7 +3,7 @@ package chat.simplex.common.platform
|
||||
import java.net.URI
|
||||
|
||||
fun isVideo(uri: URI): Boolean {
|
||||
val path = uri.path.lowercase()
|
||||
val path = uri.toFile().path.lowercase()
|
||||
return path.endsWith(".mov") ||
|
||||
path.endsWith(".avi") ||
|
||||
path.endsWith(".mp4") ||
|
||||
|
@ -8,14 +8,12 @@ import androidx.compose.ui.unit.Density
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.simplexWindowState
|
||||
import chat.simplex.res.MR
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import javax.imageio.ImageIO
|
||||
import kotlin.io.encoding.Base64
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
import kotlin.io.path.toPath
|
||||
|
||||
private val bStyle = SpanStyle(fontWeight = FontWeight.Bold)
|
||||
private val iStyle = SpanStyle(fontStyle = FontStyle.Italic)
|
||||
@ -90,9 +88,9 @@ actual fun escapedHtmlToAnnotatedString(text: String, density: Density): Annotat
|
||||
actual fun getAppFileUri(fileName: String): URI {
|
||||
val rh = chatModel.currentRemoteHost.value
|
||||
return if (rh == null) {
|
||||
URI(appFilesDir.toURI().toString() + "/" + fileName)
|
||||
createURIFromPath(appFilesDir.absolutePath + "/" + fileName)
|
||||
} else {
|
||||
URI(dataDir.absolutePath + "/remote_hosts/" + rh.storePath + "/simplex_v1_files/" + fileName)
|
||||
createURIFromPath(dataDir.absolutePath + "/remote_hosts/" + rh.storePath + "/simplex_v1_files/" + fileName)
|
||||
}
|
||||
}
|
||||
|
||||
@ -116,11 +114,11 @@ actual suspend fun getLoadedImage(file: CIFile?): Pair<ImageBitmap, ByteArray>?
|
||||
}
|
||||
}
|
||||
|
||||
actual fun getFileName(uri: URI): String? = uri.toPath().toFile().name
|
||||
actual fun getFileName(uri: URI): String? = uri.toFile().name
|
||||
|
||||
actual fun getAppFilePath(uri: URI): String? = uri.path
|
||||
actual fun getAppFilePath(uri: URI): String? = uri.toFile().absolutePath
|
||||
|
||||
actual fun getFileSize(uri: URI): Long? = uri.toPath().toFile().length()
|
||||
actual fun getFileSize(uri: URI): Long? = uri.toFile().length()
|
||||
|
||||
actual fun getBitmapFromUri(uri: URI, withAlertOnException: Boolean): ImageBitmap? =
|
||||
try {
|
||||
|
Loading…
Reference in New Issue
Block a user