forked from melod1n/fast-messenger
Enhance PhotoViewer with share and open-in actions, improve reply UI
This commit introduces "Share" and "Open in..." actions to the `PhotoViewScreen`, allowing users to share images via other apps or open them in external viewers.
**Key changes:**
- **PhotoViewer:**
- Added "Share" and "Open in..." options to the `PhotoViewScreen` dropdown menu.
- `PhotoViewViewModel`:
- Implemented `onShareClicked()` and `onOpenInClicked()` to handle these new actions.
- Added `shareRequest` StateFlow to manage image sharing intents.
- Introduced `downloadAndStoreImageToCache()` to download and cache images for sharing.
- `onImageShared()` resets `shareRequest` after sharing.
- Updated `TopBar` to include the new menu items.
- Added string resources for "Open in…" and "Share".
- **Reply UI:**
- `Reply.kt`: Title and summary text now use `TextOverflow.Ellipsis` to prevent long text from breaking the layout.
- **API Model:**
- `MessagesResponse.kt`: Added `MessagesMarkAsImportantResponse` data class to handle the response for marking messages as important.
- **Data Layer:**
- `MessagesRepositoryImpl`: Updated `markAsImportant` to correctly map the API response using `MessagesMarkAsImportantResponse`.
- **Minor:**
- `README.md`: Updated feature checklist for external viewer.
- `ApplicationModule.kt`: Added experimental Coil API opt-in.
This commit is contained in:
+5
-3
@@ -21,6 +21,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -50,7 +51,6 @@ fun Reply(
|
||||
bottom = bottomPadding
|
||||
)
|
||||
) {
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
@@ -74,14 +74,16 @@ fun Reply(
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
maxLines = 1
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
summary?.let {
|
||||
Text(
|
||||
text = summary,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
maxLines = 1
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+77
-12
@@ -3,10 +3,13 @@ package dev.meloda.fast.photoviewer
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.graphics.drawable.toBitmapOrNull
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
@@ -26,13 +29,21 @@ import java.io.FileOutputStream
|
||||
import java.net.URLDecoder
|
||||
import java.util.UUID
|
||||
|
||||
import dev.meloda.fast.ui.R as UiR
|
||||
|
||||
interface PhotoViewViewModel {
|
||||
val screenState: StateFlow<PhotoViewScreenState>
|
||||
|
||||
val shareRequest: StateFlow<Uri?>
|
||||
|
||||
fun onPageChanged(newPage: Int)
|
||||
|
||||
fun onShareClicked()
|
||||
fun onOpenInClicked()
|
||||
fun onCopyLinkClicked()
|
||||
fun onCopyClicked()
|
||||
|
||||
fun onImageShared()
|
||||
}
|
||||
|
||||
class PhotoViewViewModelImpl(
|
||||
@@ -42,6 +53,8 @@ class PhotoViewViewModelImpl(
|
||||
|
||||
override val screenState = MutableStateFlow(PhotoViewScreenState.EMPTY)
|
||||
|
||||
override val shareRequest = MutableStateFlow<Uri?>(null)
|
||||
|
||||
init {
|
||||
val arguments = PhotoView.from(savedStateHandle).arguments
|
||||
|
||||
@@ -59,6 +72,47 @@ class PhotoViewViewModelImpl(
|
||||
screenState.setValue { old -> old.copy(selectedPage = newPage) }
|
||||
}
|
||||
|
||||
override fun onShareClicked() {
|
||||
val url = screenState.value.images
|
||||
.getOrNull(screenState.value.selectedPage)
|
||||
?.extractUrl() ?: return
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val imageFile = downloadAndStoreImageToCache(url) ?: return@launch
|
||||
|
||||
val uri = FileProvider.getUriForFile(
|
||||
applicationContext,
|
||||
"${applicationContext.packageName}.provider",
|
||||
imageFile
|
||||
)
|
||||
|
||||
shareRequest.setValue { uri }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOpenInClicked() {
|
||||
val url = screenState.value.images
|
||||
.getOrNull(screenState.value.selectedPage)
|
||||
?.extractUrl() ?: return
|
||||
|
||||
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
|
||||
try {
|
||||
applicationContext.startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
UiR.string.error_occurred,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCopyLinkClicked() {
|
||||
val url = screenState.value.images
|
||||
.getOrNull(screenState.value.selectedPage)
|
||||
@@ -85,18 +139,7 @@ class PhotoViewViewModelImpl(
|
||||
?.extractUrl() ?: return
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val drawable = applicationContext.imageLoader.execute(
|
||||
ImageRequest.Builder(applicationContext)
|
||||
.data(url)
|
||||
.build()
|
||||
).drawable ?: return@launch
|
||||
|
||||
val imagesDir = File(applicationContext.cacheDir, "images")
|
||||
if (!imagesDir.exists()) imagesDir.mkdirs()
|
||||
val imageFile = File(imagesDir, "shared_image_id${UUID.randomUUID()}.png")
|
||||
FileOutputStream(imageFile).use {
|
||||
drawable.toBitmapOrNull()?.compress(Bitmap.CompressFormat.PNG, 100, it)
|
||||
}
|
||||
val imageFile = downloadAndStoreImageToCache(url) ?: return@launch
|
||||
|
||||
val uri = FileProvider.getUriForFile(
|
||||
applicationContext,
|
||||
@@ -116,4 +159,26 @@ class PhotoViewViewModelImpl(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onImageShared() {
|
||||
shareRequest.setValue { null }
|
||||
}
|
||||
|
||||
private suspend fun downloadAndStoreImageToCache(url: String): File? =
|
||||
withContext(Dispatchers.IO) {
|
||||
val drawable = applicationContext.imageLoader.execute(
|
||||
ImageRequest.Builder(applicationContext)
|
||||
.data(url)
|
||||
.build()
|
||||
).drawable ?: return@withContext null
|
||||
|
||||
val imagesDir = File(applicationContext.cacheDir, "images")
|
||||
if (!imagesDir.exists()) imagesDir.mkdirs()
|
||||
val imageFile = File(imagesDir, "shared_image_id${UUID.randomUUID()}.png")
|
||||
FileOutputStream(imageFile).use {
|
||||
drawable.toBitmapOrNull()?.compress(Bitmap.CompressFormat.PNG, 100, it)
|
||||
}
|
||||
|
||||
imageFile
|
||||
}
|
||||
}
|
||||
|
||||
+68
-4
@@ -1,10 +1,13 @@
|
||||
package dev.meloda.fast.photoviewer.presentation
|
||||
|
||||
import android.content.Intent
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.draggable
|
||||
import androidx.compose.foundation.gestures.rememberDraggableState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
@@ -33,6 +36,7 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
@@ -41,6 +45,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.painter.ColorPainter
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
@@ -53,6 +58,8 @@ import dev.meloda.fast.photoviewer.PhotoViewViewModel
|
||||
import dev.meloda.fast.photoviewer.PhotoViewViewModelImpl
|
||||
import dev.meloda.fast.photoviewer.model.PhotoViewScreenState
|
||||
import dev.meloda.fast.ui.util.getImage
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import kotlin.math.abs
|
||||
import dev.meloda.fast.ui.R as UiR
|
||||
@@ -63,11 +70,46 @@ fun PhotoViewRoute(
|
||||
viewModel: PhotoViewViewModel = koinViewModel<PhotoViewViewModelImpl>()
|
||||
) {
|
||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||
val shareRequest by viewModel.shareRequest.collectAsStateWithLifecycle()
|
||||
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(shareRequest) {
|
||||
if (shareRequest != null) {
|
||||
viewModel.onImageShared()
|
||||
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
setType("image/png")
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
putExtra(Intent.EXTRA_STREAM, shareRequest)
|
||||
}
|
||||
|
||||
val chooserIntent = Intent.createChooser(intent, null)
|
||||
chooserIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
|
||||
try {
|
||||
context.startActivity(chooserIntent)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
|
||||
scope.launch(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
UiR.string.error_occurred,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PhotoViewScreen(
|
||||
screenState = screenState,
|
||||
onBack = onBack,
|
||||
onPageChanged = viewModel::onPageChanged,
|
||||
onShareClicked = viewModel::onShareClicked,
|
||||
onOpenInClicked = viewModel::onOpenInClicked,
|
||||
onCopyLinkClicked = viewModel::onCopyLinkClicked,
|
||||
onCopyClicked = viewModel::onCopyClicked
|
||||
)
|
||||
@@ -78,6 +120,8 @@ fun PhotoViewScreen(
|
||||
screenState: PhotoViewScreenState = PhotoViewScreenState.EMPTY,
|
||||
onBack: () -> Unit = {},
|
||||
onPageChanged: (index: Int) -> Unit = {},
|
||||
onShareClicked: () -> Unit = {},
|
||||
onOpenInClicked: () -> Unit = {},
|
||||
onCopyLinkClicked: () -> Unit = {},
|
||||
onCopyClicked: () -> Unit = {}
|
||||
) {
|
||||
@@ -108,6 +152,8 @@ fun PhotoViewScreen(
|
||||
topBar = {
|
||||
TopBar(
|
||||
onBack = onBack,
|
||||
onShareClicked = onShareClicked,
|
||||
onOpenInClicked = onOpenInClicked,
|
||||
onCopyClicked = onCopyClicked,
|
||||
onCopyLinkClicked = onCopyLinkClicked,
|
||||
)
|
||||
@@ -116,7 +162,7 @@ fun PhotoViewScreen(
|
||||
alpha = calculatedAlpha
|
||||
)
|
||||
) { padding ->
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Pager(
|
||||
pagerState = pagerState,
|
||||
state = screenState,
|
||||
@@ -134,6 +180,8 @@ fun PhotoViewScreen(
|
||||
fun TopBar(
|
||||
modifier: Modifier = Modifier,
|
||||
onBack: () -> Unit,
|
||||
onShareClicked: () -> Unit,
|
||||
onOpenInClicked: () -> Unit,
|
||||
onCopyClicked: () -> Unit,
|
||||
onCopyLinkClicked: () -> Unit
|
||||
) {
|
||||
@@ -141,9 +189,7 @@ fun TopBar(
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
val hideDropDownMenu by rememberUpdatedState(
|
||||
{ dropdownMenuShown = false }
|
||||
)
|
||||
val hideDropDownMenu by rememberUpdatedState { dropdownMenuShown = false }
|
||||
|
||||
TopAppBar(
|
||||
modifier = modifier,
|
||||
@@ -172,6 +218,24 @@ fun TopBar(
|
||||
onDismissRequest = { dropdownMenuShown = false },
|
||||
offset = DpOffset(x = (10).dp, y = (-60).dp)
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
hideDropDownMenu()
|
||||
onShareClicked()
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(UiR.string.action_share))
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
hideDropDownMenu()
|
||||
onOpenInClicked()
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(UiR.string.action_open_in))
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
hideDropDownMenu()
|
||||
|
||||
Reference in New Issue
Block a user