Add options to download/share image on long press in ItemScreen

This commit is contained in:
Shinokuni 2024-04-14 14:40:32 +02:00
parent eeb054f068
commit afbf8129ca
9 changed files with 198 additions and 25 deletions

View File

@ -9,10 +9,19 @@
android:supportsRtl="true"
android:theme="@style/Theme.Readrops">
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<activity
android:name=".MainActivity"
android:exported="true"
android:label="Articles">
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@ -23,5 +32,4 @@
</application>
</manifest>

View File

@ -0,0 +1,54 @@
package com.readrops.app.compose.item
import androidx.compose.foundation.layout.Column
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.readrops.app.compose.R
import com.readrops.app.compose.util.components.BaseDialog
import com.readrops.app.compose.util.components.SelectableImageText
import com.readrops.app.compose.util.theme.spacing
enum class ItemImageChoice {
SHARE,
DOWNLOAD
}
@Composable
fun ItemImageDialog(
onChoice: (ItemImageChoice) -> Unit,
onDismiss: () -> Unit
) {
BaseDialog(
title = stringResource(id = R.string.image_options),
icon = painterResource(id = R.drawable.ic_image),
onDismiss = onDismiss
) {
Column {
SelectableImageText(
image = rememberVectorPainter(image = Icons.Default.Share),
text = stringResource(id = R.string.share_image),
style = MaterialTheme.typography.titleMedium,
spacing = MaterialTheme.spacing.shortSpacing,
padding = MaterialTheme.spacing.shortSpacing,
imageSize = 16.dp,
onClick = { onChoice(ItemImageChoice.SHARE) }
)
SelectableImageText(
image = painterResource(id = R.drawable.ic_download),
text = stringResource(id = R.string.download_image),
style = MaterialTheme.typography.titleMedium,
spacing = MaterialTheme.spacing.shortSpacing,
padding = MaterialTheme.spacing.shortSpacing,
imageSize = 16.dp,
onClick = { onChoice(ItemImageChoice.DOWNLOAD) }
)
}
}
}

View File

