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