From 1817698031a068e8489d0b337fee6faa061dfa6a Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Tue, 16 Jul 2024 10:29:37 +0300 Subject: [PATCH] simple photo viewer --- app/build.gradle.kts | 1 + .../dev/meloda/fast/navigation/MainGraph.kt | 2 + .../meloda/fast/presentation/MainScreen.kt | 24 +- .../meloda/fast/presentation/RootScreen.kt | 12 +- .../dev/meloda/fast/ui/util/Extensions.kt | 25 ++ .../navigation/ChatMaterialsNavigation.kt | 8 +- .../presentation/ChatMaterialItem.kt | 5 +- .../presentation/ChatMaterialsScreen.kt | 34 ++- .../navigation/ConversationsNavigation.kt | 2 + .../presentation/ConversationItem.kt | 34 +-- .../presentation/ConversationsList.kt | 8 +- .../presentation/ConversationsScreen.kt | 20 +- .../friends/navigation/FriendsNavigation.kt | 6 +- .../fast/friends/presentation/FriendItem.kt | 12 +- .../fast/friends/presentation/FriendsList.kt | 6 +- .../friends/presentation/FriendsScreen.kt | 18 +- .../presentation/MessagesHistoryScreen.kt | 6 +- .../meloda/fast/messageshistory/util/Ext.kt | 2 +- feature/photoviewer/build.gradle.kts | 10 + .../fast/photoviewer/PhotoViewViewModel.kt | 30 ++- .../photoviewer/model/PhotoViewArguments.kt | 12 +- ...toViewState.kt => PhotoViewScreenState.kt} | 4 +- .../navigation/PhotoViewNavigation.kt | 41 +++ .../presentation/PhotoViewScreen.kt | 249 ++++++++++++++++++ .../presentation/PhotoViewScreenContent.kt | 177 ------------- .../fast/profile/navigation/ProfileRoute.kt | 2 + .../profile/presentation/ProfileScreen.kt | 12 +- 27 files changed, 484 insertions(+), 278 deletions(-) rename feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/model/{PhotoViewState.kt => PhotoViewScreenState.kt} (72%) create mode 100644 feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/navigation/PhotoViewNavigation.kt create mode 100644 feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/presentation/PhotoViewScreen.kt delete mode 100644 feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/presentation/PhotoViewScreenContent.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d283fac7..2d9a0193 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -129,6 +129,7 @@ dependencies { implementation(projects.feature.settings) implementation(projects.feature.friends) implementation(projects.feature.profile) + implementation(projects.feature.photoviewer) implementation(projects.core.common) implementation(projects.core.ui) diff --git a/app/src/main/kotlin/dev/meloda/fast/navigation/MainGraph.kt b/app/src/main/kotlin/dev/meloda/fast/navigation/MainGraph.kt index 12fa1a75..dea46be2 100644 --- a/app/src/main/kotlin/dev/meloda/fast/navigation/MainGraph.kt +++ b/app/src/main/kotlin/dev/meloda/fast/navigation/MainGraph.kt @@ -21,6 +21,7 @@ fun NavGraphBuilder.mainScreen( onError: (BaseError) -> Unit, onSettingsButtonClicked: () -> Unit, onConversationClicked: (conversationId: Int) -> Unit, + onPhotoClicked: (url: String) -> Unit ) { val navigationItems = listOf( BottomNavigationItem( @@ -49,6 +50,7 @@ fun NavGraphBuilder.mainScreen( onError = onError, onSettingsButtonClicked = onSettingsButtonClicked, onConversationItemClicked = onConversationClicked, + onPhotoClicked = onPhotoClicked ) } } diff --git a/app/src/main/kotlin/dev/meloda/fast/presentation/MainScreen.kt b/app/src/main/kotlin/dev/meloda/fast/presentation/MainScreen.kt index 3488fdf9..8de90ec5 100644 --- a/app/src/main/kotlin/dev/meloda/fast/presentation/MainScreen.kt +++ b/app/src/main/kotlin/dev/meloda/fast/presentation/MainScreen.kt @@ -25,19 +25,19 @@ import androidx.compose.ui.unit.dp import androidx.navigation.compose.NavHost import androidx.navigation.compose.navigation import androidx.navigation.compose.rememberNavController +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.hazeChild +import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi +import dev.chrisbanes.haze.materials.HazeMaterials import dev.meloda.fast.conversations.navigation.conversationsScreen -import dev.meloda.fast.ui.theme.LocalBottomPadding -import dev.meloda.fast.ui.theme.LocalHazeState -import dev.meloda.fast.ui.theme.LocalThemeConfig import dev.meloda.fast.friends.navigation.friendsScreen import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.BottomNavigationItem import dev.meloda.fast.navigation.MainGraph import dev.meloda.fast.profile.navigation.profileScreen -import dev.chrisbanes.haze.HazeState -import dev.chrisbanes.haze.hazeChild -import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi -import dev.chrisbanes.haze.materials.HazeMaterials +import dev.meloda.fast.ui.theme.LocalBottomPadding +import dev.meloda.fast.ui.theme.LocalHazeState +import dev.meloda.fast.ui.theme.LocalThemeConfig @OptIn(ExperimentalHazeMaterialsApi::class) @Composable @@ -45,7 +45,8 @@ fun MainScreen( navigationItems: List, onError: (BaseError) -> Unit = {}, onSettingsButtonClicked: () -> Unit = {}, - onConversationItemClicked: (conversationId: Int) -> Unit = {} + onConversationItemClicked: (conversationId: Int) -> Unit = {}, + onPhotoClicked: (url: String) -> Unit = {} ) { val currentTheme = LocalThemeConfig.current val hazeState = remember { HazeState() } @@ -120,16 +121,19 @@ fun MainScreen( navigation(startDestination = navigationItems[selectedItemIndex].route) { friendsScreen( onError = onError, - navController = navController + navController = navController, + onPhotoClicked = onPhotoClicked ) conversationsScreen( onError = onError, onConversationItemClicked = onConversationItemClicked, - navController = navController + navController = navController, + onPhotoClicked = onPhotoClicked ) profileScreen( onError = onError, onSettingsButtonClicked = onSettingsButtonClicked, + onPhotoClicked = onPhotoClicked, navController = navController ) } diff --git a/app/src/main/kotlin/dev/meloda/fast/presentation/RootScreen.kt b/app/src/main/kotlin/dev/meloda/fast/presentation/RootScreen.kt index d71ce375..27dc82c9 100644 --- a/app/src/main/kotlin/dev/meloda/fast/presentation/RootScreen.kt +++ b/app/src/main/kotlin/dev/meloda/fast/presentation/RootScreen.kt @@ -26,15 +26,17 @@ import dev.meloda.fast.auth.authNavGraph import dev.meloda.fast.auth.navigateToAuth import dev.meloda.fast.chatmaterials.navigation.chatMaterialsScreen import dev.meloda.fast.chatmaterials.navigation.navigateToChatMaterials -import dev.meloda.fast.ui.R import dev.meloda.fast.languagepicker.navigation.languagePickerScreen import dev.meloda.fast.languagepicker.navigation.navigateToLanguagePicker import dev.meloda.fast.messageshistory.navigation.messagesHistoryScreen import dev.meloda.fast.messageshistory.navigation.navigateToMessagesHistory import dev.meloda.fast.navigation.Main import dev.meloda.fast.navigation.mainScreen +import dev.meloda.fast.photoviewer.navigation.navigateToPhotoView +import dev.meloda.fast.photoviewer.navigation.photoViewScreen import dev.meloda.fast.settings.navigation.navigateToSettings import dev.meloda.fast.settings.navigation.settingsScreen +import dev.meloda.fast.ui.R import org.koin.androidx.compose.koinViewModel @Composable @@ -122,7 +124,8 @@ fun RootScreen( mainScreen( onError = viewModel::onError, onSettingsButtonClicked = navController::navigateToSettings, - onConversationClicked = navController::navigateToMessagesHistory + onConversationClicked = navController::navigateToMessagesHistory, + onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) } ) messagesHistoryScreen( @@ -131,7 +134,8 @@ fun RootScreen( onChatMaterialsDropdownItemClicked = navController::navigateToChatMaterials ) chatMaterialsScreen( - onBack = navController::navigateUp + onBack = navController::navigateUp, + onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) } ) settingsScreen( @@ -140,6 +144,8 @@ fun RootScreen( onLanguageItemClicked = navController::navigateToLanguagePicker ) languagePickerScreen(onBack = navController::navigateUp) + + photoViewScreen(onBack = navController::navigateUp) } } } diff --git a/core/ui/src/main/kotlin/dev/meloda/fast/ui/util/Extensions.kt b/core/ui/src/main/kotlin/dev/meloda/fast/ui/util/Extensions.kt index e7ba4986..211e7f58 100644 --- a/core/ui/src/main/kotlin/dev/meloda/fast/ui/util/Extensions.kt +++ b/core/ui/src/main/kotlin/dev/meloda/fast/ui/util/Extensions.kt @@ -1,6 +1,7 @@ package dev.meloda.fast.ui.util import android.content.res.Configuration +import android.graphics.drawable.ColorDrawable import android.os.PowerManager import android.view.KeyEvent import androidx.compose.foundation.lazy.LazyListState @@ -11,12 +12,17 @@ import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.core.content.getSystemService import dev.meloda.fast.common.model.DarkMode +import dev.meloda.fast.common.model.UiImage import dev.meloda.fast.common.model.UiText @Composable @@ -47,6 +53,25 @@ fun UiText?.getString(): String? { } } +@Composable +fun UiImage.getResourcePainter(): Painter? { + return when (this) { + is UiImage.Resource -> painterResource(id = resId) + else -> null + } +} + +@Composable +fun UiImage.getImage(): Any { + return when (this) { + is UiImage.Color -> ColorDrawable(color) + is UiImage.ColorResource -> ColorDrawable(colorResource(id = resId).toArgb()) + is UiImage.Resource -> painterResource(id = resId) + is UiImage.Simple -> drawable + is UiImage.Url -> url + } +} + fun Modifier.handleTabKey( action: () -> Boolean ): Modifier = this.onKeyEvent { event -> diff --git a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/navigation/ChatMaterialsNavigation.kt b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/navigation/ChatMaterialsNavigation.kt index b169b702..d16700eb 100644 --- a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/navigation/ChatMaterialsNavigation.kt +++ b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/navigation/ChatMaterialsNavigation.kt @@ -20,10 +20,14 @@ data class ChatMaterials( } fun NavGraphBuilder.chatMaterialsScreen( - onBack: () -> Unit + onBack: () -> Unit, + onPhotoClicked: (url: String) -> Unit ) { composable { - ChatMaterialsRoute(onBack = onBack) + ChatMaterialsRoute( + onBack = onBack, + onPhotoClicked = onPhotoClicked + ) } } diff --git a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/ChatMaterialItem.kt b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/ChatMaterialItem.kt index a279578e..ee2eaf4f 100644 --- a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/ChatMaterialItem.kt +++ b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/ChatMaterialItem.kt @@ -14,7 +14,10 @@ import coil.compose.AsyncImage import dev.meloda.fast.chatmaterials.model.UiChatMaterial @Composable -fun ChatMaterialItem(item: UiChatMaterial) { +fun ChatMaterialItem( + item: UiChatMaterial, + onClick: () -> Unit +) { when (item) { is UiChatMaterial.Photo -> { AsyncImage( diff --git a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/ChatMaterialsScreen.kt b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/ChatMaterialsScreen.kt index 363d4063..ddaa084a 100644 --- a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/ChatMaterialsScreen.kt +++ b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/ChatMaterialsScreen.kt @@ -61,23 +61,25 @@ import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import dev.meloda.fast.chatmaterials.ChatMaterialsViewModel -import dev.meloda.fast.chatmaterials.ChatMaterialsViewModelImpl -import dev.meloda.fast.chatmaterials.model.ChatMaterialsScreenState -import dev.meloda.fast.datastore.UserSettings -import dev.meloda.fast.ui.R -import dev.meloda.fast.ui.theme.LocalThemeConfig import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.haze import dev.chrisbanes.haze.hazeChild import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.HazeMaterials +import dev.meloda.fast.chatmaterials.ChatMaterialsViewModel +import dev.meloda.fast.chatmaterials.ChatMaterialsViewModelImpl +import dev.meloda.fast.chatmaterials.model.ChatMaterialsScreenState +import dev.meloda.fast.chatmaterials.model.UiChatMaterial +import dev.meloda.fast.datastore.UserSettings +import dev.meloda.fast.ui.R +import dev.meloda.fast.ui.theme.LocalThemeConfig import org.koin.androidx.compose.koinViewModel import org.koin.compose.koinInject @Composable fun ChatMaterialsRoute( onBack: () -> Unit, + onPhotoClicked: (url: String) -> Unit, viewModel: ChatMaterialsViewModel = koinViewModel() ) { val userSettings: UserSettings = koinInject() @@ -92,7 +94,8 @@ fun ChatMaterialsRoute( onBack = onBack, onTypeChanged = viewModel::onTypeChanged, onRefreshDropdownItemClicked = viewModel::onRefresh, - onRefresh = viewModel::onRefresh + onRefresh = viewModel::onRefresh, + onPhotoClicked = onPhotoClicked ) } @@ -108,7 +111,8 @@ fun ChatMaterialsScreen( onBack: () -> Unit = {}, onTypeChanged: (String) -> Unit = {}, onRefreshDropdownItemClicked: () -> Unit = {}, - onRefresh: () -> Unit = {} + onRefresh: () -> Unit = {}, + onPhotoClicked: (url: String) -> Unit = {} ) { val currentTheme = LocalThemeConfig.current @@ -318,7 +322,14 @@ fun ChatMaterialsScreen( } } items(attachments) { item -> - ChatMaterialItem(item = item) + ChatMaterialItem( + item = item, + onClick = { + if (item is UiChatMaterial.Photo) { + onPhotoClicked(item.previewUrl) + } + } + ) } repeat(3) { item { @@ -347,7 +358,10 @@ fun ChatMaterialsScreen( Spacer(modifier = Modifier.height(padding.calculateTopPadding())) } items(attachments) { item -> - ChatMaterialItem(item = item) + ChatMaterialItem( + item = item, + onClick = {} + ) } item { Spacer(modifier = Modifier.height(padding.calculateBottomPadding())) diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/navigation/ConversationsNavigation.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/navigation/ConversationsNavigation.kt index 20cb4e90..69977433 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/navigation/ConversationsNavigation.kt +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/navigation/ConversationsNavigation.kt @@ -16,6 +16,7 @@ object Conversations fun NavGraphBuilder.conversationsScreen( onError: (BaseError) -> Unit, onConversationItemClicked: (id: Int) -> Unit, + onPhotoClicked: (url: String) -> Unit, navController: NavController, ) { composable { @@ -25,6 +26,7 @@ fun NavGraphBuilder.conversationsScreen( ConversationsRoute( onError = onError, onConversationItemClicked = onConversationItemClicked, + onPhotoClicked = onPhotoClicked, viewModel = viewModel ) } diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationItem.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationItem.kt index 0bcc9dba..53b5c32f 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationItem.kt +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationItem.kt @@ -1,6 +1,5 @@ package dev.meloda.fast.conversations.presentation -import android.graphics.drawable.ColorDrawable import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.fadeIn @@ -8,6 +7,7 @@ import androidx.compose.animation.fadeOut import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement @@ -41,47 +41,27 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback -import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil.compose.AsyncImage -import dev.meloda.fast.common.model.UiImage import dev.meloda.fast.conversations.model.ConversationOption import dev.meloda.fast.conversations.model.UiConversation import dev.meloda.fast.ui.basic.ContentAlpha import dev.meloda.fast.ui.basic.LocalContentAlpha import dev.meloda.fast.ui.components.DotsFlashing +import dev.meloda.fast.ui.util.getImage +import dev.meloda.fast.ui.util.getResourcePainter import dev.meloda.fast.ui.util.getString import dev.meloda.fast.ui.R as UiR val BirthdayColor = Color(0xffb00b69) -@Composable -fun UiImage.getResourcePainter(): Painter? { - return when (this) { - is UiImage.Resource -> painterResource(id = resId) - else -> null - } -} - -@Composable -fun UiImage.getImage(): Any { - return when (this) { - is UiImage.Color -> ColorDrawable(color) - is UiImage.ColorResource -> ColorDrawable(colorResource(id = resId).toArgb()) - is UiImage.Resource -> painterResource(id = resId) - is UiImage.Simple -> drawable - is UiImage.Url -> url - } -} - @OptIn(ExperimentalFoundationApi::class) @Composable fun ConversationItem( @@ -92,6 +72,7 @@ fun ConversationItem( isUserAccount: Boolean, conversation: UiConversation, modifier: Modifier = Modifier, + onPhotoClicked: (url: String) -> Unit ) { val context = LocalContext.current val hapticFeedback = LocalHapticFeedback.current @@ -174,7 +155,12 @@ fun ConversationItem( contentDescription = "Avatar", modifier = Modifier .fillMaxSize() - .clip(CircleShape), + .clip(CircleShape) + .clickable { + if (avatarImage is String) { + onPhotoClicked(avatarImage) + } + }, placeholder = painterResource(id = UiR.drawable.ic_account_circle_cut) ) } diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsList.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsList.kt index e8ff7f0b..15bd9180 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsList.kt +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsList.kt @@ -32,7 +32,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @Composable -fun ConversationsListComposable( +fun ConversationsList( onConversationsClick: (Int) -> Unit, onConversationsLongClick: (UiConversation) -> Unit, screenState: ConversationsScreenState, @@ -40,7 +40,8 @@ fun ConversationsListComposable( maxLines: Int, modifier: Modifier, onOptionClicked: (UiConversation, ConversationOption) -> Unit, - padding: PaddingValues + padding: PaddingValues, + onPhotoClicked: (url: String) -> Unit ) { val coroutineScope = rememberCoroutineScope() @@ -72,7 +73,8 @@ fun ConversationsListComposable( maxLines = maxLines, isUserAccount = isUserAccount, conversation = conversation, - modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null) + modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null), + onPhotoClicked = onPhotoClicked ) Spacer(modifier = Modifier.height(8.dp)) diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt index bbd37b1c..58fc683b 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt @@ -66,6 +66,10 @@ import androidx.core.view.HapticFeedbackConstantsCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.imageLoader import coil.request.ImageRequest +import dev.chrisbanes.haze.haze +import dev.chrisbanes.haze.hazeChild +import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi +import dev.chrisbanes.haze.materials.HazeMaterials import dev.meloda.fast.conversations.ConversationsViewModel import dev.meloda.fast.conversations.ConversationsViewModelImpl import dev.meloda.fast.conversations.model.ConversationOption @@ -80,10 +84,6 @@ import dev.meloda.fast.ui.theme.LocalBottomPadding import dev.meloda.fast.ui.theme.LocalHazeState import dev.meloda.fast.ui.theme.LocalThemeConfig import dev.meloda.fast.ui.util.isScrollingUp -import dev.chrisbanes.haze.haze -import dev.chrisbanes.haze.hazeChild -import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi -import dev.chrisbanes.haze.materials.HazeMaterials import kotlinx.coroutines.launch import org.koin.androidx.compose.koinViewModel import org.koin.compose.koinInject @@ -93,6 +93,7 @@ import dev.meloda.fast.ui.R as UiR fun ConversationsRoute( onError: (BaseError) -> Unit, onConversationItemClicked: (conversationId: Int) -> Unit, + onPhotoClicked: (url: String) -> Unit, viewModel: ConversationsViewModel = koinViewModel() ) { val context = LocalContext.current @@ -130,7 +131,8 @@ fun ConversationsRoute( onOptionClicked = viewModel::onOptionClicked, onPaginationConditionsMet = viewModel::onPaginationConditionsMet, onRefreshDropdownItemClicked = viewModel::onRefresh, - onRefresh = viewModel::onRefresh + onRefresh = viewModel::onRefresh, + onPhotoClicked = onPhotoClicked ) @@ -156,7 +158,8 @@ fun ConversationsScreen( onOptionClicked: (UiConversation, ConversationOption) -> Unit = { _, _ -> }, onPaginationConditionsMet: () -> Unit = {}, onRefreshDropdownItemClicked: () -> Unit = {}, - onRefresh: () -> Unit = {} + onRefresh: () -> Unit = {}, + onPhotoClicked: (url: String) -> Unit = {} ) { val view = LocalView.current val currentTheme = LocalThemeConfig.current @@ -349,7 +352,7 @@ fun ConversationsScreen( } else Modifier ) ) { - ConversationsListComposable( + ConversationsList( onConversationsClick = onConversationItemClicked, onConversationsLongClick = onConversationItemLongClicked, screenState = screenState, @@ -364,7 +367,8 @@ fun ConversationsScreen( Modifier }.fillMaxSize(), onOptionClicked = onOptionClicked, - padding = padding + padding = padding, + onPhotoClicked = onPhotoClicked ) if (enablePullToRefresh) { diff --git a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/navigation/FriendsNavigation.kt b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/navigation/FriendsNavigation.kt index 301398f8..ca6896a7 100644 --- a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/navigation/FriendsNavigation.kt +++ b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/navigation/FriendsNavigation.kt @@ -15,7 +15,8 @@ object Friends fun NavGraphBuilder.friendsScreen( onError: (BaseError) -> Unit, - navController: NavController + navController: NavController, + onPhotoClicked: (url: String) -> Unit ) { composable { val viewModel: FriendsViewModel = @@ -23,7 +24,8 @@ fun NavGraphBuilder.friendsScreen( FriendsRoute( onError = onError, - viewModel = viewModel + viewModel = viewModel, + onPhotoClicked = onPhotoClicked ) } } diff --git a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendItem.kt b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendItem.kt index 29e0fc0b..154eef5c 100644 --- a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendItem.kt +++ b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendItem.kt @@ -2,6 +2,7 @@ package dev.meloda.fast.friends.presentation import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -18,22 +19,20 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil.compose.AsyncImage -import dev.meloda.fast.ui.R import dev.meloda.fast.friends.model.UiFriend +import dev.meloda.fast.ui.R @Composable fun FriendItem( modifier: Modifier = Modifier, friend: UiFriend, - maxLines: Int + maxLines: Int, + onPhotoClicked: (url: String) -> Unit ) { - val context = LocalContext.current - Row( modifier = modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically @@ -58,7 +57,8 @@ fun FriendItem( contentDescription = null, modifier = Modifier .fillMaxSize() - .clip(CircleShape), + .clip(CircleShape) + .clickable { onPhotoClicked(friendAvatar) }, placeholder = painterResource(id = R.drawable.ic_account_circle_cut) ) } diff --git a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsList.kt b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsList.kt index 6098937c..8b0d8a70 100644 --- a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsList.kt +++ b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsList.kt @@ -34,7 +34,8 @@ fun FriendsList( uiFriends: ImmutableList, listState: LazyListState, maxLines: Int, - padding: PaddingValues + padding: PaddingValues, + onPhotoClicked: (url: String) -> Unit ) { val coroutineScope = rememberCoroutineScope() @@ -58,7 +59,8 @@ fun FriendsList( FriendItem( friend = friend, - maxLines = maxLines + maxLines = maxLines, + onPhotoClicked = onPhotoClicked ) Spacer(modifier = Modifier.height(16.dp)) diff --git a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsScreen.kt b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsScreen.kt index 1b10b855..3e920a18 100644 --- a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsScreen.kt +++ b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsScreen.kt @@ -49,6 +49,10 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.imageLoader import coil.request.ImageRequest +import dev.chrisbanes.haze.haze +import dev.chrisbanes.haze.hazeChild +import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi +import dev.chrisbanes.haze.materials.HazeMaterials import dev.meloda.fast.datastore.UserSettings import dev.meloda.fast.friends.FriendsViewModel import dev.meloda.fast.friends.FriendsViewModelImpl @@ -61,10 +65,6 @@ import dev.meloda.fast.ui.model.TabItem import dev.meloda.fast.ui.theme.LocalHazeState import dev.meloda.fast.ui.theme.LocalThemeConfig import dev.meloda.fast.ui.util.ImmutableList -import dev.chrisbanes.haze.haze -import dev.chrisbanes.haze.hazeChild -import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi -import dev.chrisbanes.haze.materials.HazeMaterials import org.koin.androidx.compose.koinViewModel import org.koin.compose.koinInject import dev.meloda.fast.ui.R as UiR @@ -72,6 +72,7 @@ import dev.meloda.fast.ui.R as UiR @Composable fun FriendsRoute( onError: (BaseError) -> Unit, + onPhotoClicked: (url: String) -> Unit, viewModel: FriendsViewModel = koinViewModel() ) { val context = LocalContext.current @@ -102,7 +103,8 @@ fun FriendsRoute( canPaginate = canPaginate, onSessionExpiredLogOutButtonClicked = { onError(BaseError.SessionExpired) }, onPaginationConditionsMet = viewModel::onPaginationConditionsMet, - onRefresh = viewModel::onRefresh + onRefresh = viewModel::onRefresh, + onPhotoClicked = onPhotoClicked ) } @@ -120,7 +122,8 @@ fun FriendsScreen( canPaginate: Boolean = false, onSessionExpiredLogOutButtonClicked: () -> Unit = {}, onPaginationConditionsMet: () -> Unit = {}, - onRefresh: () -> Unit = {} + onRefresh: () -> Unit = {}, + onPhotoClicked: (url: String) -> Unit = {} ) { val currentTheme = LocalThemeConfig.current @@ -307,7 +310,8 @@ fun FriendsScreen( uiFriends = ImmutableList.copyOf(friendsToDisplay), listState = listState, maxLines = maxLines, - padding = padding + padding = padding, + onPhotoClicked = onPhotoClicked ) if (friendsToDisplay.isEmpty()) { diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt index dee4ce74..426bf734 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt @@ -169,7 +169,7 @@ fun MessagesHistoryScreen( mutableStateOf(false) } - val hazeSate = remember { HazeState() } + val hazeState = remember { HazeState() } var animationsEnabled by remember { mutableStateOf( @@ -202,7 +202,7 @@ fun MessagesHistoryScreen( .then( if (currentTheme.enableBlur) { Modifier.hazeChild( - state = hazeSate, + state = hazeState, style = HazeMaterials.thick() ) } else Modifier @@ -312,7 +312,7 @@ fun MessagesHistoryScreen( .padding(bottom = padding.calculateBottomPadding()), ) { MessagesList( - hazeState = hazeSate, + hazeState = hazeState, listState = listState, immutableMessages = ImmutableList.copyOf(screenState.messages), isPaginating = screenState.isPaginating, diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/util/Ext.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/util/Ext.kt index 7cbab6cf..de916b08 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/util/Ext.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/util/Ext.kt @@ -2,7 +2,7 @@ package dev.meloda.fast.messageshistory.util import dev.meloda.fast.messageshistory.model.UiItem -fun List.firstMessage(): UiItem.Message = first() as UiItem.Message +fun List.firstMessage(): UiItem.Message = filterIsInstance().first() fun List.indexOfMessageById(messageId: Int): Int = indexOfFirst { it.id == messageId } diff --git a/feature/photoviewer/build.gradle.kts b/feature/photoviewer/build.gradle.kts index c669bf19..94f6c362 100644 --- a/feature/photoviewer/build.gradle.kts +++ b/feature/photoviewer/build.gradle.kts @@ -2,6 +2,8 @@ plugins { alias(libs.plugins.android.library) alias(libs.plugins.org.jetbrains.kotlin.android) alias(libs.plugins.kotlin.compose.compiler) + alias(libs.plugins.org.jetbrains.kotlin.plugin.parcelize) + alias(libs.plugins.kotlin.serialization) } group = "dev.meloda.fast.photoviewer" @@ -52,4 +54,12 @@ dependencies { implementation(libs.bundles.compose) implementation(libs.coil.compose) + + implementation(libs.haze) + implementation(libs.haze.materials) + + debugImplementation(libs.compose.ui.tooling) + + implementation(libs.androidx.navigation.compose) + implementation(libs.kotlin.serialization) } diff --git a/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/PhotoViewViewModel.kt b/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/PhotoViewViewModel.kt index 2f54aae5..193a0d32 100644 --- a/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/PhotoViewViewModel.kt +++ b/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/PhotoViewViewModel.kt @@ -1,22 +1,34 @@ package dev.meloda.fast.photoviewer +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import dev.meloda.fast.common.extensions.setValue -import dev.meloda.fast.photoviewer.model.PhotoViewArguments -import dev.meloda.fast.photoviewer.model.PhotoViewState +import dev.meloda.fast.common.model.UiImage +import dev.meloda.fast.photoviewer.model.PhotoViewScreenState +import dev.meloda.fast.photoviewer.navigation.PhotoView import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import java.net.URLDecoder interface PhotoViewViewModel { - val state: StateFlow - - fun setArguments(arguments: PhotoViewArguments) + val screenState: StateFlow } -class PhotoViewViewModelImpl : PhotoViewViewModel, ViewModel() { - override val state = MutableStateFlow(PhotoViewState.EMPTY) +class PhotoViewViewModelImpl( + savedStateHandle: SavedStateHandle +) : PhotoViewViewModel, ViewModel() { - override fun setArguments(arguments: PhotoViewArguments) { - state.setValue { old -> old.copy(images = arguments.images) } + override val screenState = MutableStateFlow(PhotoViewScreenState.EMPTY) + + init { + val arguments = PhotoView.from(savedStateHandle).arguments + + screenState.setValue { old -> + old.copy( + images = arguments.images + .map { URLDecoder.decode(it, "utf-8") } + .map(UiImage::Url) + ) + } } } diff --git a/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/model/PhotoViewArguments.kt b/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/model/PhotoViewArguments.kt index 30e5963b..dfecfe8c 100644 --- a/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/model/PhotoViewArguments.kt +++ b/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/model/PhotoViewArguments.kt @@ -1,9 +1,11 @@ package dev.meloda.fast.photoviewer.model -import androidx.compose.runtime.Immutable -import dev.meloda.fast.common.model.UiImage +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable -@Immutable +@Parcelize +@Serializable data class PhotoViewArguments( - val images: List -) + val images: List +) : Parcelable diff --git a/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/model/PhotoViewState.kt b/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/model/PhotoViewScreenState.kt similarity index 72% rename from feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/model/PhotoViewState.kt rename to feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/model/PhotoViewScreenState.kt index 6ec96ea4..112ded2d 100644 --- a/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/model/PhotoViewState.kt +++ b/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/model/PhotoViewScreenState.kt @@ -4,12 +4,12 @@ import androidx.compose.runtime.Immutable import dev.meloda.fast.common.model.UiImage @Immutable -data class PhotoViewState( +data class PhotoViewScreenState( val images: List ) { companion object { - val EMPTY: PhotoViewState = PhotoViewState( + val EMPTY: PhotoViewScreenState = PhotoViewScreenState( images = emptyList() ) } diff --git a/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/navigation/PhotoViewNavigation.kt b/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/navigation/PhotoViewNavigation.kt new file mode 100644 index 00000000..e216e95f --- /dev/null +++ b/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/navigation/PhotoViewNavigation.kt @@ -0,0 +1,41 @@ +package dev.meloda.fast.photoviewer.navigation + +import androidx.lifecycle.SavedStateHandle +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import androidx.navigation.toRoute +import dev.meloda.fast.common.extensions.customNavType +import dev.meloda.fast.photoviewer.model.PhotoViewArguments +import dev.meloda.fast.photoviewer.presentation.PhotoViewRoute +import kotlinx.serialization.Serializable +import java.net.URLEncoder +import kotlin.reflect.typeOf + +@Serializable +data class PhotoView(val arguments: PhotoViewArguments) { + companion object { + val typeMap = mapOf(typeOf() to customNavType()) + + fun from(savedStateHandle: SavedStateHandle) = + savedStateHandle.toRoute(typeMap) + } +} + +fun NavGraphBuilder.photoViewScreen( + onBack: () -> Unit +) { + composable(typeMap = PhotoView.typeMap) { + PhotoViewRoute(onBack = onBack) + } +} + +fun NavController.navigateToPhotoView(images: List) { + this.navigate( + PhotoView( + arguments = PhotoViewArguments( + images.map { URLEncoder.encode(it, "utf-8") } + ) + ) + ) +} diff --git a/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/presentation/PhotoViewScreen.kt b/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/presentation/PhotoViewScreen.kt new file mode 100644 index 00000000..87f6da87 --- /dev/null +++ b/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/presentation/PhotoViewScreen.kt @@ -0,0 +1,249 @@ +package dev.meloda.fast.photoviewer.presentation + +import android.util.Log +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.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 +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.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.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +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.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 +import dev.meloda.fast.common.model.UiImage +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 org.koin.androidx.compose.koinViewModel +import kotlin.math.PI +import kotlin.math.abs +import kotlin.math.cos +import kotlin.math.sin +import dev.meloda.fast.ui.R as UiR + +@Composable +fun PhotoViewRoute( + onBack: () -> Unit, + viewModel: PhotoViewViewModel = koinViewModel() +) { + val screenState by viewModel.screenState.collectAsStateWithLifecycle() + + PhotoViewScreen( + screenState = screenState, + onBack = onBack + ) +} + +@Composable +fun PhotoViewScreen( + screenState: PhotoViewScreenState = PhotoViewScreenState.EMPTY, + onBack: () -> Unit = {} +) { + val pagerState = rememberPagerState(pageCount = { screenState.images.size }) + + var offsetY by remember { mutableFloatStateOf(0f) } + + val calculatedAlpha by remember(offsetY) { + derivedStateOf { + val absoluteOffset = abs(offsetY) + + 1 - if (absoluteOffset >= 1700) { + 0.85f + } else absoluteOffset / 2000 + } + } + + Scaffold( + modifier = Modifier.graphicsLayer(alpha = calculatedAlpha), + topBar = { TopBar(onBack = onBack) }, + containerColor = MaterialTheme.colorScheme.background.copy( + alpha = calculatedAlpha + ) + ) { padding -> + Column(modifier = Modifier.fillMaxSize()) { + Pager( + pagerState = pagerState, + state = screenState, + padding = padding, + onBack = onBack, + onVerticalDrag = { offset -> offsetY = offset }, + modifier = Modifier + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TopBar( + modifier: Modifier = Modifier, + onBack: () -> Unit +) { + val context = LocalContext.current + + var dropdownMenuShown by remember { + mutableStateOf(false) + } + + TopAppBar( + modifier = modifier, + title = {}, + navigationIcon = { + IconButton(onClick = onBack) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.ArrowBack, + contentDescription = "Back button" + ) + } + }, + actions = { + 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") }, + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent) + ) +} + +@Composable +fun Pager( + modifier: Modifier = Modifier, + pagerState: PagerState, + state: PhotoViewScreenState, + padding: PaddingValues, + onBack: () -> Unit, + onVerticalDrag: (offset: Float) -> Unit +) { + HorizontalPager( + state = pagerState, + modifier = modifier.fillMaxSize() + ) { page -> + val model = state.images[page].getImage() + + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + if (model is Painter) { + Image( + painter = model, + contentDescription = "Image", + modifier = Modifier.fillMaxSize() + ) + } else { + var offsetY by remember { mutableFloatStateOf(0f) } + + val animatedOffset by animateFloatAsState( + targetValue = offsetY, + label = "animatedOffset" + ) + var useAnimatedOffset by remember { + mutableStateOf(false) + } + + AsyncImage( + model = model, + contentDescription = "Image", + modifier = Modifier + .graphicsLayer { + this.translationY = if (useAnimatedOffset) { + animatedOffset + } else offsetY + } + .draggable( + state = rememberDraggableState { delta -> + useAnimatedOffset = false + offsetY += delta + onVerticalDrag(offsetY) + }, + orientation = Orientation.Vertical, + onDragStopped = { + if (abs(offsetY.pxToDp()) >= 200) { + onBack() + } else { + useAnimatedOffset = true + offsetY = 0f + onVerticalDrag(0f) + } + } + ) + .fillMaxSize(), + placeholder = ColorPainter(Color.DarkGray), + error = ColorPainter(Color.Red) + ) + } + } + } +} + +@Preview +@Composable +private fun PhotoViewScreenPreview() { + PhotoViewScreen( + screenState = PhotoViewScreenState( + images = List(200) { + UiImage.Resource(UiR.drawable.test_captcha) + } + ) + ) +} diff --git a/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/presentation/PhotoViewScreenContent.kt b/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/presentation/PhotoViewScreenContent.kt deleted file mode 100644 index da8955d4..00000000 --- a/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/presentation/PhotoViewScreenContent.kt +++ /dev/null @@ -1,177 +0,0 @@ -package dev.meloda.fast.photoviewer.presentation - -import android.graphics.drawable.ColorDrawable -import android.widget.Toast -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.foundation.pager.HorizontalPager -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.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.painter.ColorPainter -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.DpOffset -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import coil.compose.AsyncImage -import dev.meloda.fast.common.model.UiImage -import dev.meloda.fast.photoviewer.PhotoViewViewModel -import dev.meloda.fast.photoviewer.model.PhotoViewState - -@Composable -fun PhotoViewScreenContent( - onBackClick: () -> Unit, - viewModel: PhotoViewViewModel -) { - val state by viewModel.state.collectAsStateWithLifecycle() - val images = state.images - - val pagerState = rememberPagerState(pageCount = { images.size }) - - // TODO: 23/11/2023, Danil Nikolaev: заюзать штуку для цветов статус бара и навбара - Box( - modifier = Modifier.fillMaxSize() - ) { - Column( - modifier = Modifier - .fillMaxSize() - .background(Color(0xffb00b69)) - ) { - Spacer( - modifier = Modifier - .statusBarsPadding() - .padding(top = 56.dp) - ) - Pager( - pagerState = pagerState, - state = state - ) - Spacer(modifier = Modifier.navigationBarsPadding()) - } - - AppBar(onBackClick = onBackClick) - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun AppBar(onBackClick: () -> Unit) { - val context = LocalContext.current - - var dropdownMenuShown by remember { - mutableStateOf(false) - } - - TopAppBar( - title = {}, - navigationIcon = { - IconButton(onClick = onBackClick) { - Icon( - imageVector = Icons.AutoMirrored.Rounded.ArrowBack, - contentDescription = "Back button" - ) - } - }, - actions = { - 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") }, - ) - } - } - ) -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun Pager( - pagerState: PagerState, - padding: PaddingValues = PaddingValues(0.dp), - state: PhotoViewState -) { - val images = state.images - - HorizontalPager( - state = pagerState, - modifier = Modifier - .fillMaxSize() - .padding(padding), - key = { index -> images[index].hashCode() } - ) { page -> - val model = images[page].getImage() - if (model is Painter) { - Image( - painter = model, - contentDescription = "Image", - modifier = Modifier.fillMaxSize() - ) - } else { - AsyncImage( - model = model, - contentDescription = "Image", - modifier = Modifier.fillMaxSize(), - placeholder = ColorPainter(Color.DarkGray), - error = ColorPainter(Color.Red) - ) - } - } -} - -@Composable -fun UiImage.getImage(): Any { - return when (this) { - is UiImage.Color -> ColorDrawable(color) - is UiImage.ColorResource -> ColorDrawable(colorResource(id = resId).toArgb()) - is UiImage.Resource -> painterResource(id = resId) - is UiImage.Simple -> drawable - is UiImage.Url -> url - } -} diff --git a/feature/profile/src/main/kotlin/dev/meloda/fast/profile/navigation/ProfileRoute.kt b/feature/profile/src/main/kotlin/dev/meloda/fast/profile/navigation/ProfileRoute.kt index 4a2c6c32..97c7e75a 100644 --- a/feature/profile/src/main/kotlin/dev/meloda/fast/profile/navigation/ProfileRoute.kt +++ b/feature/profile/src/main/kotlin/dev/meloda/fast/profile/navigation/ProfileRoute.kt @@ -16,6 +16,7 @@ object Profile fun NavGraphBuilder.profileScreen( onError: (BaseError) -> Unit, onSettingsButtonClicked: () -> Unit, + onPhotoClicked: (url: String) -> Unit, navController: NavController ) { composable { @@ -25,6 +26,7 @@ fun NavGraphBuilder.profileScreen( ProfileRoute( onError = onError, onSettingsButtonClicked = onSettingsButtonClicked, + onPhotoClicked = onPhotoClicked, viewModel = viewModel ) } diff --git a/feature/profile/src/main/kotlin/dev/meloda/fast/profile/presentation/ProfileScreen.kt b/feature/profile/src/main/kotlin/dev/meloda/fast/profile/presentation/ProfileScreen.kt index 87b655cd..2dc74311 100644 --- a/feature/profile/src/main/kotlin/dev/meloda/fast/profile/presentation/ProfileScreen.kt +++ b/feature/profile/src/main/kotlin/dev/meloda/fast/profile/presentation/ProfileScreen.kt @@ -1,5 +1,6 @@ package dev.meloda.fast.profile.presentation +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -46,6 +47,7 @@ import dev.meloda.fast.ui.R as UiR fun ProfileRoute( onError: (BaseError) -> Unit, onSettingsButtonClicked: () -> Unit, + onPhotoClicked: (url: String) -> Unit, viewModel: ProfileViewModel = koinViewModel() ) { val screenState by viewModel.screenState.collectAsStateWithLifecycle() @@ -54,8 +56,8 @@ fun ProfileRoute( ProfileScreen( screenState = screenState, baseError = baseError, - onSettingsButtonClicked = onSettingsButtonClicked - + onSettingsButtonClicked = onSettingsButtonClicked, + onPhotoClicked = onPhotoClicked ) } @@ -66,6 +68,7 @@ fun ProfileScreen( screenState: ProfileScreenState = ProfileScreenState.EMPTY, baseError: BaseError? = null, onSettingsButtonClicked: () -> Unit = {}, + onPhotoClicked: (url: String) -> Unit = {} ) { Scaffold( topBar = { @@ -105,7 +108,10 @@ fun ProfileScreen( AsyncImage( modifier = Modifier .size(120.dp) - .clip(CircleShape), + .clip(CircleShape) + .clickable { + onPhotoClicked(screenState.avatarUrl.orEmpty()) + }, model = screenState.avatarUrl, contentDescription = null, contentScale = ContentScale.Crop,