simple photo viewer

This commit is contained in:
2024-07-16 10:29:37 +03:00
parent 9e09cbb640
commit 1817698031
27 changed files with 484 additions and 278 deletions
+1
View File
@@ -129,6 +129,7 @@ dependencies {
implementation(projects.feature.settings) implementation(projects.feature.settings)
implementation(projects.feature.friends) implementation(projects.feature.friends)
implementation(projects.feature.profile) implementation(projects.feature.profile)
implementation(projects.feature.photoviewer)
implementation(projects.core.common) implementation(projects.core.common)
implementation(projects.core.ui) implementation(projects.core.ui)
@@ -21,6 +21,7 @@ fun NavGraphBuilder.mainScreen(
onError: (BaseError) -> Unit, onError: (BaseError) -> Unit,
onSettingsButtonClicked: () -> Unit, onSettingsButtonClicked: () -> Unit,
onConversationClicked: (conversationId: Int) -> Unit, onConversationClicked: (conversationId: Int) -> Unit,
onPhotoClicked: (url: String) -> Unit
) { ) {
val navigationItems = listOf( val navigationItems = listOf(
BottomNavigationItem( BottomNavigationItem(
@@ -49,6 +50,7 @@ fun NavGraphBuilder.mainScreen(
onError = onError, onError = onError,
onSettingsButtonClicked = onSettingsButtonClicked, onSettingsButtonClicked = onSettingsButtonClicked,
onConversationItemClicked = onConversationClicked, onConversationItemClicked = onConversationClicked,
onPhotoClicked = onPhotoClicked
) )
} }
} }
@@ -25,19 +25,19 @@ import androidx.compose.ui.unit.dp
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.navigation import androidx.navigation.compose.navigation
import androidx.navigation.compose.rememberNavController 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.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.friends.navigation.friendsScreen
import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.BaseError
import dev.meloda.fast.model.BottomNavigationItem import dev.meloda.fast.model.BottomNavigationItem
import dev.meloda.fast.navigation.MainGraph import dev.meloda.fast.navigation.MainGraph
import dev.meloda.fast.profile.navigation.profileScreen import dev.meloda.fast.profile.navigation.profileScreen
import dev.chrisbanes.haze.HazeState import dev.meloda.fast.ui.theme.LocalBottomPadding
import dev.chrisbanes.haze.hazeChild import dev.meloda.fast.ui.theme.LocalHazeState
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.meloda.fast.ui.theme.LocalThemeConfig
import dev.chrisbanes.haze.materials.HazeMaterials
@OptIn(ExperimentalHazeMaterialsApi::class) @OptIn(ExperimentalHazeMaterialsApi::class)
@Composable @Composable
@@ -45,7 +45,8 @@ fun MainScreen(
navigationItems: List<BottomNavigationItem>, navigationItems: List<BottomNavigationItem>,
onError: (BaseError) -> Unit = {}, onError: (BaseError) -> Unit = {},
onSettingsButtonClicked: () -> Unit = {}, onSettingsButtonClicked: () -> Unit = {},
onConversationItemClicked: (conversationId: Int) -> Unit = {} onConversationItemClicked: (conversationId: Int) -> Unit = {},
onPhotoClicked: (url: String) -> Unit = {}
) { ) {
val currentTheme = LocalThemeConfig.current val currentTheme = LocalThemeConfig.current
val hazeState = remember { HazeState() } val hazeState = remember { HazeState() }
@@ -120,16 +121,19 @@ fun MainScreen(
navigation<MainGraph>(startDestination = navigationItems[selectedItemIndex].route) { navigation<MainGraph>(startDestination = navigationItems[selectedItemIndex].route) {
friendsScreen( friendsScreen(
onError = onError, onError = onError,
navController = navController navController = navController,
onPhotoClicked = onPhotoClicked
) )
conversationsScreen( conversationsScreen(
onError = onError, onError = onError,
onConversationItemClicked = onConversationItemClicked, onConversationItemClicked = onConversationItemClicked,
navController = navController navController = navController,
onPhotoClicked = onPhotoClicked
) )
profileScreen( profileScreen(
onError = onError, onError = onError,
onSettingsButtonClicked = onSettingsButtonClicked, onSettingsButtonClicked = onSettingsButtonClicked,
onPhotoClicked = onPhotoClicked,
navController = navController navController = navController
) )
} }
@@ -26,15 +26,17 @@ import dev.meloda.fast.auth.authNavGraph
import dev.meloda.fast.auth.navigateToAuth import dev.meloda.fast.auth.navigateToAuth
import dev.meloda.fast.chatmaterials.navigation.chatMaterialsScreen import dev.meloda.fast.chatmaterials.navigation.chatMaterialsScreen
import dev.meloda.fast.chatmaterials.navigation.navigateToChatMaterials 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.languagePickerScreen
import dev.meloda.fast.languagepicker.navigation.navigateToLanguagePicker import dev.meloda.fast.languagepicker.navigation.navigateToLanguagePicker
import dev.meloda.fast.messageshistory.navigation.messagesHistoryScreen 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.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 org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
@Composable @Composable
@@ -122,7 +124,8 @@ fun RootScreen(
mainScreen( mainScreen(
onError = viewModel::onError, onError = viewModel::onError,
onSettingsButtonClicked = navController::navigateToSettings, onSettingsButtonClicked = navController::navigateToSettings,
onConversationClicked = navController::navigateToMessagesHistory onConversationClicked = navController::navigateToMessagesHistory,
onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) }
) )
messagesHistoryScreen( messagesHistoryScreen(
@@ -131,7 +134,8 @@ fun RootScreen(
onChatMaterialsDropdownItemClicked = navController::navigateToChatMaterials onChatMaterialsDropdownItemClicked = navController::navigateToChatMaterials
) )
chatMaterialsScreen( chatMaterialsScreen(
onBack = navController::navigateUp onBack = navController::navigateUp,
onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) }
) )
settingsScreen( settingsScreen(
@@ -140,6 +144,8 @@ fun RootScreen(
onLanguageItemClicked = navController::navigateToLanguagePicker onLanguageItemClicked = navController::navigateToLanguagePicker
) )
languagePickerScreen(onBack = navController::navigateUp) languagePickerScreen(onBack = navController::navigateUp)
photoViewScreen(onBack = navController::navigateUp)
} }
} }
} }
@@ -1,6 +1,7 @@
package dev.meloda.fast.ui.util package dev.meloda.fast.ui.util
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.drawable.ColorDrawable
import android.os.PowerManager import android.os.PowerManager
import android.view.KeyEvent import android.view.KeyEvent
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
@@ -11,12 +12,17 @@ import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier 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.input.key.onKeyEvent
import androidx.compose.ui.platform.LocalContext 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.pluralStringResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import dev.meloda.fast.common.model.DarkMode import dev.meloda.fast.common.model.DarkMode
import dev.meloda.fast.common.model.UiImage
import dev.meloda.fast.common.model.UiText import dev.meloda.fast.common.model.UiText
@Composable @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( fun Modifier.handleTabKey(
action: () -> Boolean action: () -> Boolean
): Modifier = this.onKeyEvent { event -> ): Modifier = this.onKeyEvent { event ->
@@ -20,10 +20,14 @@ data class ChatMaterials(
} }
fun NavGraphBuilder.chatMaterialsScreen( fun NavGraphBuilder.chatMaterialsScreen(
onBack: () -> Unit onBack: () -> Unit,
onPhotoClicked: (url: String) -> Unit
) { ) {
composable<ChatMaterials> { composable<ChatMaterials> {
ChatMaterialsRoute(onBack = onBack) ChatMaterialsRoute(
onBack = onBack,
onPhotoClicked = onPhotoClicked
)
} }
} }
@@ -14,7 +14,10 @@ import coil.compose.AsyncImage
import dev.meloda.fast.chatmaterials.model.UiChatMaterial import dev.meloda.fast.chatmaterials.model.UiChatMaterial
@Composable @Composable
fun ChatMaterialItem(item: UiChatMaterial) { fun ChatMaterialItem(
item: UiChatMaterial,
onClick: () -> Unit
) {
when (item) { when (item) {
is UiChatMaterial.Photo -> { is UiChatMaterial.Photo -> {
AsyncImage( AsyncImage(
@@ -61,23 +61,25 @@ import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle 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.HazeState
import dev.chrisbanes.haze.haze import dev.chrisbanes.haze.haze
import dev.chrisbanes.haze.hazeChild import dev.chrisbanes.haze.hazeChild
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials 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.androidx.compose.koinViewModel
import org.koin.compose.koinInject import org.koin.compose.koinInject
@Composable @Composable
fun ChatMaterialsRoute( fun ChatMaterialsRoute(
onBack: () -> Unit, onBack: () -> Unit,
onPhotoClicked: (url: String) -> Unit,
viewModel: ChatMaterialsViewModel = koinViewModel<ChatMaterialsViewModelImpl>() viewModel: ChatMaterialsViewModel = koinViewModel<ChatMaterialsViewModelImpl>()
) { ) {
val userSettings: UserSettings = koinInject() val userSettings: UserSettings = koinInject()
@@ -92,7 +94,8 @@ fun ChatMaterialsRoute(
onBack = onBack, onBack = onBack,
onTypeChanged = viewModel::onTypeChanged, onTypeChanged = viewModel::onTypeChanged,
onRefreshDropdownItemClicked = viewModel::onRefresh, onRefreshDropdownItemClicked = viewModel::onRefresh,
onRefresh = viewModel::onRefresh onRefresh = viewModel::onRefresh,
onPhotoClicked = onPhotoClicked
) )
} }
@@ -108,7 +111,8 @@ fun ChatMaterialsScreen(
onBack: () -> Unit = {}, onBack: () -> Unit = {},
onTypeChanged: (String) -> Unit = {}, onTypeChanged: (String) -> Unit = {},
onRefreshDropdownItemClicked: () -> Unit = {}, onRefreshDropdownItemClicked: () -> Unit = {},
onRefresh: () -> Unit = {} onRefresh: () -> Unit = {},
onPhotoClicked: (url: String) -> Unit = {}
) { ) {
val currentTheme = LocalThemeConfig.current val currentTheme = LocalThemeConfig.current
@@ -318,7 +322,14 @@ fun ChatMaterialsScreen(
} }
} }
items(attachments) { item -> items(attachments) { item ->
ChatMaterialItem(item = item) ChatMaterialItem(
item = item,
onClick = {
if (item is UiChatMaterial.Photo) {
onPhotoClicked(item.previewUrl)
}
}
)
} }
repeat(3) { repeat(3) {
item { item {
@@ -347,7 +358,10 @@ fun ChatMaterialsScreen(
Spacer(modifier = Modifier.height(padding.calculateTopPadding())) Spacer(modifier = Modifier.height(padding.calculateTopPadding()))
} }
items(attachments) { item -> items(attachments) { item ->
ChatMaterialItem(item = item) ChatMaterialItem(
item = item,
onClick = {}
)
} }
item { item {
Spacer(modifier = Modifier.height(padding.calculateBottomPadding())) Spacer(modifier = Modifier.height(padding.calculateBottomPadding()))
@@ -16,6 +16,7 @@ object Conversations
fun NavGraphBuilder.conversationsScreen( fun NavGraphBuilder.conversationsScreen(
onError: (BaseError) -> Unit, onError: (BaseError) -> Unit,
onConversationItemClicked: (id: Int) -> Unit, onConversationItemClicked: (id: Int) -> Unit,
onPhotoClicked: (url: String) -> Unit,
navController: NavController, navController: NavController,
) { ) {
composable<Conversations> { composable<Conversations> {
@@ -25,6 +26,7 @@ fun NavGraphBuilder.conversationsScreen(
ConversationsRoute( ConversationsRoute(
onError = onError, onError = onError,
onConversationItemClicked = onConversationItemClicked, onConversationItemClicked = onConversationItemClicked,
onPhotoClicked = onPhotoClicked,
viewModel = viewModel viewModel = viewModel
) )
} }
@@ -1,6 +1,5 @@
package dev.meloda.fast.conversations.presentation package dev.meloda.fast.conversations.presentation
import android.graphics.drawable.ColorDrawable
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
@@ -8,6 +7,7 @@ import androidx.compose.animation.fadeOut
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement 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.Color
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import dev.meloda.fast.common.model.UiImage
import dev.meloda.fast.conversations.model.ConversationOption import dev.meloda.fast.conversations.model.ConversationOption
import dev.meloda.fast.conversations.model.UiConversation import dev.meloda.fast.conversations.model.UiConversation
import dev.meloda.fast.ui.basic.ContentAlpha import dev.meloda.fast.ui.basic.ContentAlpha
import dev.meloda.fast.ui.basic.LocalContentAlpha import dev.meloda.fast.ui.basic.LocalContentAlpha
import dev.meloda.fast.ui.components.DotsFlashing 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.util.getString
import dev.meloda.fast.ui.R as UiR import dev.meloda.fast.ui.R as UiR
val BirthdayColor = Color(0xffb00b69) 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) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun ConversationItem( fun ConversationItem(
@@ -92,6 +72,7 @@ fun ConversationItem(
isUserAccount: Boolean, isUserAccount: Boolean,
conversation: UiConversation, conversation: UiConversation,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onPhotoClicked: (url: String) -> Unit
) { ) {
val context = LocalContext.current val context = LocalContext.current
val hapticFeedback = LocalHapticFeedback.current val hapticFeedback = LocalHapticFeedback.current
@@ -174,7 +155,12 @@ fun ConversationItem(
contentDescription = "Avatar", contentDescription = "Avatar",
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.clip(CircleShape), .clip(CircleShape)
.clickable {
if (avatarImage is String) {
onPhotoClicked(avatarImage)
}
},
placeholder = painterResource(id = UiR.drawable.ic_account_circle_cut) placeholder = painterResource(id = UiR.drawable.ic_account_circle_cut)
) )
} }
@@ -32,7 +32,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @Composable
fun ConversationsListComposable( fun ConversationsList(
onConversationsClick: (Int) -> Unit, onConversationsClick: (Int) -> Unit,
onConversationsLongClick: (UiConversation) -> Unit, onConversationsLongClick: (UiConversation) -> Unit,
screenState: ConversationsScreenState, screenState: ConversationsScreenState,
@@ -40,7 +40,8 @@ fun ConversationsListComposable(
maxLines: Int, maxLines: Int,
modifier: Modifier, modifier: Modifier,
onOptionClicked: (UiConversation, ConversationOption) -> Unit, onOptionClicked: (UiConversation, ConversationOption) -> Unit,
padding: PaddingValues padding: PaddingValues,
onPhotoClicked: (url: String) -> Unit
) { ) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
@@ -72,7 +73,8 @@ fun ConversationsListComposable(
maxLines = maxLines, maxLines = maxLines,
isUserAccount = isUserAccount, isUserAccount = isUserAccount,
conversation = conversation, conversation = conversation,
modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null) modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null),
onPhotoClicked = onPhotoClicked
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
@@ -66,6 +66,10 @@ import androidx.core.view.HapticFeedbackConstantsCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.imageLoader import coil.imageLoader
import coil.request.ImageRequest 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.ConversationsViewModel
import dev.meloda.fast.conversations.ConversationsViewModelImpl import dev.meloda.fast.conversations.ConversationsViewModelImpl
import dev.meloda.fast.conversations.model.ConversationOption 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.LocalHazeState
import dev.meloda.fast.ui.theme.LocalThemeConfig import dev.meloda.fast.ui.theme.LocalThemeConfig
import dev.meloda.fast.ui.util.isScrollingUp 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 kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
import org.koin.compose.koinInject import org.koin.compose.koinInject
@@ -93,6 +93,7 @@ import dev.meloda.fast.ui.R as UiR
fun ConversationsRoute( fun ConversationsRoute(
onError: (BaseError) -> Unit, onError: (BaseError) -> Unit,
onConversationItemClicked: (conversationId: Int) -> Unit, onConversationItemClicked: (conversationId: Int) -> Unit,
onPhotoClicked: (url: String) -> Unit,
viewModel: ConversationsViewModel = koinViewModel<ConversationsViewModelImpl>() viewModel: ConversationsViewModel = koinViewModel<ConversationsViewModelImpl>()
) { ) {
val context = LocalContext.current val context = LocalContext.current
@@ -130,7 +131,8 @@ fun ConversationsRoute(
onOptionClicked = viewModel::onOptionClicked, onOptionClicked = viewModel::onOptionClicked,
onPaginationConditionsMet = viewModel::onPaginationConditionsMet, onPaginationConditionsMet = viewModel::onPaginationConditionsMet,
onRefreshDropdownItemClicked = viewModel::onRefresh, onRefreshDropdownItemClicked = viewModel::onRefresh,
onRefresh = viewModel::onRefresh onRefresh = viewModel::onRefresh,
onPhotoClicked = onPhotoClicked
) )
@@ -156,7 +158,8 @@ fun ConversationsScreen(
onOptionClicked: (UiConversation, ConversationOption) -> Unit = { _, _ -> }, onOptionClicked: (UiConversation, ConversationOption) -> Unit = { _, _ -> },
onPaginationConditionsMet: () -> Unit = {}, onPaginationConditionsMet: () -> Unit = {},
onRefreshDropdownItemClicked: () -> Unit = {}, onRefreshDropdownItemClicked: () -> Unit = {},
onRefresh: () -> Unit = {} onRefresh: () -> Unit = {},
onPhotoClicked: (url: String) -> Unit = {}
) { ) {
val view = LocalView.current val view = LocalView.current
val currentTheme = LocalThemeConfig.current val currentTheme = LocalThemeConfig.current
@@ -349,7 +352,7 @@ fun ConversationsScreen(
} else Modifier } else Modifier
) )
) { ) {
ConversationsListComposable( ConversationsList(
onConversationsClick = onConversationItemClicked, onConversationsClick = onConversationItemClicked,
onConversationsLongClick = onConversationItemLongClicked, onConversationsLongClick = onConversationItemLongClicked,
screenState = screenState, screenState = screenState,
@@ -364,7 +367,8 @@ fun ConversationsScreen(
Modifier Modifier
}.fillMaxSize(), }.fillMaxSize(),
onOptionClicked = onOptionClicked, onOptionClicked = onOptionClicked,
padding = padding padding = padding,
onPhotoClicked = onPhotoClicked
) )
if (enablePullToRefresh) { if (enablePullToRefresh) {
@@ -15,7 +15,8 @@ object Friends
fun NavGraphBuilder.friendsScreen( fun NavGraphBuilder.friendsScreen(
onError: (BaseError) -> Unit, onError: (BaseError) -> Unit,
navController: NavController navController: NavController,
onPhotoClicked: (url: String) -> Unit
) { ) {
composable<Friends> { composable<Friends> {
val viewModel: FriendsViewModel = val viewModel: FriendsViewModel =
@@ -23,7 +24,8 @@ fun NavGraphBuilder.friendsScreen(
FriendsRoute( FriendsRoute(
onError = onError, onError = onError,
viewModel = viewModel viewModel = viewModel,
onPhotoClicked = onPhotoClicked
) )
} }
} }
@@ -2,6 +2,7 @@ package dev.meloda.fast.friends.presentation
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@@ -18,22 +19,20 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import dev.meloda.fast.ui.R
import dev.meloda.fast.friends.model.UiFriend import dev.meloda.fast.friends.model.UiFriend
import dev.meloda.fast.ui.R
@Composable @Composable
fun FriendItem( fun FriendItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
friend: UiFriend, friend: UiFriend,
maxLines: Int maxLines: Int,
onPhotoClicked: (url: String) -> Unit
) { ) {
val context = LocalContext.current
Row( Row(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
@@ -58,7 +57,8 @@ fun FriendItem(
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.clip(CircleShape), .clip(CircleShape)
.clickable { onPhotoClicked(friendAvatar) },
placeholder = painterResource(id = R.drawable.ic_account_circle_cut) placeholder = painterResource(id = R.drawable.ic_account_circle_cut)
) )
} }
@@ -34,7 +34,8 @@ fun FriendsList(
uiFriends: ImmutableList<UiFriend>, uiFriends: ImmutableList<UiFriend>,
listState: LazyListState, listState: LazyListState,
maxLines: Int, maxLines: Int,
padding: PaddingValues padding: PaddingValues,
onPhotoClicked: (url: String) -> Unit
) { ) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
@@ -58,7 +59,8 @@ fun FriendsList(
FriendItem( FriendItem(
friend = friend, friend = friend,
maxLines = maxLines maxLines = maxLines,
onPhotoClicked = onPhotoClicked
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
@@ -49,6 +49,10 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.imageLoader import coil.imageLoader
import coil.request.ImageRequest 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.datastore.UserSettings
import dev.meloda.fast.friends.FriendsViewModel import dev.meloda.fast.friends.FriendsViewModel
import dev.meloda.fast.friends.FriendsViewModelImpl 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.LocalHazeState
import dev.meloda.fast.ui.theme.LocalThemeConfig import dev.meloda.fast.ui.theme.LocalThemeConfig
import dev.meloda.fast.ui.util.ImmutableList 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.androidx.compose.koinViewModel
import org.koin.compose.koinInject import org.koin.compose.koinInject
import dev.meloda.fast.ui.R as UiR import dev.meloda.fast.ui.R as UiR
@@ -72,6 +72,7 @@ import dev.meloda.fast.ui.R as UiR
@Composable @Composable
fun FriendsRoute( fun FriendsRoute(
onError: (BaseError) -> Unit, onError: (BaseError) -> Unit,
onPhotoClicked: (url: String) -> Unit,
viewModel: FriendsViewModel = koinViewModel<FriendsViewModelImpl>() viewModel: FriendsViewModel = koinViewModel<FriendsViewModelImpl>()
) { ) {
val context = LocalContext.current val context = LocalContext.current
@@ -102,7 +103,8 @@ fun FriendsRoute(
canPaginate = canPaginate, canPaginate = canPaginate,
onSessionExpiredLogOutButtonClicked = { onError(BaseError.SessionExpired) }, onSessionExpiredLogOutButtonClicked = { onError(BaseError.SessionExpired) },
onPaginationConditionsMet = viewModel::onPaginationConditionsMet, onPaginationConditionsMet = viewModel::onPaginationConditionsMet,
onRefresh = viewModel::onRefresh onRefresh = viewModel::onRefresh,
onPhotoClicked = onPhotoClicked
) )
} }
@@ -120,7 +122,8 @@ fun FriendsScreen(
canPaginate: Boolean = false, canPaginate: Boolean = false,
onSessionExpiredLogOutButtonClicked: () -> Unit = {}, onSessionExpiredLogOutButtonClicked: () -> Unit = {},
onPaginationConditionsMet: () -> Unit = {}, onPaginationConditionsMet: () -> Unit = {},
onRefresh: () -> Unit = {} onRefresh: () -> Unit = {},
onPhotoClicked: (url: String) -> Unit = {}
) { ) {
val currentTheme = LocalThemeConfig.current val currentTheme = LocalThemeConfig.current
@@ -307,7 +310,8 @@ fun FriendsScreen(
uiFriends = ImmutableList.copyOf(friendsToDisplay), uiFriends = ImmutableList.copyOf(friendsToDisplay),
listState = listState, listState = listState,
maxLines = maxLines, maxLines = maxLines,
padding = padding padding = padding,
onPhotoClicked = onPhotoClicked
) )
if (friendsToDisplay.isEmpty()) { if (friendsToDisplay.isEmpty()) {
@@ -169,7 +169,7 @@ fun MessagesHistoryScreen(
mutableStateOf(false) mutableStateOf(false)
} }
val hazeSate = remember { HazeState() } val hazeState = remember { HazeState() }
var animationsEnabled by remember { var animationsEnabled by remember {
mutableStateOf( mutableStateOf(
@@ -202,7 +202,7 @@ fun MessagesHistoryScreen(
.then( .then(
if (currentTheme.enableBlur) { if (currentTheme.enableBlur) {
Modifier.hazeChild( Modifier.hazeChild(
state = hazeSate, state = hazeState,
style = HazeMaterials.thick() style = HazeMaterials.thick()
) )
} else Modifier } else Modifier
@@ -312,7 +312,7 @@ fun MessagesHistoryScreen(
.padding(bottom = padding.calculateBottomPadding()), .padding(bottom = padding.calculateBottomPadding()),
) { ) {
MessagesList( MessagesList(
hazeState = hazeSate, hazeState = hazeState,
listState = listState, listState = listState,
immutableMessages = ImmutableList.copyOf(screenState.messages), immutableMessages = ImmutableList.copyOf(screenState.messages),
isPaginating = screenState.isPaginating, isPaginating = screenState.isPaginating,
@@ -2,7 +2,7 @@ package dev.meloda.fast.messageshistory.util
import dev.meloda.fast.messageshistory.model.UiItem import dev.meloda.fast.messageshistory.model.UiItem
fun List<UiItem>.firstMessage(): UiItem.Message = first() as UiItem.Message fun List<UiItem>.firstMessage(): UiItem.Message = filterIsInstance<UiItem.Message>().first()
fun List<UiItem>.indexOfMessageById(messageId: Int): Int = fun List<UiItem>.indexOfMessageById(messageId: Int): Int =
indexOfFirst { it.id == messageId } indexOfFirst { it.id == messageId }
+10
View File
@@ -2,6 +2,8 @@ plugins {
alias(libs.plugins.android.library) alias(libs.plugins.android.library)
alias(libs.plugins.org.jetbrains.kotlin.android) alias(libs.plugins.org.jetbrains.kotlin.android)
alias(libs.plugins.kotlin.compose.compiler) alias(libs.plugins.kotlin.compose.compiler)
alias(libs.plugins.org.jetbrains.kotlin.plugin.parcelize)
alias(libs.plugins.kotlin.serialization)
} }
group = "dev.meloda.fast.photoviewer" group = "dev.meloda.fast.photoviewer"
@@ -52,4 +54,12 @@ dependencies {
implementation(libs.bundles.compose) implementation(libs.bundles.compose)
implementation(libs.coil.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)
} }
@@ -1,22 +1,34 @@
package dev.meloda.fast.photoviewer package dev.meloda.fast.photoviewer
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import dev.meloda.fast.common.extensions.setValue import dev.meloda.fast.common.extensions.setValue
import dev.meloda.fast.photoviewer.model.PhotoViewArguments import dev.meloda.fast.common.model.UiImage
import dev.meloda.fast.photoviewer.model.PhotoViewState import dev.meloda.fast.photoviewer.model.PhotoViewScreenState
import dev.meloda.fast.photoviewer.navigation.PhotoView
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import java.net.URLDecoder
interface PhotoViewViewModel { interface PhotoViewViewModel {
val state: StateFlow<PhotoViewState> val screenState: StateFlow<PhotoViewScreenState>
fun setArguments(arguments: PhotoViewArguments)
} }
class PhotoViewViewModelImpl : PhotoViewViewModel, ViewModel() { class PhotoViewViewModelImpl(
override val state = MutableStateFlow(PhotoViewState.EMPTY) savedStateHandle: SavedStateHandle
) : PhotoViewViewModel, ViewModel() {
override fun setArguments(arguments: PhotoViewArguments) { override val screenState = MutableStateFlow(PhotoViewScreenState.EMPTY)
state.setValue { old -> old.copy(images = arguments.images) }
init {
val arguments = PhotoView.from(savedStateHandle).arguments
screenState.setValue { old ->
old.copy(
images = arguments.images
.map { URLDecoder.decode(it, "utf-8") }
.map(UiImage::Url)
)
}
} }
} }
@@ -1,9 +1,11 @@
package dev.meloda.fast.photoviewer.model package dev.meloda.fast.photoviewer.model
import androidx.compose.runtime.Immutable import android.os.Parcelable
import dev.meloda.fast.common.model.UiImage import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
@Immutable @Parcelize
@Serializable
data class PhotoViewArguments( data class PhotoViewArguments(
val images: List<UiImage> val images: List<String>
) ) : Parcelable
@@ -4,12 +4,12 @@ import androidx.compose.runtime.Immutable
import dev.meloda.fast.common.model.UiImage import dev.meloda.fast.common.model.UiImage
@Immutable @Immutable
data class PhotoViewState( data class PhotoViewScreenState(
val images: List<UiImage> val images: List<UiImage>
) { ) {
companion object { companion object {
val EMPTY: PhotoViewState = PhotoViewState( val EMPTY: PhotoViewScreenState = PhotoViewScreenState(
images = emptyList() images = emptyList()
) )
} }
@@ -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<PhotoViewArguments>() to customNavType<PhotoViewArguments>())
fun from(savedStateHandle: SavedStateHandle) =
savedStateHandle.toRoute<PhotoView>(typeMap)
}
}
fun NavGraphBuilder.photoViewScreen(
onBack: () -> Unit
) {
composable<PhotoView>(typeMap = PhotoView.typeMap) {
PhotoViewRoute(onBack = onBack)
}
}
fun NavController.navigateToPhotoView(images: List<String>) {
this.navigate(
PhotoView(
arguments = PhotoViewArguments(
images.map { URLEncoder.encode(it, "utf-8") }
)
)
)
}
@@ -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<PhotoViewViewModelImpl>()
) {
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)
}
)
)
}
@@ -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
}
}
@@ -16,6 +16,7 @@ object Profile
fun NavGraphBuilder.profileScreen( fun NavGraphBuilder.profileScreen(
onError: (BaseError) -> Unit, onError: (BaseError) -> Unit,
onSettingsButtonClicked: () -> Unit, onSettingsButtonClicked: () -> Unit,
onPhotoClicked: (url: String) -> Unit,
navController: NavController navController: NavController
) { ) {
composable<Profile> { composable<Profile> {
@@ -25,6 +26,7 @@ fun NavGraphBuilder.profileScreen(
ProfileRoute( ProfileRoute(
onError = onError, onError = onError,
onSettingsButtonClicked = onSettingsButtonClicked, onSettingsButtonClicked = onSettingsButtonClicked,
onPhotoClicked = onPhotoClicked,
viewModel = viewModel viewModel = viewModel
) )
} }
@@ -1,5 +1,6 @@
package dev.meloda.fast.profile.presentation package dev.meloda.fast.profile.presentation
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@@ -46,6 +47,7 @@ import dev.meloda.fast.ui.R as UiR
fun ProfileRoute( fun ProfileRoute(
onError: (BaseError) -> Unit, onError: (BaseError) -> Unit,
onSettingsButtonClicked: () -> Unit, onSettingsButtonClicked: () -> Unit,
onPhotoClicked: (url: String) -> Unit,
viewModel: ProfileViewModel = koinViewModel<ProfileViewModelImpl>() viewModel: ProfileViewModel = koinViewModel<ProfileViewModelImpl>()
) { ) {
val screenState by viewModel.screenState.collectAsStateWithLifecycle() val screenState by viewModel.screenState.collectAsStateWithLifecycle()
@@ -54,8 +56,8 @@ fun ProfileRoute(
ProfileScreen( ProfileScreen(
screenState = screenState, screenState = screenState,
baseError = baseError, baseError = baseError,
onSettingsButtonClicked = onSettingsButtonClicked onSettingsButtonClicked = onSettingsButtonClicked,
onPhotoClicked = onPhotoClicked
) )
} }
@@ -66,6 +68,7 @@ fun ProfileScreen(
screenState: ProfileScreenState = ProfileScreenState.EMPTY, screenState: ProfileScreenState = ProfileScreenState.EMPTY,
baseError: BaseError? = null, baseError: BaseError? = null,
onSettingsButtonClicked: () -> Unit = {}, onSettingsButtonClicked: () -> Unit = {},
onPhotoClicked: (url: String) -> Unit = {}
) { ) {
Scaffold( Scaffold(
topBar = { topBar = {
@@ -105,7 +108,10 @@ fun ProfileScreen(
AsyncImage( AsyncImage(
modifier = Modifier modifier = Modifier
.size(120.dp) .size(120.dp)
.clip(CircleShape), .clip(CircleShape)
.clickable {
onPhotoClicked(screenState.avatarUrl.orEmpty())
},
model = screenState.avatarUrl, model = screenState.avatarUrl,
contentDescription = null, contentDescription = null,
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,