forked from melod1n/fast-messenger
Refactor: Introduce FullScreenContainedLoader and use rememberUpdatedState
This commit introduces `FullScreenContainedLoader` and replaces usages of `FullScreenLoader` where appropriate. It also updates several composables to use `rememberUpdatedState` for lambda parameters to ensure the latest versions are used. Additionally, the following changes are included: - Add a setting to show/hide the attachment button in the chat input bar. - Implement navigation to `PhotoViewScreen` when a photo attachment is clicked in a message. - Add "Copy link" and "Copy image" actions to `PhotoViewScreen`. - Remove unused settings and their corresponding logic from `SettingsViewModel` and `UserSettings`.
This commit is contained in:
+88
-3
@@ -1,21 +1,43 @@
|
||||
package dev.meloda.fast.photoviewer
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.graphics.drawable.toBitmapOrNull
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import coil.imageLoader
|
||||
import coil.request.ImageRequest
|
||||
import dev.meloda.fast.common.extensions.setValue
|
||||
import dev.meloda.fast.common.model.UiImage
|
||||
import dev.meloda.fast.photoviewer.model.PhotoViewScreenState
|
||||
import dev.meloda.fast.photoviewer.navigation.PhotoView
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.net.URLDecoder
|
||||
import java.util.UUID
|
||||
|
||||
interface PhotoViewViewModel {
|
||||
val screenState: StateFlow<PhotoViewScreenState>
|
||||
|
||||
fun onPageChanged(newPage: Int)
|
||||
|
||||
fun onCopyLinkClicked()
|
||||
fun onCopyClicked()
|
||||
}
|
||||
|
||||
class PhotoViewViewModelImpl(
|
||||
savedStateHandle: SavedStateHandle
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val applicationContext: Context
|
||||
) : PhotoViewViewModel, ViewModel() {
|
||||
|
||||
override val screenState = MutableStateFlow(PhotoViewScreenState.EMPTY)
|
||||
@@ -25,10 +47,73 @@ class PhotoViewViewModelImpl(
|
||||
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
images = arguments.images
|
||||
images = arguments.imageUrls
|
||||
.map { URLDecoder.decode(it, "utf-8") }
|
||||
.map(UiImage::Url)
|
||||
.map(UiImage::Url),
|
||||
selectedPage = arguments.selectedIndex?.takeIf { it != -1 } ?: 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPageChanged(newPage: Int) {
|
||||
screenState.setValue { old -> old.copy(selectedPage = newPage) }
|
||||
}
|
||||
|
||||
override fun onCopyLinkClicked() {
|
||||
val url = screenState.value.images
|
||||
.getOrNull(screenState.value.selectedPage)
|
||||
?.extractUrl() ?: return
|
||||
|
||||
val clipboardManager =
|
||||
applicationContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
|
||||
clipboardManager.setPrimaryClip(ClipData.newPlainText("URL", url))
|
||||
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
"URL copied to clipboard",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
override fun onCopyClicked() {
|
||||
val clipboardManager =
|
||||
applicationContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
|
||||
val url = screenState.value.images
|
||||
.getOrNull(screenState.value.selectedPage)
|
||||
?.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 uri = FileProvider.getUriForFile(
|
||||
applicationContext,
|
||||
"${applicationContext.packageName}.provider",
|
||||
imageFile
|
||||
)
|
||||
|
||||
val clip = ClipData.newUri(applicationContext.contentResolver, "Image", uri)
|
||||
clipboardManager.setPrimaryClip(clip)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
"Image copied to clipboard",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -7,5 +7,6 @@ import kotlinx.serialization.Serializable
|
||||
@Parcelize
|
||||
@Serializable
|
||||
data class PhotoViewArguments(
|
||||
val images: List<String>
|
||||
val imageUrls: List<String>,
|
||||
val selectedIndex: Int?
|
||||
) : Parcelable
|
||||
|
||||
+4
-2
@@ -5,12 +5,14 @@ import dev.meloda.fast.common.model.UiImage
|
||||
|
||||
@Immutable
|
||||
data class PhotoViewScreenState(
|
||||
val images: List<UiImage>
|
||||
val images: List<UiImage>,
|
||||
val selectedPage: Int
|
||||
) {
|
||||
|
||||
companion object {
|
||||
val EMPTY: PhotoViewScreenState = PhotoViewScreenState(
|
||||
images = emptyList()
|
||||
images = emptyList(),
|
||||
selectedPage = 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+6
-2
@@ -30,11 +30,15 @@ fun NavGraphBuilder.photoViewScreen(
|
||||
}
|
||||
}
|
||||
|
||||
fun NavController.navigateToPhotoView(images: List<String>) {
|
||||
fun NavController.navigateToPhotoView(
|
||||
images: List<String>,
|
||||
selectedIndex: Int? = null
|
||||
) {
|
||||
this.navigate(
|
||||
PhotoView(
|
||||
arguments = PhotoViewArguments(
|
||||
images.map { URLEncoder.encode(it, "utf-8") }
|
||||
imageUrls = images.map { URLEncoder.encode(it, "utf-8") },
|
||||
selectedIndex = selectedIndex
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
+77
-31
@@ -7,6 +7,7 @@ import androidx.compose.foundation.gestures.draggable
|
||||
import androidx.compose.foundation.gestures.rememberDraggableState
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
@@ -14,27 +15,36 @@ import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||
import androidx.compose.material.icons.rounded.MoreVert
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Modifier
|
||||
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
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import coil.compose.AsyncImage
|
||||
import com.conena.nanokt.android.content.pxToDp
|
||||
@@ -56,16 +66,30 @@ fun PhotoViewRoute(
|
||||
|
||||
PhotoViewScreen(
|
||||
screenState = screenState,
|
||||
onBack = onBack
|
||||
onBack = onBack,
|
||||
onPageChanged = viewModel::onPageChanged,
|
||||
onCopyLinkClicked = viewModel::onCopyLinkClicked,
|
||||
onCopyClicked = viewModel::onCopyClicked
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PhotoViewScreen(
|
||||
screenState: PhotoViewScreenState = PhotoViewScreenState.EMPTY,
|
||||
onBack: () -> Unit = {}
|
||||
onBack: () -> Unit = {},
|
||||
onPageChanged: (index: Int) -> Unit = {},
|
||||
onCopyLinkClicked: () -> Unit = {},
|
||||
onCopyClicked: () -> Unit = {}
|
||||
) {
|
||||
val pagerState = rememberPagerState(pageCount = { screenState.images.size })
|
||||
val pagerState = rememberPagerState(
|
||||
pageCount = { screenState.images.size },
|
||||
initialPage = screenState.selectedPage
|
||||
)
|
||||
|
||||
LaunchedEffect(pagerState) {
|
||||
snapshotFlow { pagerState.currentPage }
|
||||
.collect(onPageChanged)
|
||||
}
|
||||
|
||||
var offsetY by remember { mutableFloatStateOf(0f) }
|
||||
|
||||
@@ -81,7 +105,13 @@ fun PhotoViewScreen(
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.graphicsLayer(alpha = calculatedAlpha),
|
||||
topBar = { TopBar(onBack = onBack) },
|
||||
topBar = {
|
||||
TopBar(
|
||||
onBack = onBack,
|
||||
onCopyClicked = onCopyClicked,
|
||||
onCopyLinkClicked = onCopyLinkClicked,
|
||||
)
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.background.copy(
|
||||
alpha = calculatedAlpha
|
||||
)
|
||||
@@ -103,14 +133,18 @@ fun PhotoViewScreen(
|
||||
@Composable
|
||||
fun TopBar(
|
||||
modifier: Modifier = Modifier,
|
||||
onBack: () -> Unit
|
||||
onBack: () -> Unit,
|
||||
onCopyClicked: () -> Unit,
|
||||
onCopyLinkClicked: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
var dropdownMenuShown by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
val hideDropDownMenu by rememberUpdatedState(
|
||||
{ dropdownMenuShown = false }
|
||||
)
|
||||
|
||||
TopAppBar(
|
||||
modifier = modifier,
|
||||
title = {},
|
||||
@@ -123,29 +157,40 @@ fun TopBar(
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
// IconButton.kt(
|
||||
// onClick = { dropdownMenuShown = true }
|
||||
// ) {
|
||||
// Icon(
|
||||
// imageVector = Icons.Rounded.MoreVert,
|
||||
// contentDescription = "Options"
|
||||
// )
|
||||
// }
|
||||
IconButton(
|
||||
onClick = { dropdownMenuShown = true }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.MoreVert,
|
||||
contentDescription = "Options"
|
||||
)
|
||||
}
|
||||
|
||||
// DropdownMenu(
|
||||
// modifier = Modifier.defaultMinSize(minWidth = 140.dp),
|
||||
// expanded = dropdownMenuShown,
|
||||
// onDismissRequest = { dropdownMenuShown = false },
|
||||
// offset = DpOffset(x = (10).dp, y = (-60).dp)
|
||||
// ) {
|
||||
// DropdownMenuItem(
|
||||
// onClick = {
|
||||
// Toast.makeText(context, "Save clicked", Toast.LENGTH_SHORT).show()
|
||||
// dropdownMenuShown = false
|
||||
// },
|
||||
// text = { Text(text = "Save") },
|
||||
// )
|
||||
// }
|
||||
DropdownMenu(
|
||||
modifier = Modifier.defaultMinSize(minWidth = 140.dp),
|
||||
expanded = dropdownMenuShown,
|
||||
onDismissRequest = { dropdownMenuShown = false },
|
||||
offset = DpOffset(x = (10).dp, y = (-60).dp)
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
hideDropDownMenu()
|
||||
onCopyLinkClicked()
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(UiR.string.action_copy_link))
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
hideDropDownMenu()
|
||||
onCopyClicked()
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(UiR.string.action_copy_image))
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent)
|
||||
)
|
||||
@@ -230,7 +275,8 @@ private fun PhotoViewScreenPreview() {
|
||||
screenState = PhotoViewScreenState(
|
||||
images = List(200) {
|
||||
UiImage.Resource(UiR.drawable.test_captcha)
|
||||
}
|
||||
},
|
||||
selectedPage = 0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user