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:supportsRtl="true"
|
||||||
android:theme="@style/Theme.Readrops">
|
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
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true">
|
||||||
android:label="Articles">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
@ -23,5 +32,4 @@
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</manifest>
|
</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.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SnackbarHost
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
@ -74,6 +77,7 @@ class ItemScreen(
|
||||||
val backgroundColor = MaterialTheme.colorScheme.background
|
val backgroundColor = MaterialTheme.colorScheme.background
|
||||||
val onBackgroundColor = MaterialTheme.colorScheme.onBackground
|
val onBackgroundColor = MaterialTheme.colorScheme.onBackground
|
||||||
|
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
var isScrollable by remember { mutableStateOf(true) }
|
var isScrollable by remember { mutableStateOf(true) }
|
||||||
|
|
||||||
// https://developer.android.com/develop/ui/compose/touch-input/pointer-input/scroll#parent-compose-child-view
|
// 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) {
|
if (state.itemWithFeed != null) {
|
||||||
val itemWithFeed = state.itemWithFeed!!
|
val itemWithFeed = state.itemWithFeed!!
|
||||||
val item = itemWithFeed.item
|
val item = itemWithFeed.item
|
||||||
|
@ -111,6 +137,7 @@ class ItemScreen(
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.nestedScroll(nestedScrollConnection),
|
.nestedScroll(nestedScrollConnection),
|
||||||
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
ItemScreenBottomBar(
|
ItemScreenBottomBar(
|
||||||
item = item,
|
item = item,
|
||||||
|
@ -148,12 +175,11 @@ class ItemScreen(
|
||||||
onGlobalLayoutListener = { viewHeight, contentHeight ->
|
onGlobalLayoutListener = { viewHeight, contentHeight ->
|
||||||
isScrollable = viewHeight - contentHeight < 0
|
isScrollable = viewHeight - contentHeight < 0
|
||||||
},
|
},
|
||||||
onUrlClick = { url -> openUrl(url) }
|
onUrlClick = { url -> openUrl(url) },
|
||||||
|
onImageLongPress = { url -> screenModel.openImageDialog(url) }
|
||||||
) {
|
) {
|
||||||
if (item.imageLink != null) {
|
if (item.imageLink != null) {
|
||||||
BackgroundTitle(
|
BackgroundTitle(itemWithFeed = itemWithFeed)
|
||||||
itemWithFeed = itemWithFeed
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
val tintColor = if (itemWithFeed.bgColor != 0) {
|
val tintColor = if (itemWithFeed.bgColor != 0) {
|
||||||
Color(itemWithFeed.bgColor)
|
Color(itemWithFeed.bgColor)
|
||||||
|
|
|
@ -2,9 +2,16 @@ package com.readrops.app.compose.item
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
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.compose.runtime.Stable
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
import cafe.adriel.voyager.core.model.screenModelScope
|
import cafe.adriel.voyager.core.model.screenModelScope
|
||||||
|
import coil.imageLoader
|
||||||
|
import coil.request.ImageRequest
|
||||||
import com.readrops.app.compose.repositories.BaseRepository
|
import com.readrops.app.compose.repositories.BaseRepository
|
||||||
import com.readrops.db.Database
|
import com.readrops.db.Database
|
||||||
import com.readrops.db.entities.Item
|
import com.readrops.db.entities.Item
|
||||||
|
@ -15,11 +22,11 @@ import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jsoup.Jsoup
|
|
||||||
import org.jsoup.parser.Parser
|
|
||||||
import org.koin.core.component.KoinComponent
|
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.FileOutputStream
|
||||||
|
|
||||||
class ItemScreenModel(
|
class ItemScreenModel(
|
||||||
private val database: Database,
|
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) {
|
fun shareItem(item: Item, context: Context) {
|
||||||
Intent().apply {
|
Intent().apply {
|
||||||
action = Intent.ACTION_SEND
|
action = Intent.ACTION_SEND
|
||||||
|
@ -89,9 +83,73 @@ class ItemScreenModel(
|
||||||
repository.setItemStarState(item)
|
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
|
@Stable
|
||||||
data class ItemState(
|
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,
|
context: Context,
|
||||||
onGlobalLayoutListener: (viewHeight: Int, contentHeight: Int) -> Unit,
|
onGlobalLayoutListener: (viewHeight: Int, contentHeight: Int) -> Unit,
|
||||||
onUrlClick: (String) -> Unit,
|
onUrlClick: (String) -> Unit,
|
||||||
|
onImageLongPress: (String) -> Unit,
|
||||||
composeViewContent: @Composable () -> Unit
|
composeViewContent: @Composable () -> Unit
|
||||||
) : NestedScrollView(context) {
|
) : NestedScrollView(context) {
|
||||||
|
|
||||||
|
@ -38,7 +39,8 @@ class ItemNestedScrollView(
|
||||||
|
|
||||||
val webView = ItemWebView(
|
val webView = ItemWebView(
|
||||||
context = context,
|
context = context,
|
||||||
onUrlClick = onUrlClick
|
onUrlClick = onUrlClick,
|
||||||
|
onImageLongPress = onImageLongPress
|
||||||
).apply {
|
).apply {
|
||||||
id = 2
|
id = 2
|
||||||
ViewCompat.setNestedScrollingEnabled(this, true)
|
ViewCompat.setNestedScrollingEnabled(this, true)
|
||||||
|
|
|
@ -17,6 +17,7 @@ import org.jsoup.parser.Parser
|
||||||
class ItemWebView(
|
class ItemWebView(
|
||||||
context: Context,
|
context: Context,
|
||||||
onUrlClick: (String) -> Unit,
|
onUrlClick: (String) -> Unit,
|
||||||
|
onImageLongPress: (String) -> Unit,
|
||||||
attrs: AttributeSet? = null,
|
attrs: AttributeSet? = null,
|
||||||
) : WebView(context, attrs) {
|
) : WebView(context, attrs) {
|
||||||
|
|
||||||
|
@ -33,6 +34,15 @@ class ItemWebView(
|
||||||
return true
|
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(
|
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