diff --git a/appcompose/src/main/AndroidManifest.xml b/appcompose/src/main/AndroidManifest.xml
index 379d77bf..f19b909f 100644
--- a/appcompose/src/main/AndroidManifest.xml
+++ b/appcompose/src/main/AndroidManifest.xml
@@ -9,10 +9,19 @@
android:supportsRtl="true"
android:theme="@style/Theme.Readrops">
+
+
+
+
+ android:exported="true">
@@ -23,5 +32,4 @@
-
\ No newline at end of file
diff --git a/appcompose/src/main/java/com/readrops/app/compose/item/ItemImageDialog.kt b/appcompose/src/main/java/com/readrops/app/compose/item/ItemImageDialog.kt
new file mode 100644
index 00000000..463acf65
--- /dev/null
+++ b/appcompose/src/main/java/com/readrops/app/compose/item/ItemImageDialog.kt
@@ -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) }
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/appcompose/src/main/java/com/readrops/app/compose/item/ItemScreen.kt b/appcompose/src/main/java/com/readrops/app/compose/item/ItemScreen.kt
index 5b357a5d..3df954e1 100644
--- a/appcompose/src/main/java/com/readrops/app/compose/item/ItemScreen.kt
+++ b/appcompose/src/main/java/com/readrops/app/compose/item/ItemScreen.kt
@@ -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)
diff --git a/appcompose/src/main/java/com/readrops/app/compose/item/ItemScreenModel.kt b/appcompose/src/main/java/com/readrops/app/compose/item/ItemScreenModel.kt
index f9c3a6f4..43613b67 100644
--- a/appcompose/src/main/java/com/readrops/app/compose/item/ItemScreenModel.kt
+++ b/appcompose/src/main/java/com/readrops/app/compose/item/ItemScreenModel.kt
@@ -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
)
\ No newline at end of file
diff --git a/appcompose/src/main/java/com/readrops/app/compose/item/view/ItemNestedScrollView.kt b/appcompose/src/main/java/com/readrops/app/compose/item/view/ItemNestedScrollView.kt
index 0a4453db..c99a809e 100644
--- a/appcompose/src/main/java/com/readrops/app/compose/item/view/ItemNestedScrollView.kt
+++ b/appcompose/src/main/java/com/readrops/app/compose/item/view/ItemNestedScrollView.kt
@@ -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)
diff --git a/appcompose/src/main/java/com/readrops/app/compose/item/view/ItemWebView.kt b/appcompose/src/main/java/com/readrops/app/compose/item/view/ItemWebView.kt
index 48dcb408..08e67c30 100644
--- a/appcompose/src/main/java/com/readrops/app/compose/item/view/ItemWebView.kt
+++ b/appcompose/src/main/java/com/readrops/app/compose/item/view/ItemWebView.kt
@@ -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(
""
}
}
-
}
\ No newline at end of file
diff --git a/appcompose/src/main/res/drawable/ic_download.xml b/appcompose/src/main/res/drawable/ic_download.xml
new file mode 100644
index 00000000..818e0a01
--- /dev/null
+++ b/appcompose/src/main/res/drawable/ic_download.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/appcompose/src/main/res/drawable/ic_image.xml b/appcompose/src/main/res/drawable/ic_image.xml
new file mode 100644
index 00000000..fd890cef
--- /dev/null
+++ b/appcompose/src/main/res/drawable/ic_image.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/appcompose/src/main/res/xml/file_paths.xml b/appcompose/src/main/res/xml/file_paths.xml
new file mode 100644
index 00000000..0d906caf
--- /dev/null
+++ b/appcompose/src/main/res/xml/file_paths.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file