Improve image share/download in ItemScreen

* Downloaded image now appears in media gallery (fixes #226)

* Fix some shared images which could be corrupted

* Shared image preview now appears in share dialog

* Fix crash when no bitmap is fetched due to various errors
This commit is contained in:
Shinokuni 2024-11-23 21:15:40 +01:00
parent 640744bf6d
commit b803058210
4 changed files with 49 additions and 16 deletions

View File

@ -135,6 +135,12 @@ class ItemScreen(
} }
} }
LaunchedEffect(state.error) {
if (state.error != null) {
snackbarHostState.showSnackbar(state.error!!)
}
}
if (state.itemWithFeed != null) { if (state.itemWithFeed != null) {
val itemWithFeed = state.itemWithFeed!! val itemWithFeed = state.itemWithFeed!!
val item = itemWithFeed.item val item = itemWithFeed.item

View File

@ -1,9 +1,11 @@
package com.readrops.app.item package com.readrops.app.item
import android.content.ClipData
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.graphics.Bitmap import android.graphics.Bitmap
import android.media.MediaScannerConnection
import android.net.Uri import android.net.Uri
import android.os.Environment import android.os.Environment
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
@ -14,6 +16,7 @@ import coil3.imageLoader
import coil3.request.ImageRequest import coil3.request.ImageRequest
import coil3.request.allowHardware import coil3.request.allowHardware
import coil3.toBitmap import coil3.toBitmap
import com.readrops.app.R
import com.readrops.app.repositories.BaseRepository import com.readrops.app.repositories.BaseRepository
import com.readrops.app.util.Preferences import com.readrops.app.util.Preferences
import com.readrops.db.Database import com.readrops.db.Database
@ -33,7 +36,7 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.get import org.koin.core.component.get
import org.koin.core.parameter.parametersOf import org.koin.core.parameter.parametersOf
import java.io.File import java.io.File
import java.io.FileOutputStream import java.net.URI
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class ItemScreenModel( class ItemScreenModel(
@ -107,6 +110,7 @@ class ItemScreenModel(
action = Intent.ACTION_SEND action = Intent.ACTION_SEND
type = "text/plain" type = "text/plain"
putExtra(Intent.EXTRA_TEXT, item.link) putExtra(Intent.EXTRA_TEXT, item.link)
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
}.also { }.also {
context.startActivity(Intent.createChooser(it, null)) context.startActivity(Intent.createChooser(it, null))
} }
@ -132,16 +136,23 @@ class ItemScreenModel(
screenModelScope.launch(dispatcher) { screenModelScope.launch(dispatcher) {
val bitmap = getImage(url, context) val bitmap = getImage(url, context)
if (bitmap == null) {
mutableState.update { it.copy(error = context.getString(R.string.error_image_download)) }
return@launch
}
val target = File( val target = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
url.substringAfterLast('/') url.substringAfterLast('/')
) ).apply {
FileOutputStream(target).apply { outputStream().apply {
bitmap.compress(Bitmap.CompressFormat.PNG, 90, this) bitmap.compress(Bitmap.CompressFormat.PNG, 90, this)
flush() flush()
close() close()
} }
}
MediaScannerConnection.scanFile(context, arrayOf(target.absolutePath), null, null)
mutableState.update { it.copy(fileDownloadedEvent = true) } mutableState.update { it.copy(fileDownloadedEvent = true) }
} }
} }
@ -149,39 +160,52 @@ class ItemScreenModel(
fun shareImage(url: String, context: Context) { fun shareImage(url: String, context: Context) {
screenModelScope.launch(dispatcher) { screenModelScope.launch(dispatcher) {
val bitmap = getImage(url, context) val bitmap = getImage(url, context)
if (bitmap == null) {
mutableState.update { it.copy(error = context.getString(R.string.error_image_download)) }
return@launch
}
val uri = saveImageInCache(bitmap, url, context) val uri = saveImageInCache(bitmap, url, context)
Intent().apply { Intent().apply {
action = Intent.ACTION_SEND action = Intent.ACTION_SEND
type = "image/*"
clipData = ClipData.newRawUri(null, uri)
putExtra(Intent.EXTRA_STREAM, uri) putExtra(Intent.EXTRA_STREAM, uri)
type = "image/*"
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
}.also { }.also {
context.startActivity(Intent.createChooser(it, null)) context.startActivity(Intent.createChooser(it, null))
} }
} }
} }
private suspend fun getImage(url: String, context: Context): Bitmap { private suspend fun getImage(url: String, context: Context): Bitmap? {
val downloader = context.imageLoader val downloader = context.imageLoader
return (downloader.execute( val image = downloader.execute(
ImageRequest.Builder(context) ImageRequest.Builder(context)
.data(url) .data(url)
.allowHardware(false) .allowHardware(false)
.build() .build()
).image!!.toBitmap()) ).image
return image?.toBitmap()
} }
private fun saveImageInCache(bitmap: Bitmap, url: String, context: Context): Uri { private fun saveImageInCache(bitmap: Bitmap, url: String, context: Context): Uri {
val imagesFolder = File(context.cacheDir.absolutePath, "images") val imagesFolder = File(context.cacheDir.absolutePath, "images")
if (!imagesFolder.exists()) imagesFolder.mkdirs() if (!imagesFolder.exists()) imagesFolder.mkdirs()
val image = File(imagesFolder, url.substringAfterLast('/')) val name = URI.create(url).path.substringAfterLast('/')
FileOutputStream(image).apply { val image = File(imagesFolder, name).apply {
outputStream().apply {
bitmap.compress(Bitmap.CompressFormat.PNG, 90, this) bitmap.compress(Bitmap.CompressFormat.PNG, 90, this)
flush() flush()
close() close()
} }
}
return FileProvider.getUriForFile(context, context.packageName, image) return FileProvider.getUriForFile(context, context.packageName, image)
} }
@ -194,5 +218,6 @@ data class ItemState(
val imageDialogUrl: String? = null, val imageDialogUrl: String? = null,
val fileDownloadedEvent: Boolean = false, val fileDownloadedEvent: Boolean = false,
val openInExternalBrowser: Boolean = false, val openInExternalBrowser: Boolean = false,
val theme: String? = "" val theme: String? = "",
val error: String? = null
) )

View File

@ -185,4 +185,5 @@
<string name="feed_already_exists">Le flux existe déjà</string> <string name="feed_already_exists">Le flux existe déjà</string>
<string name="invalid_folder">Dossier invalide</string> <string name="invalid_folder">Dossier invalide</string>
<string name="invalid_feed">Flux invalide</string> <string name="invalid_feed">Flux invalide</string>
<string name="error_image_download">Une erreur s\'est produite lors du téléchargement de l\'image</string>
</resources> </resources>

View File

@ -194,4 +194,5 @@
<string name="feed_already_exists">Feed already exists</string> <string name="feed_already_exists">Feed already exists</string>
<string name="invalid_folder">Invalid folder</string> <string name="invalid_folder">Invalid folder</string>
<string name="invalid_feed">Invalid feed</string> <string name="invalid_feed">Invalid feed</string>
<string name="error_image_download">An error occurred while downloading the image</string>
</resources> </resources>