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:
2025-06-24 14:44:51 +03:00
parent c1e76e1c60
commit 3dae1fe101
29 changed files with 406 additions and 192 deletions
@@ -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()
}
}
}
}
@@ -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
@@ -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
)
}
}
@@ -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
)
)
)
@@ -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
)
)
}