Refactor: Use Dialog for PhotoViewScreen
This commit refactors the PhotoViewScreen to be displayed as a Dialog instead of a separate navigation destination.
Key changes:
- Introduced `PhotoViewDialog` composable that wraps `PhotoViewRoute` in a `FullScreenDialog`.
- Modified `RootScreen` to use `PhotoViewDialog` for displaying images.
- Updated `PhotoViewViewModelImpl` to handle loading state and display a loader while downloading images.
- Made `Loader` and `ContainedLoader` colors configurable.
- Adjusted `PhotoViewScreen` UI:
- Set background to translucent black.
- Updated TopAppBar background color and icon tints.
- Improved vertical drag gesture for dismissing the viewer.
- Made `VkUserData.LastSeen.platform` nullable.
- Removed unused navigation functions related to the old PhotoViewScreen.
This commit is contained in:
@@ -7,6 +7,8 @@ import androidx.activity.compose.LocalActivity
|
|||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
@@ -14,6 +16,10 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
@@ -35,8 +41,7 @@ import dev.meloda.fast.messageshistory.navigation.messagesHistoryScreen
|
|||||||
import dev.meloda.fast.messageshistory.navigation.navigateToMessagesHistory
|
import dev.meloda.fast.messageshistory.navigation.navigateToMessagesHistory
|
||||||
import dev.meloda.fast.navigation.Main
|
import dev.meloda.fast.navigation.Main
|
||||||
import dev.meloda.fast.navigation.mainScreen
|
import dev.meloda.fast.navigation.mainScreen
|
||||||
import dev.meloda.fast.photoviewer.navigation.navigateToPhotoView
|
import dev.meloda.fast.photoviewer.presentation.PhotoViewDialog
|
||||||
import dev.meloda.fast.photoviewer.navigation.photoViewScreen
|
|
||||||
import dev.meloda.fast.settings.navigation.navigateToSettings
|
import dev.meloda.fast.settings.navigation.navigateToSettings
|
||||||
import dev.meloda.fast.settings.navigation.settingsScreen
|
import dev.meloda.fast.settings.navigation.settingsScreen
|
||||||
import dev.meloda.fast.ui.R
|
import dev.meloda.fast.ui.R
|
||||||
@@ -120,64 +125,75 @@ fun RootScreen(
|
|||||||
LocalNavRootController provides navController,
|
LocalNavRootController provides navController,
|
||||||
LocalNavController provides navController
|
LocalNavController provides navController
|
||||||
) {
|
) {
|
||||||
NavHost(
|
var photoViewerInfo by rememberSaveable {
|
||||||
navController = navController,
|
mutableStateOf<Pair<List<String>, Int?>?>(null)
|
||||||
startDestination = requireNotNull(startDestination),
|
}
|
||||||
enterTransition = { fadeIn(animationSpec = tween(200)) },
|
|
||||||
exitTransition = { fadeOut(animationSpec = tween(200)) }
|
|
||||||
) {
|
|
||||||
authNavGraph(
|
|
||||||
onNavigateToMain = {
|
|
||||||
viewModel.onUserAuthenticated()
|
|
||||||
navController.navigateToMain()
|
|
||||||
},
|
|
||||||
onNavigateToSettings = navController::navigateToSettings,
|
|
||||||
navController = navController
|
|
||||||
)
|
|
||||||
|
|
||||||
mainScreen(
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
onError = viewModel::onError,
|
NavHost(
|
||||||
onSettingsButtonClicked = navController::navigateToSettings,
|
navController = navController,
|
||||||
onNavigateToMessagesHistory = navController::navigateToMessagesHistory,
|
startDestination = requireNotNull(startDestination),
|
||||||
onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) },
|
enterTransition = { fadeIn(animationSpec = tween(200)) },
|
||||||
onMessageClicked = navController::navigateToMessagesHistory,
|
exitTransition = { fadeOut(animationSpec = tween(200)) }
|
||||||
onNavigateToCreateChat = navController::navigateToCreateChat
|
) {
|
||||||
)
|
authNavGraph(
|
||||||
|
onNavigateToMain = {
|
||||||
|
viewModel.onUserAuthenticated()
|
||||||
|
navController.navigateToMain()
|
||||||
|
},
|
||||||
|
onNavigateToSettings = navController::navigateToSettings,
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
|
|
||||||
messagesHistoryScreen(
|
mainScreen(
|
||||||
onError = viewModel::onError,
|
onError = viewModel::onError,
|
||||||
onBack = navController::navigateUp,
|
onSettingsButtonClicked = navController::navigateToSettings,
|
||||||
onNavigateToChatMaterials = navController::navigateToChatMaterials,
|
onNavigateToMessagesHistory = navController::navigateToMessagesHistory,
|
||||||
onNavigateToPhotoViewer = navController::navigateToPhotoView
|
onPhotoClicked = { url -> photoViewerInfo = listOf(url) to null },
|
||||||
)
|
onMessageClicked = navController::navigateToMessagesHistory,
|
||||||
chatMaterialsScreen(
|
onNavigateToCreateChat = navController::navigateToCreateChat
|
||||||
onBack = navController::navigateUp,
|
)
|
||||||
onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) }
|
|
||||||
)
|
|
||||||
createChatScreen(
|
|
||||||
onChatCreated = { conversationId ->
|
|
||||||
navController.popBackStack()
|
|
||||||
navController.navigateToMessagesHistory(conversationId)
|
|
||||||
},
|
|
||||||
navController = navController
|
|
||||||
)
|
|
||||||
|
|
||||||
settingsScreen(
|
messagesHistoryScreen(
|
||||||
onBack = navController::navigateUp,
|
onError = viewModel::onError,
|
||||||
onLogOutButtonClicked = { navController.navigateToAuth(true) },
|
onBack = navController::navigateUp,
|
||||||
onLanguageItemClicked = navController::navigateToLanguagePicker,
|
onNavigateToChatMaterials = navController::navigateToChatMaterials,
|
||||||
onRestartRequired = {
|
onNavigateToPhotoViewer = { photos, index ->
|
||||||
activity?.let {
|
photoViewerInfo = photos to index
|
||||||
val intent = Intent(activity, MainActivity::class.java)
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
|
||||||
activity.startActivity(intent)
|
|
||||||
activity.finish()
|
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
)
|
chatMaterialsScreen(
|
||||||
languagePickerScreen(onBack = navController::navigateUp)
|
onBack = navController::navigateUp,
|
||||||
|
onPhotoClicked = { url -> photoViewerInfo = listOf(url) to null }
|
||||||
|
)
|
||||||
|
createChatScreen(
|
||||||
|
onChatCreated = { conversationId ->
|
||||||
|
navController.popBackStack()
|
||||||
|
navController.navigateToMessagesHistory(conversationId)
|
||||||
|
},
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
|
|
||||||
photoViewScreen(onBack = navController::navigateUp)
|
settingsScreen(
|
||||||
|
onBack = navController::navigateUp,
|
||||||
|
onLogOutButtonClicked = { navController.navigateToAuth(true) },
|
||||||
|
onLanguageItemClicked = navController::navigateToLanguagePicker,
|
||||||
|
onRestartRequired = {
|
||||||
|
activity?.let {
|
||||||
|
val intent = Intent(activity, MainActivity::class.java)
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||||
|
activity.startActivity(intent)
|
||||||
|
activity.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
languagePickerScreen(onBack = navController::navigateUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
PhotoViewDialog(
|
||||||
|
photoViewerInfo = photoViewerInfo,
|
||||||
|
onDismiss = { photoViewerInfo = null }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ data class VkUserData(
|
|||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class LastSeen(
|
data class LastSeen(
|
||||||
@Json(name = "platform") val platform: Int,
|
@Json(name = "platform") val platform: Int?,
|
||||||
@Json(name = "time") val time: Int
|
@Json(name = "time") val time: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package dev.meloda.fast.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FullScreenDialog(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = {},
|
||||||
|
DialogProperties(
|
||||||
|
usePlatformDefaultWidth = false,
|
||||||
|
decorFitsSystemWindows = false
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.zIndex(10F),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,52 +10,70 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@Preview
|
@Preview
|
||||||
fun FullScreenContainedLoader(modifier: Modifier = Modifier) {
|
fun FullScreenContainedLoader(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
containerColor: Color = MaterialTheme.colorScheme.primary,
|
||||||
|
indicatorColor: Color = MaterialTheme.colorScheme.primaryContainer
|
||||||
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.navigationBarsPadding(),
|
.navigationBarsPadding(),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
ContainedLoader()
|
ContainedLoader(
|
||||||
|
containerColor = containerColor,
|
||||||
|
indicatorColor = indicatorColor
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun FullScreenLoader(modifier: Modifier = Modifier) {
|
fun FullScreenLoader(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
color: Color = MaterialTheme.colorScheme.primary
|
||||||
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.navigationBarsPadding(),
|
.navigationBarsPadding(),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Loader()
|
Loader(color = color)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@Preview
|
@Preview
|
||||||
fun ContainedLoader(modifier: Modifier = Modifier) {
|
fun ContainedLoader(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
containerColor: Color = MaterialTheme.colorScheme.primary,
|
||||||
|
indicatorColor: Color = MaterialTheme.colorScheme.primaryContainer
|
||||||
|
) {
|
||||||
ContainedLoadingIndicator(
|
ContainedLoadingIndicator(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
containerColor = MaterialTheme.colorScheme.primary,
|
containerColor = containerColor,
|
||||||
indicatorColor = MaterialTheme.colorScheme.primaryContainer
|
indicatorColor = indicatorColor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@Preview
|
@Preview
|
||||||
fun Loader(modifier: Modifier = Modifier) {
|
fun Loader(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
color: Color = MaterialTheme.colorScheme.primary
|
||||||
|
) {
|
||||||
LoadingIndicator(
|
LoadingIndicator(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
color = MaterialTheme.colorScheme.primary
|
color = color
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+41
-18
@@ -17,6 +17,7 @@ import coil.imageLoader
|
|||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import dev.meloda.fast.common.extensions.setValue
|
import dev.meloda.fast.common.extensions.setValue
|
||||||
import dev.meloda.fast.common.model.UiImage
|
import dev.meloda.fast.common.model.UiImage
|
||||||
|
import dev.meloda.fast.photoviewer.model.PhotoViewArguments
|
||||||
import dev.meloda.fast.photoviewer.model.PhotoViewScreenState
|
import dev.meloda.fast.photoviewer.model.PhotoViewScreenState
|
||||||
import dev.meloda.fast.photoviewer.navigation.PhotoView
|
import dev.meloda.fast.photoviewer.navigation.PhotoView
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -47,17 +48,22 @@ interface PhotoViewViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class PhotoViewViewModelImpl(
|
class PhotoViewViewModelImpl(
|
||||||
savedStateHandle: SavedStateHandle,
|
arguments: PhotoViewArguments,
|
||||||
private val applicationContext: Context
|
private val applicationContext: Context
|
||||||
) : PhotoViewViewModel, ViewModel() {
|
) : PhotoViewViewModel, ViewModel() {
|
||||||
|
|
||||||
override val screenState = MutableStateFlow(PhotoViewScreenState.EMPTY)
|
constructor(
|
||||||
|
savedStateHandle: SavedStateHandle,
|
||||||
|
applicationContext: Context
|
||||||
|
) : this(
|
||||||
|
arguments = PhotoView.from(savedStateHandle).arguments,
|
||||||
|
applicationContext = applicationContext
|
||||||
|
)
|
||||||
|
|
||||||
|
override val screenState = MutableStateFlow(PhotoViewScreenState.EMPTY)
|
||||||
override val shareRequest = MutableStateFlow<Uri?>(null)
|
override val shareRequest = MutableStateFlow<Uri?>(null)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val arguments = PhotoView.from(savedStateHandle).arguments
|
|
||||||
|
|
||||||
screenState.setValue { old ->
|
screenState.setValue { old ->
|
||||||
old.copy(
|
old.copy(
|
||||||
images = arguments.imageUrls
|
images = arguments.imageUrls
|
||||||
@@ -165,20 +171,37 @@ class PhotoViewViewModelImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun downloadAndStoreImageToCache(url: String): File? =
|
private suspend fun downloadAndStoreImageToCache(url: String): File? =
|
||||||
withContext(Dispatchers.IO) {
|
runCatching {
|
||||||
val drawable = applicationContext.imageLoader.execute(
|
withContext(Dispatchers.IO) {
|
||||||
ImageRequest.Builder(applicationContext)
|
screenState.setValue { old -> old.copy(isLoading = true) }
|
||||||
.data(url)
|
|
||||||
.build()
|
|
||||||
).drawable ?: return@withContext null
|
|
||||||
|
|
||||||
val imagesDir = File(applicationContext.cacheDir, "images")
|
val drawable = applicationContext.imageLoader.execute(
|
||||||
if (!imagesDir.exists()) imagesDir.mkdirs()
|
ImageRequest.Builder(applicationContext)
|
||||||
val imageFile = File(imagesDir, "shared_image_id${UUID.randomUUID()}.png")
|
.data(url)
|
||||||
FileOutputStream(imageFile).use {
|
.build()
|
||||||
drawable.toBitmapOrNull()?.compress(Bitmap.CompressFormat.PNG, 100, it)
|
).drawable ?: run {
|
||||||
|
screenState.setValue { old -> old.copy(isLoading = false) }
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
}.fold(
|
||||||
imageFile
|
onSuccess = { file ->
|
||||||
}
|
screenState.setValue { old -> old.copy(isLoading = false) }
|
||||||
|
file
|
||||||
|
},
|
||||||
|
onFailure = { e ->
|
||||||
|
e.printStackTrace()
|
||||||
|
screenState.setValue { old -> old.copy(isLoading = false) }
|
||||||
|
null
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,15 @@ package dev.meloda.fast.photoviewer.di
|
|||||||
|
|
||||||
import dev.meloda.fast.photoviewer.PhotoViewViewModel
|
import dev.meloda.fast.photoviewer.PhotoViewViewModel
|
||||||
import dev.meloda.fast.photoviewer.PhotoViewViewModelImpl
|
import dev.meloda.fast.photoviewer.PhotoViewViewModelImpl
|
||||||
import org.koin.core.module.dsl.viewModelOf
|
import org.koin.core.module.dsl.viewModel
|
||||||
import org.koin.dsl.bind
|
import org.koin.dsl.bind
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val photoViewModule = module {
|
val photoViewModule = module {
|
||||||
viewModelOf(::PhotoViewViewModelImpl) bind PhotoViewViewModel::class
|
viewModel {
|
||||||
|
PhotoViewViewModelImpl(
|
||||||
|
savedStateHandle = get(),
|
||||||
|
applicationContext = get()
|
||||||
|
)
|
||||||
|
} bind PhotoViewViewModel::class
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -6,13 +6,15 @@ import dev.meloda.fast.common.model.UiImage
|
|||||||
@Immutable
|
@Immutable
|
||||||
data class PhotoViewScreenState(
|
data class PhotoViewScreenState(
|
||||||
val images: List<UiImage>,
|
val images: List<UiImage>,
|
||||||
val selectedPage: Int
|
val selectedPage: Int,
|
||||||
|
val isLoading: Boolean
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val EMPTY: PhotoViewScreenState = PhotoViewScreenState(
|
val EMPTY: PhotoViewScreenState = PhotoViewScreenState(
|
||||||
images = emptyList(),
|
images = emptyList(),
|
||||||
selectedPage = 0
|
selectedPage = 0,
|
||||||
|
isLoading = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-27
@@ -1,15 +1,10 @@
|
|||||||
package dev.meloda.fast.photoviewer.navigation
|
package dev.meloda.fast.photoviewer.navigation
|
||||||
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.navigation.NavController
|
|
||||||
import androidx.navigation.NavGraphBuilder
|
|
||||||
import androidx.navigation.compose.composable
|
|
||||||
import androidx.navigation.toRoute
|
import androidx.navigation.toRoute
|
||||||
import dev.meloda.fast.photoviewer.model.PhotoViewArguments
|
import dev.meloda.fast.photoviewer.model.PhotoViewArguments
|
||||||
import dev.meloda.fast.photoviewer.presentation.PhotoViewRoute
|
|
||||||
import dev.meloda.fast.ui.extensions.customNavType
|
import dev.meloda.fast.ui.extensions.customNavType
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.net.URLEncoder
|
|
||||||
import kotlin.reflect.typeOf
|
import kotlin.reflect.typeOf
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -21,25 +16,3 @@ data class PhotoView(val arguments: PhotoViewArguments) {
|
|||||||
savedStateHandle.toRoute<PhotoView>(typeMap)
|
savedStateHandle.toRoute<PhotoView>(typeMap)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun NavGraphBuilder.photoViewScreen(
|
|
||||||
onBack: () -> Unit
|
|
||||||
) {
|
|
||||||
composable<PhotoView>(typeMap = PhotoView.typeMap) {
|
|
||||||
PhotoViewRoute(onBack = onBack)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun NavController.navigateToPhotoView(
|
|
||||||
images: List<String>,
|
|
||||||
selectedIndex: Int? = null
|
|
||||||
) {
|
|
||||||
this.navigate(
|
|
||||||
PhotoView(
|
|
||||||
arguments = PhotoViewArguments(
|
|
||||||
imageUrls = images.map { URLEncoder.encode(it, "utf-8") },
|
|
||||||
selectedIndex = selectedIndex
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
+88
-39
@@ -2,8 +2,13 @@ package dev.meloda.fast.photoviewer.presentation
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.LinearEasing
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.gestures.Orientation
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
import androidx.compose.foundation.gestures.draggable
|
import androidx.compose.foundation.gestures.draggable
|
||||||
import androidx.compose.foundation.gestures.rememberDraggableState
|
import androidx.compose.foundation.gestures.rememberDraggableState
|
||||||
@@ -24,7 +29,6 @@ import androidx.compose.material3.DropdownMenuItem
|
|||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
@@ -40,32 +44,65 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.graphics.painter.ColorPainter
|
import androidx.compose.ui.graphics.painter.ColorPainter
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalWindowInfo
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.DpOffset
|
import androidx.compose.ui.unit.DpOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import com.conena.nanokt.android.content.pxToDp
|
|
||||||
import dev.meloda.fast.common.model.UiImage
|
|
||||||
import dev.meloda.fast.photoviewer.PhotoViewViewModel
|
import dev.meloda.fast.photoviewer.PhotoViewViewModel
|
||||||
import dev.meloda.fast.photoviewer.PhotoViewViewModelImpl
|
import dev.meloda.fast.photoviewer.PhotoViewViewModelImpl
|
||||||
|
import dev.meloda.fast.photoviewer.model.PhotoViewArguments
|
||||||
import dev.meloda.fast.photoviewer.model.PhotoViewScreenState
|
import dev.meloda.fast.photoviewer.model.PhotoViewScreenState
|
||||||
|
import dev.meloda.fast.ui.components.FullScreenDialog
|
||||||
|
import dev.meloda.fast.ui.components.Loader
|
||||||
import dev.meloda.fast.ui.util.getImage
|
import dev.meloda.fast.ui.util.getImage
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
import java.net.URLEncoder
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import dev.meloda.fast.ui.R as UiR
|
import dev.meloda.fast.ui.R as UiR
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PhotoViewRoute(
|
fun PhotoViewDialog(
|
||||||
|
photoViewerInfo: Pair<List<String>, Int?>?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
val applicationContext = LocalContext.current.applicationContext
|
||||||
|
|
||||||
|
if (photoViewerInfo != null) {
|
||||||
|
FullScreenDialog(modifier = modifier) {
|
||||||
|
val viewModel = remember(true) {
|
||||||
|
PhotoViewViewModelImpl(
|
||||||
|
arguments = PhotoViewArguments(
|
||||||
|
imageUrls = photoViewerInfo.first.map {
|
||||||
|
URLEncoder.encode(it, "utf-8")
|
||||||
|
},
|
||||||
|
selectedIndex = photoViewerInfo.second
|
||||||
|
),
|
||||||
|
applicationContext = applicationContext
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
PhotoViewRoute(
|
||||||
|
onBack = onDismiss,
|
||||||
|
viewModel = viewModel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PhotoViewRoute(
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
viewModel: PhotoViewViewModel = koinViewModel<PhotoViewViewModelImpl>()
|
viewModel: PhotoViewViewModel = koinViewModel<PhotoViewViewModelImpl>()
|
||||||
) {
|
) {
|
||||||
@@ -80,7 +117,7 @@ fun PhotoViewRoute(
|
|||||||
viewModel.onImageShared()
|
viewModel.onImageShared()
|
||||||
|
|
||||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||||
setType("image/png")
|
type = "image/png"
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
putExtra(Intent.EXTRA_STREAM, shareRequest)
|
putExtra(Intent.EXTRA_STREAM, shareRequest)
|
||||||
}
|
}
|
||||||
@@ -116,7 +153,7 @@ fun PhotoViewRoute(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PhotoViewScreen(
|
private fun PhotoViewScreen(
|
||||||
screenState: PhotoViewScreenState = PhotoViewScreenState.EMPTY,
|
screenState: PhotoViewScreenState = PhotoViewScreenState.EMPTY,
|
||||||
onBack: () -> Unit = {},
|
onBack: () -> Unit = {},
|
||||||
onPageChanged: (index: Int) -> Unit = {},
|
onPageChanged: (index: Int) -> Unit = {},
|
||||||
@@ -148,7 +185,6 @@ fun PhotoViewScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.graphicsLayer(alpha = calculatedAlpha),
|
|
||||||
topBar = {
|
topBar = {
|
||||||
TopBar(
|
TopBar(
|
||||||
onBack = onBack,
|
onBack = onBack,
|
||||||
@@ -158,9 +194,7 @@ fun PhotoViewScreen(
|
|||||||
onCopyLinkClicked = onCopyLinkClicked,
|
onCopyLinkClicked = onCopyLinkClicked,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
containerColor = MaterialTheme.colorScheme.background.copy(
|
containerColor = Color.Black.copy(alpha = calculatedAlpha)
|
||||||
alpha = calculatedAlpha
|
|
||||||
)
|
|
||||||
) { padding ->
|
) { padding ->
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
Pager(
|
Pager(
|
||||||
@@ -169,15 +203,34 @@ fun PhotoViewScreen(
|
|||||||
padding = padding,
|
padding = padding,
|
||||||
onBack = onBack,
|
onBack = onBack,
|
||||||
onVerticalDrag = { offset -> offsetY = offset },
|
onVerticalDrag = { offset -> offsetY = offset },
|
||||||
modifier = Modifier
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = screenState.isLoading,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clickable(
|
||||||
|
interactionSource = null,
|
||||||
|
indication = null,
|
||||||
|
onClick = {}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Black.copy(alpha = 0.5f)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Loader(color = Color.White)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun TopBar(
|
private fun TopBar(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onShareClicked: () -> Unit,
|
onShareClicked: () -> Unit,
|
||||||
@@ -193,12 +246,16 @@ fun TopBar(
|
|||||||
|
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = Color.Black.copy(alpha = 0.3f)
|
||||||
|
),
|
||||||
title = {},
|
title = {},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = onBack) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
|
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
|
||||||
contentDescription = "Back button"
|
contentDescription = "Back button",
|
||||||
|
tint = Color.White
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -208,7 +265,8 @@ fun TopBar(
|
|||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.MoreVert,
|
imageVector = Icons.Rounded.MoreVert,
|
||||||
contentDescription = "Options"
|
contentDescription = "Options",
|
||||||
|
tint = Color.White
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,13 +313,12 @@ fun TopBar(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Pager(
|
private fun Pager(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
pagerState: PagerState,
|
pagerState: PagerState,
|
||||||
state: PhotoViewScreenState,
|
state: PhotoViewScreenState,
|
||||||
@@ -269,6 +326,8 @@ fun Pager(
|
|||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onVerticalDrag: (offset: Float) -> Unit
|
onVerticalDrag: (offset: Float) -> Unit
|
||||||
) {
|
) {
|
||||||
|
val windowInfo = LocalWindowInfo.current
|
||||||
|
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
modifier = modifier.fillMaxSize()
|
modifier = modifier.fillMaxSize()
|
||||||
@@ -289,22 +348,25 @@ fun Pager(
|
|||||||
} else {
|
} else {
|
||||||
var offsetY by remember { mutableFloatStateOf(0f) }
|
var offsetY by remember { mutableFloatStateOf(0f) }
|
||||||
|
|
||||||
val animatedOffset by animateFloatAsState(
|
|
||||||
targetValue = offsetY,
|
|
||||||
label = "animatedOffset"
|
|
||||||
)
|
|
||||||
var useAnimatedOffset by remember {
|
var useAnimatedOffset by remember {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val animatedOffset by animateFloatAsState(
|
||||||
|
targetValue = offsetY,
|
||||||
|
label = "animatedOffset",
|
||||||
|
animationSpec = tween(
|
||||||
|
durationMillis = if (useAnimatedOffset) 150 else 0,
|
||||||
|
easing = LinearEasing
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = model,
|
model = model,
|
||||||
contentDescription = "Image",
|
contentDescription = "Image",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
this.translationY = if (useAnimatedOffset) {
|
this.translationY = animatedOffset
|
||||||
animatedOffset
|
|
||||||
} else offsetY
|
|
||||||
}
|
}
|
||||||
.draggable(
|
.draggable(
|
||||||
state = rememberDraggableState { delta ->
|
state = rememberDraggableState { delta ->
|
||||||
@@ -314,7 +376,7 @@ fun Pager(
|
|||||||
},
|
},
|
||||||
orientation = Orientation.Vertical,
|
orientation = Orientation.Vertical,
|
||||||
onDragStopped = {
|
onDragStopped = {
|
||||||
if (abs(offsetY.pxToDp()) >= 200) {
|
if (abs(offsetY) / windowInfo.containerSize.height >= 0.25) {
|
||||||
onBack()
|
onBack()
|
||||||
} else {
|
} else {
|
||||||
useAnimatedOffset = true
|
useAnimatedOffset = true
|
||||||
@@ -331,16 +393,3 @@ fun Pager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
private fun PhotoViewScreenPreview() {
|
|
||||||
PhotoViewScreen(
|
|
||||||
screenState = PhotoViewScreenState(
|
|
||||||
images = List(200) {
|
|
||||||
UiImage.Resource(UiR.drawable.test_captcha)
|
|
||||||
},
|
|
||||||
selectedPage = 0
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user