mirror of https://github.com/readrops/Readrops.git
Add options to download/share image on long press in ItemScreen
This commit is contained in:
parent
eeb054f068
commit
afbf8129ca
|
@ -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>
|
|
@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
|||
""
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<paths>
|
||||
<cache-path name="images" path="images/" />
|
||||
</paths>
|
||||
</resources>
|
Loading…
Reference in New Issue