@ -15,9 +15,12 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
@ -74,6 +77,7 @@ class ItemScreen(
val backgroundColor = MaterialTheme.colorScheme.background
val onBackgroundColor = MaterialTheme.colorScheme.onBackground
val snackbarHostState = remember { SnackbarHostState() }
var isScrollable by remember { mutableStateOf(true) }
// https://developer.android.com/develop/ui/compose/touch-input/pointer-input/scroll#parent-compose-child-view
@ -93,6 +97,28 @@ class ItemScreen(
}
}
if (state.imageDialogUrl != null) {
ItemImageDialog(
onChoice = {
if (it == ItemImageChoice.SHARE) {
screenModel.shareImage(state.imageDialogUrl!!, context)
} else {
screenModel.downloadImage(state.imageDialogUrl!!, context)
}
screenModel.closeImageDialog()
},
onDismiss = { screenModel.closeImageDialog() }
)
}
LaunchedEffect(state.fileDownloadedEvent) {
if (state.fileDownloadedEvent) {
snackbarHostState.showSnackbar("Downloaded file!")
}
}
if (state.itemWithFeed != null) {
val itemWithFeed = state.itemWithFeed!!
val item = itemWithFeed.item
@ -111,6 +137,7 @@ class ItemScreen(
Scaffold(
modifier = Modifier
.nestedScroll(nestedScrollConnection),
snackbarHost = { SnackbarHost(snackbarHostState) },
bottomBar = {
ItemScreenBottomBar(
item = item,
@ -148,12 +175,11 @@ class ItemScreen(
onGlobalLayoutListener = { viewHeight, contentHeight ->
isScrollable = viewHeight - contentHeight < 0
},
onUrlClick = { url -> openUrl(url) }
onUrlClick = { url -> openUrl(url) },
onImageLongPress = { url -> screenModel.openImageDialog(url) }
) {
if (item.imageLink != null) {
BackgroundTitle(
itemWithFeed = itemWithFeed
)
BackgroundTitle(itemWithFeed = itemWithFeed)
} else {
val tintColor = if (itemWithFeed.bgColor != 0) {
Color(itemWithFeed.bgColor)

View File

@ -2,9 +2,16 @@ package com.readrops.app.compose.item
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.net.Uri
import android.os.Environment
import androidx.compose.runtime.Stable
import androidx.core.content.FileProvider
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import coil.imageLoader
import coil.request.ImageRequest
import com.readrops.app.compose.repositories.BaseRepository
import com.readrops.db.Database
import com.readrops.db.entities.Item
@ -15,11 +22,11 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.jsoup.Jsoup
import org.jsoup.parser.Parser
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koin.core.parameter.parametersOf
import java.io.File
import java.io.FileOutputStream
class ItemScreenModel(
private val database: Database,
@ -53,19 +60,6 @@ class ItemScreenModel(
}
}
fun formatText(): String {
val itemWithFeed = state.value.itemWithFeed!!
val document = if (itemWithFeed.websiteUrl != null) Jsoup.parse(
Parser.unescapeEntities(itemWithFeed.item.text, false), itemWithFeed.websiteUrl
) else Jsoup.parse(
Parser.unescapeEntities(itemWithFeed.item.text, false)
)
document.select("div,span").forEach { it.clearAttributes() }
return document.body().html()
}
fun shareItem(item: Item, context: Context) {
Intent().apply {
action = Intent.ACTION_SEND
@ -89,9 +83,73 @@ class ItemScreenModel(
repository.setItemStarState(item)
}
}
fun openImageDialog(url: String) = mutableState.update { it.copy(imageDialogUrl = url) }
fun closeImageDialog() = mutableState.update { it.copy(imageDialogUrl = null) }
fun downloadImage(url: String, context: Context) {
screenModelScope.launch(dispatcher) {
val bitmap = getImage(url, context)
val target = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
url.substringAfterLast('/')
)
FileOutputStream(target).apply {
bitmap.compress(Bitmap.CompressFormat.PNG, 90, this)
flush()
close()
}
mutableState.update { it.copy(fileDownloadedEvent = true) }
}
}
fun shareImage(url: String, context: Context) {
screenModelScope.launch(dispatcher) {
val bitmap = getImage(url, context)
val uri = saveImageInCache(bitmap, url, context)
Intent().apply {
action = Intent.ACTION_SEND
type = "image/*"
putExtra(Intent.EXTRA_STREAM, uri)
}.also {
context.startActivity(Intent.createChooser(it, null))
}
}
}
private suspend fun getImage(url: String, context: Context): Bitmap {
val downloader = context.imageLoader
return (downloader.execute(
ImageRequest.Builder(context)
.data(url)
.allowHardware(false)
.build()
).drawable as BitmapDrawable).bitmap
}
private fun saveImageInCache(bitmap: Bitmap, url: String, context: Context): Uri {
val imagesFolder = File(context.cacheDir.absolutePath, "images")
if (!imagesFolder.exists()) imagesFolder.mkdirs()
val image = File(imagesFolder, url.substringAfterLast('/'))
FileOutputStream(image).apply {
bitmap.compress(Bitmap.CompressFormat.PNG, 90, this)
flush()
close()
}
return FileProvider.getUriForFile(context, context.packageName, image)
}
}
@Stable
data class ItemState(
val itemWithFeed: ItemWithFeed? = null
val itemWithFeed: ItemWithFeed? = null,
val imageDialogUrl: String? = null,
val fileDownloadedEvent: Boolean = false
)

View File

@ -13,6 +13,7 @@ class ItemNestedScrollView(
context: Context,
onGlobalLayoutListener: (viewHeight: Int, contentHeight: Int) -> Unit,
onUrlClick: (String) -> Unit,
onImageLongPress: (String) -> Unit,
composeViewContent: @Composable () -> Unit
) : NestedScrollView(context) {
@ -38,7 +39,8 @@ class ItemNestedScrollView(
val webView = ItemWebView(
context = context,
onUrlClick = onUrlClick
onUrlClick = onUrlClick,
onImageLongPress = onImageLongPress
).apply {
id = 2
ViewCompat.setNestedScrollingEnabled(this, true)

View File

@ -17,6 +17,7 @@ import org.jsoup.parser.Parser
class ItemWebView(
context: Context,
onUrlClick: (String) -> Unit,
onImageLongPress: (String) -> Unit,
attrs: AttributeSet? = null,
) : WebView(context, attrs) {
@ -33,6 +34,15 @@ class ItemWebView(
return true
}
}
setOnLongClickListener {
val type = hitTestResult.type
if (type == HitTestResult.IMAGE_TYPE || type == HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
hitTestResult.extra?.let { onImageLongPress(it) }
}
false
}
}
fun loadText(
@ -72,5 +82,4 @@ class ItemWebView(
""
}
}
}

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M5,20h14v-2H5V20zM19,9h-4V3H9v6H5l7,7L19,9z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/>
</vector>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<paths>
<cache-path name="images" path="images/" />
</paths>
</resources>