From 70f50da1d6676c9f4bf495287906efbbb35e7d5d Mon Sep 17 00:00:00 2001 From: tawhidmonowar Date: Mon, 10 Feb 2025 00:46:08 +0600 Subject: [PATCH] Home Screen Updated --- .../app/presentation/home/HomeAction.kt | 5 + .../app/presentation/home/HomeScreen.kt | 98 +++++++++++++ .../app/presentation/home/HomeState.kt | 5 + .../app/presentation/home/HomeViewModel.kt | 21 +++ .../home/components/ContestHorizontalPager.kt | 129 ++++++++++++++++ .../home/components/UpcomingContest.kt | 138 ++++++++++++++++++ .../app/presentation/profile/ProfileScreen.kt | 45 +----- .../greedycoder/core/injection/Modules.kt | 3 + 8 files changed, 406 insertions(+), 38 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/org/onedroid/greedycoder/app/presentation/home/HomeAction.kt create mode 100644 composeApp/src/commonMain/kotlin/org/onedroid/greedycoder/app/presentation/home/HomeScreen.kt create mode 100644 composeApp/src/commonMain/kotlin/org/onedroid/greedycoder/app/presentation/home/HomeState.kt create mode 100644 composeApp/src/commonMain/kotlin/org/onedroid/greedycoder/app/presentation/home/HomeViewModel.kt create mode 100644 composeApp/src/commonMain/kotlin/org/onedroid/greedycoder/app/presentation/home/components/ContestHorizontalPager.kt create mode 100644 composeApp/src/commonMain/kotlin/org/onedroid/greedycoder/app/presentation/home/components/UpcomingContest.kt diff --git a/composeApp/src/commonMain/kotlin/org/onedroid/greedycoder/app/presentation/home/HomeAction.kt b/composeApp/src/commonMain/kotlin/org/onedroid/greedycoder/app/presentation/home/HomeAction.kt new file mode 100644 index 0000000..97853c9 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/onedroid/greedycoder/app/presentation/home/HomeAction.kt @@ -0,0 +1,5 @@ +package org.onedroid.greedycoder.app.presentation.home + +sealed interface HomeAction { + data class OnTabSelected(val index: Int) : HomeAction +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/onedroid/greedycoder/app/presentation/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/org/onedroid/greedycoder/app/presentation/home/HomeScreen.kt new file mode 100644 index 0000000..1674b0a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/onedroid/greedycoder/app/presentation/home/HomeScreen.kt @@ -0,0 +1,98 @@ +package org.onedroid.greedycoder.app.presentation.home + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.koin.compose.viewmodel.koinViewModel +import org.onedroid.greedycoder.app.presentation.home.components.ContestHorizontalPager +import org.onedroid.greedycoder.app.presentation.home.components.UpcomingContestCardMostRecent +import org.onedroid.greedycoder.core.components.CustomTopAppBar + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HomeScreenRoot( + viewModel: HomeViewModel = koinViewModel(), + innerPadding: PaddingValues, + onSettingClick: () -> Unit = {}, +) { + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + val state by viewModel.state.collectAsStateWithLifecycle() + val keyboardController = LocalSoftwareKeyboardController.current + + Surface( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .background( + brush = Brush.verticalGradient( + listOf( + MaterialTheme.colorScheme.background, + MaterialTheme.colorScheme.background + ) + ) + ) + ) { + Scaffold( + modifier = Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + CustomTopAppBar( + title = "Home", + onSettingClick = { + onSettingClick() + }, + onSearchClick = { + + }, + scrollBehavior = scrollBehavior + ) + } + ) { innerPadding -> + HomeScreen( + modifier = Modifier + .fillMaxWidth() + .padding(innerPadding) + .verticalScroll(rememberScrollState()), + state = state, + onAction = { action -> + viewModel.onAction(action) + } + ) + } + } +} + +@Composable +private fun HomeScreen( + modifier: Modifier = Modifier, + state: HomeState, + onAction: (HomeAction) -> Unit = {} +) { + Column(modifier = modifier) { + UpcomingContestCardMostRecent() + ContestHorizontalPager( + onAction = { + onAction(it) + }, + state = state + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/onedroid/greedycoder/app/presentation/home/HomeState.kt b/composeApp/src/commonMain/kotlin/org/onedroid/greedycoder/app/presentation/home/HomeState.kt new file mode 100644 index 0000000..6de29b5 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/onedroid/greedycoder/app/presentation/home/HomeState.kt @@ -0,0 +1,5 @@ +package org.onedroid.greedycoder.app.presentation.home + +data class HomeState( + val selectedTabIndex: Int = 1 +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/onedroid/greedycoder/app/presentation/home/HomeViewModel.kt b/composeApp/src/commonMain/kotlin/org/onedroid/greedycoder/app/presentation/home/HomeViewModel.kt new file mode 100644 index 0000000..70cae04 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/onedroid/greedycoder/app/presentation/home/HomeViewModel.kt @@ -0,0 +1,21 @@ +package org.onedroid.greedycoder.app.presentation.home + +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +class HomeViewModel : ViewModel() { + private val _state = MutableStateFlow(HomeState()) + val state = _state.asStateFlow() + + fun onAction(action: HomeAction) { + when (action) { + is HomeAction.OnTabSelected -> { + _state.update { + it.copy(selectedTabIndex = action.index) + } + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/onedroid/greedycoder/app/presentation/home/components/ContestHorizontalPager.kt b/composeApp/src/commonMain/kotlin/org/onedroid/greedycoder/app/presentation/home/components/ContestHorizontalPager.kt new file mode 100644 index 0000000..1caa79a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/onedroid/greedycoder/app/presentation/home/components/ContestHorizontalPager.kt @@ -0,0 +1,129 @@ +package org.onedroid.greedycoder.app.presentation.home.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import org.onedroid.greedycoder.app.presentation.home.HomeAction +import org.onedroid.greedycoder.app.presentation.home.HomeState +import org.onedroid.greedycoder.core.theme.extraSmall +import org.onedroid.greedycoder.core.theme.small + +@Composable +fun ContestHorizontalPager( + onAction: (HomeAction) -> Unit, + state: HomeState +) { + val pagerState = rememberPagerState { 2 } + LaunchedEffect(state.selectedTabIndex) { + pagerState.animateScrollToPage(state.selectedTabIndex) + } + LaunchedEffect(pagerState.currentPage) { + onAction(HomeAction.OnTabSelected(pagerState.currentPage)) + } + Column( + modifier = Modifier.fillMaxWidth().padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + TabRow( + modifier = Modifier + .padding(horizontal = extraSmall) + .clip(RoundedCornerShape(8.dp)) + .fillMaxWidth(), + selectedTabIndex = state.selectedTabIndex, + containerColor = MaterialTheme.colorScheme.surfaceContainer, + divider = {}, + indicator = {} + ) { + Tab( + modifier = Modifier.weight(1f), + selected = state.selectedTabIndex == 0, + selectedContentColor = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.8f), + unselectedContentColor = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.8f), + onClick = { + onAction(HomeAction.OnTabSelected(0)) + } + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background( + color = if (state.selectedTabIndex == 0) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surfaceContainer, + shape = RoundedCornerShape(8.dp) + ) + ) { + Text( + modifier = Modifier + .align(Alignment.Center) + .padding(vertical = 12.dp), + text = "Upcoming", + style = MaterialTheme.typography.labelLarge + ) + } + } + Tab( + modifier = Modifier + .weight(1f), + selected = state.selectedTabIndex == 1, + selectedContentColor = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.8f), + unselectedContentColor = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.8f), + onClick = { + onAction(HomeAction.OnTabSelected(1)) + } + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background( + color = if (state.selectedTabIndex == 1) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surfaceContainer, + shape = RoundedCornerShape(8.dp) + ) + ) { + Text( + modifier = Modifier + .align(Alignment.Center) + .padding(vertical = 12.dp), + text = "Completed", + style = MaterialTheme.typography.labelLarge + ) + } + } + } + + HorizontalPager( + modifier = Modifier.fillMaxWidth(), + state = pagerState + ) { pageIndex -> + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + when (pageIndex) { + 0 -> { + Text(text = "Upcoming") + } + 1 -> { + Text(text = "Completed") + } + } + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/onedroid/greedycoder/app/presentation/home/components/UpcomingContest.kt b/composeApp/src/commonMain/kotlin/org/onedroid/greedycoder/app/presentation/home/components/UpcomingContest.kt new file mode 100644 index 0000000..a1971c5 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/onedroid/greedycoder/app/presentation/home/components/UpcomingContest.kt @@ -0,0 +1,138 @@ +package org.onedroid.greedycoder.app.presentation.home.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp + +@Composable +fun UpcomingContestCardMostRecent() { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp), + shape = RoundedCornerShape(12.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer + ) + ) { + Column( + modifier = Modifier + .padding(16.dp) + ) { + // Header + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column { + Text( + text = "Upcoming Contest", + color = Color.Gray, + style = MaterialTheme.typography.bodySmall + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "Feb, 09, 2025", + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.labelLarge + ) + } + Row { + TimerDisplay(5, "hrs ") + TimerDisplay(4, "mins ") + TimerDisplay(1, "secs") + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Title + Text( + text = "Codeforces Round 1003 (Div. 4)", + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // Difficulty and Tags + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = Modifier + .background(Color(0xFF2E2E2E), RoundedCornerShape(12.dp)) + .padding(horizontal = 8.dp, vertical = 4.dp) + .padding(end = 4.dp) + ) { + Text( + text = "Type: ICPC", + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.labelSmall + ) + } + Spacer(modifier = Modifier.width(5.dp)) + Box( + modifier = Modifier + .background(Color(0xFF2E2E2E), RoundedCornerShape(12.dp)) + .padding(horizontal = 8.dp, vertical = 4.dp) + .padding(end = 4.dp) + ) { + Text( + text = "Duration 2 Hrs", + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.labelSmall + ) + } + Spacer(modifier = Modifier.width(5.dp)) + Box( + modifier = Modifier + .background(Color(0xFF2E2E2E), RoundedCornerShape(12.dp)) + .padding(horizontal = 8.dp, vertical = 4.dp) + .padding(end = 4.dp) + ) { + Text( + text = "Phase: Before", + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.labelSmall + ) + } + } + } + } +} + +@Composable +fun TimerDisplay(value: Int, unit: String) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = value.toString().padStart(2, '0'), + color = Color.White, + style = MaterialTheme.typography.labelLarge + ) + Text( + text = unit, + color = Color.Gray, + style = MaterialTheme.typography.bodySmall + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/onedroid/greedycoder/app/presentation/profile/ProfileScreen.kt b/composeApp/src/commonMain/kotlin/org/onedroid/greedycoder/app/presentation/profile/ProfileScreen.kt index d6f4eb4..3c9abd7 100644 --- a/composeApp/src/commonMain/kotlin/org/onedroid/greedycoder/app/presentation/profile/ProfileScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/onedroid/greedycoder/app/presentation/profile/ProfileScreen.kt @@ -9,36 +9,24 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Search -import androidx.compose.material.icons.outlined.Settings import androidx.compose.material3.CircularProgressIndicator 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.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.graphics.Brush import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import greedycoder.composeapp.generated.resources.Res import greedycoder.composeapp.generated.resources.error_not_found -import greedycoder.composeapp.generated.resources.search -import greedycoder.composeapp.generated.resources.setting import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import org.onedroid.greedycoder.app.presentation.profile.components.ContestRankingLineChart @@ -46,6 +34,7 @@ import org.onedroid.greedycoder.app.presentation.profile.components.ProblemRatin import org.onedroid.greedycoder.app.presentation.profile.components.ProfileSection import org.onedroid.greedycoder.app.presentation.profile.components.StatsCard import org.onedroid.greedycoder.core.codeforces.domain.entity.CFUser +import org.onedroid.greedycoder.core.components.CustomTopAppBar import org.onedroid.greedycoder.core.components.EmbeddedSearchBar @OptIn(ExperimentalMaterial3Api::class) @@ -76,33 +65,13 @@ fun ProfileScreenRoot( .fillMaxSize() .nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { - TopAppBar( - title = { - Text( - text = "Greedy Coder", - style = TextStyle( - fontSize = 20.sp - ) - ) - }, - actions = { - IconButton(onClick = { - viewModel.onAction(ProfileAction.OnSearchActiveClick) - }) { - Icon( - imageVector = Icons.Outlined.Search, - contentDescription = stringResource(Res.string.search), - ) - } - IconButton(onClick = { - - }) { - Icon( - imageVector = Icons.Outlined.Settings, - contentDescription = stringResource(Res.string.setting), - ) - } + CustomTopAppBar( + title = "Profile", + onSettingClick = { + }, + onSearchClick = { + viewModel.onAction(ProfileAction.OnSearchActiveClick) }, scrollBehavior = scrollBehavior ) diff --git a/composeApp/src/commonMain/kotlin/org/onedroid/greedycoder/core/injection/Modules.kt b/composeApp/src/commonMain/kotlin/org/onedroid/greedycoder/core/injection/Modules.kt index 087bbcd..ae4788a 100644 --- a/composeApp/src/commonMain/kotlin/org/onedroid/greedycoder/core/injection/Modules.kt +++ b/composeApp/src/commonMain/kotlin/org/onedroid/greedycoder/core/injection/Modules.kt @@ -5,6 +5,7 @@ import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.bind import org.koin.dsl.module +import org.onedroid.greedycoder.app.presentation.home.HomeViewModel import org.onedroid.greedycoder.app.presentation.profile.ProfileViewModel import org.onedroid.greedycoder.app.presentation.settings.SettingViewModel import org.onedroid.greedycoder.core.codeforces.data.network.CFRemoteDataSource @@ -26,4 +27,6 @@ val sharedModule = module { viewModelOf(::SettingViewModel) singleOf(::AppPreferences) + + viewModelOf(::HomeViewModel) } \ No newline at end of file