diff --git a/presentation/build.gradle b/presentation/build.gradle index fc57e508..0fa5be7e 100644 --- a/presentation/build.gradle +++ b/presentation/build.gradle @@ -27,7 +27,7 @@ android { versionCode 10000 versionName "1.0.0" - buildConfigField ("String", "NATIVE_APP_KEY", properties['NATIVE_APP_KEY_STR']) + buildConfigField("String", "NATIVE_APP_KEY", properties['NATIVE_APP_KEY_STR']) manifestPlaceholders = [NATIVE_APP_KEY: NATIVE_APP_KEY] testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -93,6 +93,7 @@ dependencies { def landscapist_glide_version = "2.2.5" def fragment_version = "1.6.1" def activity_version = "1.7.2" + def emoji_version = "1.0.0-alpha03" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'androidx.core:core-ktx:1.10.1' @@ -111,7 +112,7 @@ dependencies { // Jetpack Compose // Material implementation("androidx.compose.material3:material3") - implementation ("androidx.compose.material:material-icons-extended:1.5.3") + implementation("androidx.compose.material:material-icons-extended:1.5.3") // Android Studio Preview support implementation("androidx.compose.ui:ui-tooling-preview") debugImplementation("androidx.compose.ui:ui-tooling") @@ -139,7 +140,8 @@ dependencies { // Testing Navigation androidTestImplementation "androidx.navigation:navigation-testing:$nav_version" // Jetpack Compose Integration - implementation("androidx.navigation:navigation-compose:$nav_version") + implementation "androidx.navigation:navigation-compose:$nav_version" + implementation "androidx.hilt:hilt-navigation-compose:1.1.0" // ViewPager2 implementation "androidx.viewpager2:viewpager2:1.0.0" @@ -198,6 +200,9 @@ dependencies { // Glide-For-Compose implementation "com.github.skydoves:landscapist-glide:$landscapist_glide_version" + // Coil + implementation "io.coil-kt:coil-compose:2.5.0" + // Preference implementation 'androidx.preference:preference-ktx:1.1.1' @@ -221,6 +226,10 @@ dependencies { // optional - Jetpack Compose integration implementation "androidx.paging:paging-compose:3.2.0" + // emoji + implementation "androidx.emoji2:emoji2:$emoji_version" + implementation "androidx.emoji2:emoji2-views:$emoji_version" + testImplementation 'junit:junit:4.+' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' diff --git a/presentation/src/main/java/daily/dayo/presentation/activity/MainActivity.kt b/presentation/src/main/java/daily/dayo/presentation/activity/MainActivity.kt index c341d73d..0c01ff5d 100644 --- a/presentation/src/main/java/daily/dayo/presentation/activity/MainActivity.kt +++ b/presentation/src/main/java/daily/dayo/presentation/activity/MainActivity.kt @@ -1,6 +1,7 @@ package daily.dayo.presentation.activity import android.Manifest +import android.annotation.SuppressLint import android.content.Intent import android.content.pm.PackageManager import android.graphics.Rect @@ -11,22 +12,63 @@ import android.view.MotionEvent import android.view.View import android.widget.Toast import androidx.activity.OnBackPressedCallback +import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.BottomNavigation +import androidx.compose.material.BottomNavigationItem +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.ModalBottomSheetState +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp import androidx.core.app.ActivityCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import androidx.core.view.forEach import androidx.navigation.NavController +import androidx.navigation.NavDestination.Companion.hierarchy +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.setupWithNavController +import dagger.hilt.android.AndroidEntryPoint import daily.dayo.presentation.R import daily.dayo.presentation.databinding.ActivityMainBinding import daily.dayo.presentation.fragment.home.HomeFragmentDirections -import daily.dayo.presentation.viewmodel.SettingNotificationViewModel -import dagger.hilt.android.AndroidEntryPoint +import daily.dayo.presentation.screen.home.HomeScreen +import daily.dayo.presentation.theme.Gray1_313131 +import daily.dayo.presentation.theme.Gray2_767B83 +import daily.dayo.presentation.theme.White_FFFFFF +import daily.dayo.presentation.view.getBottomSheetDialogState import daily.dayo.presentation.viewmodel.AccountViewModel +import daily.dayo.presentation.viewmodel.SettingNotificationViewModel +import kotlinx.coroutines.CoroutineScope @AndroidEntryPoint class MainActivity : AppCompatActivity() { @@ -36,7 +78,7 @@ class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) - setContentView(binding.root) + // setContentView(binding.root) setSystemBackClickListener() checkCurrentNotification() initBottomNavigation() @@ -44,6 +86,119 @@ class MainActivity : AppCompatActivity() { disableBottomNaviTooltip() getNotificationData() askNotificationPermission() + setContent { + MainScreen() + } + } + + @SuppressLint("UnusedMaterialScaffoldPaddingParameter") + @OptIn(ExperimentalMaterialApi::class) + @Composable + fun MainScreen() { + val navController = rememberNavController() + val coroutineScope = rememberCoroutineScope() + val bottomSheetState = getBottomSheetDialogState() + var bottomSheet: (@Composable () -> Unit)? by remember { mutableStateOf(null) } + val bottomSheetContent: (@Composable () -> Unit) -> Unit = { + bottomSheet = it + } + + Scaffold( + bottomBar = { bottomSheet?.let { it() } } + ) { + Scaffold( + bottomBar = { + MainBottomNavigation(navController = navController) + + } + ) { innerPadding -> + Box(Modifier.padding(innerPadding)) { + NavigationGraph(navController = navController, coroutineScope, bottomSheetState, bottomSheetContent) + } + } + } + } + + @Composable + fun MainBottomNavigation(navController: NavController) { + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentDestination = navBackStackEntry?.destination + val items = listOf( + Screen.Home, + Screen.Feed, + Screen.Write, + Screen.Notification, + Screen.MyPage + ) + + BottomNavigation( + backgroundColor = White_FFFFFF, + contentColor = Gray2_767B83, + modifier = Modifier.height(73.dp) + ) { + items.forEach { screen -> + val selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true + BottomNavigationItem( + icon = { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + imageVector = ImageVector.vectorResource(id = if (selected) screen.selectedIcon else screen.defaultIcon), + contentDescription = stringResource(id = screen.resourceId), + modifier = Modifier + .size(if (screen.route != Screen.Write.route) 24.dp else 36.dp) + ) + + if (screen.route != Screen.Write.route) { + Text(text = stringResource(screen.resourceId), style = MaterialTheme.typography.caption) + } + } + }, + selected = selected, + selectedContentColor = Gray1_313131, + onClick = { + navController.navigate(screen.route) { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + } + ) + } + } + } + + @OptIn(ExperimentalMaterialApi::class) + @Composable + fun NavigationGraph( + navController: NavHostController, + coroutineScope: CoroutineScope, + bottomSheetState: ModalBottomSheetState, + bottomSheetContent: (@Composable () -> Unit) -> Unit + ) { + NavHost( + navController = navController, + startDestination = Screen.Home.route + ) { + composable(Screen.Home.route) { + HomeScreen(navController, coroutineScope, bottomSheetState, bottomSheetContent) + } + composable(Screen.Feed.route) { + + } + composable(Screen.Write.route) { + + } + composable(Screen.Notification.route) { + + } + composable(Screen.MyPage.route) { + + } + } } private fun setSystemBackClickListener() { @@ -137,6 +292,7 @@ class MainActivity : AppCompatActivity() { ).show() } } + else -> { //All request are permitted // 알림 최초 허용시에 모든 알림 허용처리 @@ -204,12 +360,14 @@ class MainActivity : AppCompatActivity() { isInside = true return true } + MotionEvent.ACTION_MOVE -> { isInside = rect.contains(v!!.left + event.x.toInt(), v.top + event.y.toInt()) binding.bottomNavigationMainBar.clearFocus() return false } + MotionEvent.ACTION_UP -> { binding.bottomNavigationMainBar.menu.findItem(R.id.WriteFragment) .setIcon(R.drawable.ic_write) @@ -223,6 +381,7 @@ class MainActivity : AppCompatActivity() { } return true } + else -> return true } } @@ -249,4 +408,12 @@ class MainActivity : AppCompatActivity() { companion object { val notificationPermission = arrayOf(Manifest.permission.POST_NOTIFICATIONS) } + + sealed class Screen(val route: String, @StringRes val resourceId: Int, @DrawableRes val defaultIcon: Int, @DrawableRes val selectedIcon: Int) { + object Home : Screen("home", R.string.home, R.drawable.ic_home, R.drawable.ic_home_filled) + object Feed : Screen("feed", R.string.feed, R.drawable.ic_feed, R.drawable.ic_feed_filled) + object Write : Screen("write", R.string.write, R.drawable.ic_write, R.drawable.ic_write_filled) + object Notification : Screen("notification", R.string.notification, R.drawable.ic_notification, R.drawable.ic_notification_filled) + object MyPage : Screen("mypage", R.string.my_page, R.drawable.ic_my_page, R.drawable.ic_my_page_filled) + } } \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/home/HomeDayoPickPostListFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/home/HomeDayoPickPostListFragment.kt index 4d47f7ab..589c312c 100644 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/home/HomeDayoPickPostListFragment.kt +++ b/presentation/src/main/java/daily/dayo/presentation/fragment/home/HomeDayoPickPostListFragment.kt @@ -14,20 +14,19 @@ import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.widget.ViewPager2 import com.bumptech.glide.Glide import com.bumptech.glide.RequestManager +import dagger.hilt.android.AndroidEntryPoint +import daily.dayo.domain.model.Category +import daily.dayo.domain.model.Post import daily.dayo.presentation.R +import daily.dayo.presentation.activity.MainActivity +import daily.dayo.presentation.adapter.HomeDayoPickAdapter import daily.dayo.presentation.common.GlideLoadUtil.loadImageBackground import daily.dayo.presentation.common.Status import daily.dayo.presentation.common.autoCleared import daily.dayo.presentation.common.setOnDebounceClickListener import daily.dayo.presentation.common.toByteArray import daily.dayo.presentation.databinding.FragmentHomeDayoPickPostListBinding -import daily.dayo.presentation.adapter.HomeDayoPickAdapter import daily.dayo.presentation.viewmodel.HomeViewModel -import com.google.android.material.bottomnavigation.BottomNavigationView -import dagger.hilt.android.AndroidEntryPoint -import daily.dayo.domain.model.Category -import daily.dayo.domain.model.Post -import daily.dayo.presentation.activity.MainActivity import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -42,8 +41,10 @@ class HomeDayoPickPostListFragment : Fragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + /* if (savedInstanceState == null) loadPosts(homeViewModel.currentDayoPickCategory) + */ } override fun onCreateView( @@ -85,11 +86,12 @@ class HomeDayoPickPostListFragment : Fragment() { private fun setDayoPickPostListRefreshListener() { binding.swipeRefreshLayoutDayoPickPost.setOnRefreshListener { - loadPosts(homeViewModel.currentDayoPickCategory) + // loadPosts(homeViewModel.currentDayoPickCategory) } } private fun setInitialCategory() { + /* with(binding) { radiogroupDayopickPostCategory.check( when (homeViewModel.currentDayoPickCategory) { @@ -103,6 +105,7 @@ class HomeDayoPickPostListFragment : Fragment() { } ) } + */ } private fun setRvDayoPickPostAdapter() { @@ -129,8 +132,10 @@ class HomeDayoPickPostListFragment : Fragment() { binding.layoutDayopickPostEmpty.isVisible = postList.isEmpty() } } + Status.LOADING -> { } + Status.ERROR -> { } @@ -164,7 +169,7 @@ class HomeDayoPickPostListFragment : Fragment() { private fun loadPosts(selectCategory: Category, isSmoothScroll: Boolean = false) { with(homeViewModel) { - currentDayoPickCategory = selectCategory + // currentDayoPickCategory = selectCategory requestDayoPickPostList() } @@ -190,7 +195,7 @@ class HomeDayoPickPostListFragment : Fragment() { this@HomeDayoPickPostListFragment.tag, "PostId Null Exception Occurred" ) - loadPosts(homeViewModel.currentDayoPickCategory) + // loadPosts(homeViewModel.currentDayoPickCategory) } } } @@ -261,7 +266,7 @@ class HomeDayoPickPostListFragment : Fragment() { (requireActivity() as MainActivity).setBottomNavigationIconClickListener(reselectedIconId = R.id.HomeFragment) { if (currentViewPagerPosition == HOME_DAYOPICK_PAGE_TAB_ID) { binding.swipeRefreshLayoutDayoPickPost.isRefreshing = true - loadPosts(homeViewModel.currentDayoPickCategory, isSmoothScroll = true) + // loadPosts(homeViewModel.currentDayoPickCategory, isSmoothScroll = true) } } } diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/home/HomeNewPostListFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/home/HomeNewPostListFragment.kt index ceefafd8..0fab6996 100644 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/home/HomeNewPostListFragment.kt +++ b/presentation/src/main/java/daily/dayo/presentation/fragment/home/HomeNewPostListFragment.kt @@ -44,8 +44,10 @@ class HomeNewPostListFragment : Fragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + /* if (savedInstanceState == null) loadPosts(homeViewModel.currentNewCategory) + */ } override fun onCreateView( @@ -87,11 +89,12 @@ class HomeNewPostListFragment : Fragment() { private fun setNewPostListRefreshListener() { binding.swipeRefreshLayoutNewPost.setOnRefreshListener { - loadPosts(homeViewModel.currentNewCategory) + // loadPosts(homeViewModel.currentNewCategory) } } private fun setInitialCategory() { + /* with(binding) { radiogroupNewPostCategory.check( when (homeViewModel.currentNewCategory) { @@ -105,6 +108,7 @@ class HomeNewPostListFragment : Fragment() { } ) } + */ } private fun setRvNewPostAdapter() { @@ -164,7 +168,7 @@ class HomeNewPostListFragment : Fragment() { private fun loadPosts(selectCategory: Category, isSmoothScroll: Boolean = false) { with(homeViewModel) { - currentNewCategory = selectCategory + // currentNewCategory = selectCategory requestNewPostList() } @@ -187,7 +191,7 @@ class HomeNewPostListFragment : Fragment() { } } catch (postIdNullException: NullPointerException) { Log.e(this@HomeNewPostListFragment.tag, "PostId Null Exception Occurred") - loadPosts(homeViewModel.currentNewCategory) + // loadPosts(homeViewModel.currentNewCategory) } } } @@ -253,7 +257,7 @@ class HomeNewPostListFragment : Fragment() { (requireActivity() as MainActivity).setBottomNavigationIconClickListener(reselectedIconId = R.id.HomeFragment) { if (currentViewPagerPosition == HOME_NEW_PAGE_TAB_ID) { binding.swipeRefreshLayoutNewPost.isRefreshing = true - loadPosts(homeViewModel.currentNewCategory, isSmoothScroll = true) + // loadPosts(homeViewModel.currentNewCategory, isSmoothScroll = true) } } } diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/home/HomeDayoPickScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/home/HomeDayoPickScreen.kt new file mode 100644 index 00000000..97642c4f --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/home/HomeDayoPickScreen.kt @@ -0,0 +1,206 @@ +package daily.dayo.presentation.screen.home + +import androidx.compose.foundation.Image +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ModalBottomSheetState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import daily.dayo.presentation.R +import daily.dayo.presentation.common.Status +import daily.dayo.presentation.theme.Gray2_767B83 +import daily.dayo.presentation.theme.Gray3_9FA5AE +import daily.dayo.presentation.theme.Gray4_C5CAD2 +import daily.dayo.presentation.theme.Gray6_F0F1F3 +import daily.dayo.presentation.theme.White_FFFFFF +import daily.dayo.presentation.theme.b3 +import daily.dayo.presentation.theme.caption1 +import daily.dayo.presentation.theme.caption3 +import daily.dayo.presentation.view.EmojiView +import daily.dayo.presentation.view.FilledButton +import daily.dayo.presentation.view.HomePostView +import daily.dayo.presentation.viewmodel.HomeViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun HomeDayoPickScreen( + selectedCategoryName: String, + coroutineScope: CoroutineScope, + bottomSheetState: ModalBottomSheetState, + homeViewModel: HomeViewModel = hiltViewModel() +) { + val dayoPickPostList = homeViewModel.dayoPickPostList.observeAsState() + val refreshing by homeViewModel.isRefreshing.collectAsStateWithLifecycle() + val pullRefreshState = rememberPullRefreshState(refreshing, { homeViewModel.loadDayoPickPosts() }) + var isEmpty by rememberSaveable { mutableStateOf(false) } + + Box( + modifier = Modifier + .fillMaxSize() + .pullRefresh(pullRefreshState) + ) { + Column { + LazyVerticalGrid( + columns = GridCells.Fixed(2), + horizontalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = PaddingValues(horizontal = 18.dp), + modifier = Modifier.wrapContentHeight() + ) { + // description + item(span = { GridItemSpan(maxLineSpan) }) { + Row( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth() + .padding(top = 8.dp, bottom = 12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row { + EmojiView( + emoji = "\uD83D\uDCA1", + emojiSize = MaterialTheme.typography.bodyMedium.fontSize, + modifier = Modifier.align(Alignment.CenterVertically) + ) + + Text( + text = stringResource(id = R.string.home_dayopick_description), + style = MaterialTheme.typography.bodyMedium.copy(Color(0xFF73777C)), + modifier = Modifier + .padding(horizontal = 4.dp) + .align(Alignment.CenterVertically) + ) + } + CategoryButton(selectedCategoryName, coroutineScope, bottomSheetState) + } + } + + // dayo pick list + when (dayoPickPostList.value?.status) { + Status.SUCCESS -> { + isEmpty = dayoPickPostList.value?.data?.isEmpty() == true + dayoPickPostList.value?.data?.mapIndexed { index, post -> + item { + HomePostView( + post = post, + isDayoPick = index in 0..4, + modifier = Modifier.padding(bottom = 20.dp), + onClickPost = { + // todo move to post + }, + onClickLikePost = { + if (!post.heart) { + homeViewModel.requestLikePost(post.postId!!, isDayoPickLike = true) + } else { + homeViewModel.requestUnlikePost(post.postId!!, isDayoPickLike = true) + } + }, + onClickNickname = { + // todo move to profile + } + ) + } + } + } + + Status.LOADING -> { + } + + Status.ERROR -> { + + } + + else -> {} + } + } + + // empty view + if (isEmpty) HomeDayoPickEmptyView() + } + + // refresh indicator + PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter)) + } +} + +@Composable +private fun HomeDayoPickEmptyView() { + Column( + modifier = Modifier + .fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image(imageVector = ImageVector.vectorResource(id = R.drawable.ic_home_empty), contentDescription = null) + Spacer(modifier = Modifier.height(20.dp)) + + Text(text = stringResource(id = R.string.home_dayopick_empty_title), style = MaterialTheme.typography.b3.copy(Gray3_9FA5AE)) + Spacer(modifier = Modifier.height(2.dp)) + Text(text = stringResource(id = R.string.home_dayopick_empty_detail), style = MaterialTheme.typography.caption1.copy(Gray4_C5CAD2)) + + Spacer(modifier = Modifier.height(28.dp)) + FilledButton(onClick = { /*TODO*/ }, label = stringResource(id = R.string.home_dayopick_empty_action)) + } +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun CategoryButton( + selectedCategory: String, + coroutineScope: CoroutineScope, + bottomSheetState: ModalBottomSheetState +) { + Button( + onClick = { coroutineScope.launch { bottomSheetState.show() } }, + shape = RoundedCornerShape(8.dp), + contentPadding = PaddingValues(top = 6.dp, bottom = 6.dp, start = 12.dp, end = 4.dp), + colors = androidx.compose.material3.ButtonDefaults.buttonColors(containerColor = White_FFFFFF, contentColor = Gray2_767B83), + modifier = Modifier + .border(1.dp, Gray6_F0F1F3, shape = RoundedCornerShape(8.dp)) + ) { + Text(text = selectedCategory, style = MaterialTheme.typography.caption3) + Spacer(modifier = Modifier.width(8.dp)) + Icon(Icons.Filled.ArrowDropDown, "category menu") + } +} + diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/home/HomeNewScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/home/HomeNewScreen.kt new file mode 100644 index 00000000..af12fd4f --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/home/HomeNewScreen.kt @@ -0,0 +1,171 @@ +package daily.dayo.presentation.screen.home + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ModalBottomSheetState +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import daily.dayo.presentation.R +import daily.dayo.presentation.common.Status +import daily.dayo.presentation.theme.Gray3_9FA5AE +import daily.dayo.presentation.theme.Gray4_C5CAD2 +import daily.dayo.presentation.theme.b3 +import daily.dayo.presentation.theme.caption1 +import daily.dayo.presentation.view.EmojiView +import daily.dayo.presentation.view.FilledButton +import daily.dayo.presentation.view.HomePostView +import daily.dayo.presentation.viewmodel.HomeViewModel +import kotlinx.coroutines.CoroutineScope + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun HomeNewScreen( + selectedCategoryName: String, + coroutineScope: CoroutineScope, + bottomSheetState: ModalBottomSheetState, + homeViewModel: HomeViewModel = hiltViewModel() +) { + val newPostList = homeViewModel.newPostList.observeAsState() + val refreshing by homeViewModel.isRefreshing.collectAsStateWithLifecycle() + val pullRefreshState = rememberPullRefreshState(refreshing, { homeViewModel.loadNewPosts() }) + var isEmpty by rememberSaveable { mutableStateOf(false) } + + Box( + modifier = Modifier + .fillMaxSize() + .pullRefresh(pullRefreshState) + ) { + Column { + LazyVerticalGrid( + columns = GridCells.Fixed(2), + horizontalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = PaddingValues(horizontal = 18.dp), + modifier = Modifier.wrapContentHeight() + ) { + // description + item(span = { GridItemSpan(2) }) { + Row( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth() + .padding(top = 8.dp, bottom = 12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row { + EmojiView( + emoji = "\uD83D\uDC40", + emojiSize = MaterialTheme.typography.bodyMedium.fontSize, + modifier = Modifier.align(Alignment.CenterVertically) + ) + + Text( + text = stringResource(id = R.string.home_new_description), + style = MaterialTheme.typography.bodyMedium.copy(Color(0xFF73777C)), + modifier = Modifier + .padding(horizontal = 4.dp) + .align(Alignment.CenterVertically) + ) + } + CategoryButton(selectedCategoryName, coroutineScope, bottomSheetState) + } + } + + // new post list + when (newPostList.value?.status) { + Status.SUCCESS -> { + isEmpty = newPostList.value?.data?.isEmpty() == true + newPostList.value?.data?.mapIndexed { _, post -> + item { + HomePostView( + post = post, + modifier = Modifier.padding(bottom = 20.dp), + onClickPost = { + // todo move to post + }, + onClickLikePost = { + if (!post.heart) { + homeViewModel.requestLikePost(post.postId!!, isDayoPickLike = false) + } else { + homeViewModel.requestUnlikePost(post.postId!!, isDayoPickLike = false) + } + }, + onClickNickname = { + // todo move to profile + } + ) + } + } + } + + Status.LOADING -> { + } + + Status.ERROR -> { + + } + + else -> {} + } + } + + // empty view + if (isEmpty) HomeNewEmptyView() + } + + // refresh indicator + PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter)) + } +} + +@Composable +private fun HomeNewEmptyView() { + Column( + modifier = Modifier + .fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image(imageVector = ImageVector.vectorResource(id = R.drawable.ic_home_empty), contentDescription = null) + Spacer(modifier = Modifier.height(20.dp)) + + Text(text = stringResource(id = R.string.home_new_empty_title), style = MaterialTheme.typography.b3.copy(Gray3_9FA5AE)) + Spacer(modifier = Modifier.height(2.dp)) + Text(text = stringResource(id = R.string.home_new_empty_detail), style = MaterialTheme.typography.caption1.copy(Gray4_C5CAD2)) + + Spacer(modifier = Modifier.height(28.dp)) + FilledButton(onClick = { /*TODO*/ }, label = stringResource(id = R.string.home_new_empty_action)) + } +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/home/HomeScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/home/HomeScreen.kt new file mode 100644 index 00000000..9a4d7801 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/home/HomeScreen.kt @@ -0,0 +1,188 @@ +package daily.dayo.presentation.screen.home + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ModalBottomSheetState +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import daily.dayo.domain.model.Category +import daily.dayo.presentation.R +import daily.dayo.presentation.theme.Gray1_313131 +import daily.dayo.presentation.theme.Gray2_767B83 +import daily.dayo.presentation.theme.Gray5_E8EAEE +import daily.dayo.presentation.theme.PrimaryGreen_23C882 +import daily.dayo.presentation.view.BottomSheetDialog +import daily.dayo.presentation.view.TextButton +import daily.dayo.presentation.view.TopNavigation +import daily.dayo.presentation.view.getBottomSheetDialogState +import daily.dayo.presentation.viewmodel.HomeViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +const val HOME_DAYOPICK_PAGE_TAB_ID = 0 +const val HOME_NEW_PAGE_TAB_ID = 1 + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun HomeScreen( + navController: NavController, + coroutineScope: CoroutineScope, + bottomSheetState: ModalBottomSheetState, + bottomSheetContent: (@Composable () -> Unit) -> Unit, + homeViewModel: HomeViewModel = hiltViewModel() +) { + var homeTabState by rememberSaveable { mutableIntStateOf(HOME_DAYOPICK_PAGE_TAB_ID) } + var selectedCategory by rememberSaveable { mutableStateOf(Pair(CategoryMenu.All.name, 0)) } // name, index + val onClickCategory: (CategoryMenu, Int) -> Unit = { categoryMenu, index -> + selectedCategory = Pair(categoryMenu.name, index) + homeViewModel.setCategory(categoryMenu.category) + coroutineScope.launch { bottomSheetState.hide() } + } + + Scaffold( + topBar = { + TopNavigation( + leftIcon = { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier.padding(start = 18.dp) + ) { + TextButton( + onClick = { + homeTabState = HOME_DAYOPICK_PAGE_TAB_ID + }, + text = stringResource(id = R.string.DayoPick), + textStyle = MaterialTheme.typography.titleLarge.copy( + color = if (homeTabState == HOME_DAYOPICK_PAGE_TAB_ID) Gray1_313131 else Gray5_E8EAEE, + fontWeight = FontWeight.ExtraBold + ) + ) + + TextButton( + onClick = { + homeTabState = HOME_NEW_PAGE_TAB_ID + }, + text = stringResource(id = R.string.New), + textStyle = MaterialTheme.typography.titleLarge.copy( + color = if (homeTabState == HOME_NEW_PAGE_TAB_ID) Gray1_313131 else Gray5_E8EAEE, + fontWeight = FontWeight.ExtraBold + ) + ) + } + }, + rightIcon = { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_search), + contentDescription = "search", + tint = Gray1_313131, + modifier = Modifier + .padding(end = 12.dp) + .size(24.dp) + ) + } + ) + } + ) { innerPadding -> + Column( + modifier = Modifier.padding(innerPadding) + ) { + when (homeTabState) { + HOME_DAYOPICK_PAGE_TAB_ID -> { + HomeDayoPickScreen(selectedCategory.first, coroutineScope, bottomSheetState, homeViewModel) + homeViewModel.loadDayoPickPosts() + } + + HOME_NEW_PAGE_TAB_ID -> { + HomeNewScreen(selectedCategory.first, coroutineScope, bottomSheetState, homeViewModel) + homeViewModel.loadNewPosts() + } + } + } + } + + bottomSheetContent { + CategoryBottomSheetDialog(onClickCategory, selectedCategory, coroutineScope, bottomSheetState) + } +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +private fun CategoryBottomSheetDialog( + onCategorySelected: (CategoryMenu, Int) -> Unit, + selectedCategory: Pair, + coroutineScope: CoroutineScope, + bottomSheetState: ModalBottomSheetState +) { + val categoryMenus = listOf( + CategoryMenu.All, + CategoryMenu.Scheduler, + CategoryMenu.StudyPlanner, + CategoryMenu.PocketBook, + CategoryMenu.SixHoleDiary, + CategoryMenu.Digital, + CategoryMenu.ETC + ) + + BottomSheetDialog( + sheetState = bottomSheetState, + buttons = categoryMenus.mapIndexed { index, category -> + Pair(category.name) { + onCategorySelected(category, index) + } + }, + title = stringResource(id = R.string.filter), + leftIconButtons = categoryMenus.map { + ImageVector.vectorResource(it.defaultIcon) + }, + leftIconCheckedButtons = categoryMenus.map { + ImageVector.vectorResource(it.checkedIcon) + }, + normalColor = Gray2_767B83, + checkedColor = PrimaryGreen_23C882, + checkedButtonIndex = selectedCategory.second, + closeButtonAction = { coroutineScope.launch { bottomSheetState.hide() } } + ) +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +@Preview(showBackground = true) +private fun PreviewHomeScreen() { + MaterialTheme { + HomeScreen(rememberNavController(), rememberCoroutineScope(), getBottomSheetDialogState(), {}) + } +} + + +sealed class CategoryMenu(val name: String, @DrawableRes val defaultIcon: Int, @DrawableRes val checkedIcon: Int, val category: Category) { + object All : CategoryMenu("전체", R.drawable.ic_category_all, R.drawable.ic_category_all_checked, Category.ALL) + object Scheduler : CategoryMenu("스케줄러", R.drawable.ic_category_scheduler, R.drawable.ic_category_scheduler_checked, Category.SCHEDULER) + object StudyPlanner : CategoryMenu("스터디 플래너", R.drawable.ic_category_studyplanner, R.drawable.ic_category_studyplanner_checked, Category.STUDY_PLANNER) + object PocketBook : CategoryMenu("포켓북", R.drawable.ic_category_pocketbook, R.drawable.ic_category_pocketbook_checked, Category.POCKET_BOOK) + object SixHoleDiary : CategoryMenu("6공 다이어리", R.drawable.ic_category_sixholediary, R.drawable.ic_category_sixholediary_checked, Category.SIX_DIARY) + object Digital : CategoryMenu("모바일 다이어리", R.drawable.ic_category_digital, R.drawable.ic_category_digital_checked, Category.GOOD_NOTE) + object ETC : CategoryMenu("기타", R.drawable.ic_category_etc, R.drawable.ic_category_etc_checked, Category.STUDY_PLANNER) +} diff --git a/presentation/src/main/java/daily/dayo/presentation/view/BottomSheetDialog.kt b/presentation/src/main/java/daily/dayo/presentation/view/BottomSheetDialog.kt index abdcbd4e..ebc4a575 100644 --- a/presentation/src/main/java/daily/dayo/presentation/view/BottomSheetDialog.kt +++ b/presentation/src/main/java/daily/dayo/presentation/view/BottomSheetDialog.kt @@ -20,13 +20,13 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Button import androidx.compose.material.Divider import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.LocalContentColor import androidx.compose.material.ModalBottomSheetLayout import androidx.compose.material.ModalBottomSheetState import androidx.compose.material.ModalBottomSheetValue import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Image +import androidx.compose.material.icons.filled.ImageSearch import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -37,6 +37,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextAlign @@ -63,7 +64,10 @@ fun BottomSheetDialog( sheetState: ModalBottomSheetState, buttons: List Unit>>, leftIconButtons: List? = null, + leftIconCheckedButtons: List? = null, isFirstButtonColored: Boolean = false, + normalColor: Color = Gray1_313131, + checkedColor: Color = PrimaryGreen_23C882, title: String = "", titleButtonAction: () -> Unit = {}, rightIcon: ImageVector = ImageVector.vectorResource(id = R.drawable.ic_check_mark), @@ -141,12 +145,12 @@ fun BottomSheetDialog( ), horizontalArrangement = if (leftIconButtons == null) Arrangement.Center else Arrangement.SpaceBetween, ) { - if (leftIconButtons != null) { + if (leftIconButtons != null && leftIconCheckedButtons != null) { Icon( - imageVector = leftIconButtons[index], + imageVector = if (checkedButtonIndex == index) leftIconCheckedButtons[index] else leftIconButtons[index], contentDescription = "", modifier = Modifier.align(Alignment.CenterVertically), - tint = if (checkedButtonIndex == index) PrimaryGreen_23C882 else LocalContentColor.current + tint = Color.Unspecified ) } Text( @@ -155,7 +159,7 @@ fun BottomSheetDialog( if (leftIconButtons == null) 0.dp else 8.dp, 0.dp ), - color = if ((isFirstButtonColored && index == 0) || (checkedButtonIndex == index)) PrimaryGreen_23C882 else Gray1_313131, + color = if ((isFirstButtonColored && index == 0) || (checkedButtonIndex == index)) checkedColor else normalColor, fontSize = 16.sp, style = MaterialTheme.typography.b4 ) @@ -165,7 +169,8 @@ fun BottomSheetDialog( Icon( imageVector = rightIcon, contentDescription = "", - modifier = Modifier.align(Alignment.CenterVertically) + modifier = Modifier.align(Alignment.CenterVertically), + tint = Color.Unspecified ) } } @@ -292,6 +297,7 @@ fun PreviewMyBottomSheetDialog() { buttons = listOf(Pair("contents") { }, Pair("contents") { }, Pair("contents") { }), title = "title", leftIconButtons = listOf(Icons.Default.Image, Icons.Default.Image, Icons.Default.Image), + leftIconCheckedButtons = listOf(Icons.Default.ImageSearch, Icons.Default.ImageSearch, Icons.Default.ImageSearch), checkedButtonIndex = 0, ) } diff --git a/presentation/src/main/java/daily/dayo/presentation/view/EmojiView.kt b/presentation/src/main/java/daily/dayo/presentation/view/EmojiView.kt new file mode 100644 index 00000000..6066dcb3 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/view/EmojiView.kt @@ -0,0 +1,34 @@ +package daily.dayo.presentation.view + +import android.view.View +import androidx.appcompat.widget.AppCompatTextView +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color.Companion.Black +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.viewinterop.AndroidView + +@Composable +fun EmojiView( + emoji: String, + emojiSize: TextUnit, + modifier: Modifier = Modifier +) { + AndroidView( + modifier = modifier, + factory = { context -> + AppCompatTextView(context).apply { + setTextColor(Black.toArgb()) + text = emoji + textSize = emojiSize.value + textAlignment = View.TEXT_ALIGNMENT_CENTER + } + }, + update = { + it.apply { + text = emoji + } + }, + ) +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/view/HomePostView.kt b/presentation/src/main/java/daily/dayo/presentation/view/HomePostView.kt new file mode 100644 index 00000000..34a7aaae --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/view/HomePostView.kt @@ -0,0 +1,173 @@ +package daily.dayo.presentation.view + +import androidx.compose.foundation.Image +import androidx.compose.foundation.interaction.MutableInteractionSource +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.aspectRatio +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.wrapContentHeight +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import daily.dayo.domain.model.Post +import daily.dayo.presentation.BuildConfig +import daily.dayo.presentation.R +import daily.dayo.presentation.common.extension.clickableSingle +import daily.dayo.presentation.theme.Gray1_313131 +import daily.dayo.presentation.theme.Gray3_9FA5AE +import daily.dayo.presentation.theme.b5 +import daily.dayo.presentation.theme.caption3 +import java.text.DecimalFormat + +@Composable +fun HomePostView( + post: Post, + modifier: Modifier = Modifier, + isDayoPick: Boolean = false, + onClickPost: () -> Unit, + onClickLikePost: () -> Unit, + onClickNickname: () -> Unit +) { + val imageInteractionSource = remember { MutableInteractionSource() } + Column(modifier = modifier) { + Box( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + ) { + // thumbnail image + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data("${BuildConfig.BASE_URL}/images/${post.thumbnailImage}") + .build(), + contentDescription = "dayo pick image", + contentScale = ContentScale.Crop, + modifier = Modifier + .matchParentSize() + .clip(RoundedCornerShape(size = 8.dp)) + .clickableSingle( + interactionSource = imageInteractionSource, + indication = null, + onClick = { onClickPost() } + ) + ) + + // dayo pick icon + if (isDayoPick) { + Image( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_dayo_pick), + modifier = Modifier + .align(Alignment.TopEnd) + .padding(end = 12.dp), + contentDescription = null + ) + } + + // like button + Image( + imageVector = ImageVector.vectorResource(id = if (post.heart) R.drawable.ic_heart_filled else R.drawable.ic_heart), + modifier = Modifier + .padding(bottom = 12.dp, end = 11.dp) + .align(Alignment.BottomEnd) + .clickableSingle( + indication = rememberRipple(bounded = false), + interactionSource = remember { MutableInteractionSource() }, + onClick = { onClickLikePost() } + ), + contentDescription = "like Button", + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + // publisher info + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier + .wrapContentHeight() + .clickableSingle( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = { onClickNickname() } + ) + ) { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data("${BuildConfig.BASE_URL}/images/${post.userProfileImage}") + .build(), + contentDescription = "${post.nickname} + profile", + contentScale = ContentScale.Crop, + modifier = Modifier + .size(16.dp) + .clip(shape = CircleShape) + .align(Alignment.CenterVertically) + ) + Text(text = post.nickname, style = MaterialTheme.typography.b5.copy(Gray1_313131)) + } + + Spacer(modifier = Modifier.height(2.dp)) + + // post info + val dec = DecimalFormat("#,###") + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Text(text = stringResource(id = R.string.like) + " ${dec.format(post.heartCount)}", style = MaterialTheme.typography.caption3.copy(Gray3_9FA5AE)) + Text(text = stringResource(id = R.string.comment) + " ${dec.format(post.commentCount)}", style = MaterialTheme.typography.caption3.copy(Gray3_9FA5AE)) + } + } +} + +@Composable +@Preview(showBackground = true) +private fun PreviewHomePostView() { + MaterialTheme { + HomePostView( + modifier = Modifier.size(156.dp, 205.dp), + post = Post( + postId = 0, + thumbnailImage = "", + memberId = "", + nickname = "nickname", + userProfileImage = "", + heartCount = 123456, + commentCount = 8100, + heart = true, + category = null, + postImages = null, + contents = null, + createDateTime = null, + folderId = null, + folderName = null, + comments = null, + hashtags = null, + bookmark = null + ), + isDayoPick = true, + onClickPost = {}, + onClickLikePost = {}, + onClickNickname = {} + ) + } +} diff --git a/presentation/src/main/java/daily/dayo/presentation/viewmodel/HomeViewModel.kt b/presentation/src/main/java/daily/dayo/presentation/viewmodel/HomeViewModel.kt index 1b253ea8..69dc2378 100644 --- a/presentation/src/main/java/daily/dayo/presentation/viewmodel/HomeViewModel.kt +++ b/presentation/src/main/java/daily/dayo/presentation/viewmodel/HomeViewModel.kt @@ -4,7 +4,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import daily.dayo.presentation.common.Resource +import dagger.hilt.android.lifecycle.HiltViewModel import daily.dayo.domain.model.Category import daily.dayo.domain.model.NetworkResponse import daily.dayo.domain.model.Post @@ -14,8 +14,11 @@ import daily.dayo.domain.usecase.post.RequestDayoPickPostListCategoryUseCase import daily.dayo.domain.usecase.post.RequestDayoPickPostListUseCase import daily.dayo.domain.usecase.post.RequestNewPostListCategoryUseCase import daily.dayo.domain.usecase.post.RequestNewPostListUseCase -import dagger.hilt.android.lifecycle.HiltViewModel +import daily.dayo.presentation.common.Resource import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import javax.inject.Inject @@ -28,8 +31,13 @@ class HomeViewModel @Inject constructor( private val requestLikePostUseCase: RequestLikePostUseCase, private val requestUnlikePostUseCase: RequestUnlikePostUseCase ) : ViewModel() { - var currentDayoPickCategory = Category.ALL - var currentNewCategory = Category.ALL + private val _isRefreshing = MutableStateFlow(false) + val isRefreshing: StateFlow + get() = _isRefreshing.asStateFlow() + + private val _currentCategory = MutableStateFlow(Category.ALL) + val currentCategory: StateFlow + get() = _currentCategory.asStateFlow() private val _dayoPickPostList = MutableLiveData>>() val dayoPickPostList: LiveData>> get() = _dayoPickPostList @@ -37,21 +45,41 @@ class HomeViewModel @Inject constructor( private val _newPostList = MutableLiveData>>() val newPostList: LiveData>> get() = _newPostList + fun setCategory(category: Category) { + viewModelScope.launch { + _currentCategory.emit(category) + } + } + + fun loadDayoPickPosts() { + viewModelScope.launch { + _isRefreshing.emit(true) + requestDayoPickPostList() + _isRefreshing.emit(false) + } + } + + fun loadNewPosts() { + viewModelScope.launch { + _isRefreshing.emit(true) + requestNewPostList() + _isRefreshing.emit(false) + } + } + fun requestDayoPickPostList() = viewModelScope.launch { _dayoPickPostList.postValue(Resource.loading(null)) - if (currentDayoPickCategory == Category.ALL) { - requestHomeDayoPickPostList() - } else { - requestHomeDayoPickPostListCategory(currentDayoPickCategory) + when (currentCategory.value) { + Category.ALL -> requestHomeDayoPickPostList() + else -> requestHomeDayoPickPostListCategory(currentCategory.value) } } fun requestNewPostList() = viewModelScope.launch { _newPostList.postValue(Resource.loading(null)) - if (currentNewCategory == Category.ALL) { - requestHomeNewPostList() - } else { - requestHomeNewPostListCategory(currentNewCategory) + when (currentCategory.value) { + Category.ALL -> requestHomeNewPostList() + else -> requestHomeNewPostListCategory(currentCategory.value) } } @@ -61,12 +89,15 @@ class HomeViewModel @Inject constructor( is NetworkResponse.Success -> { _newPostList.postValue(Resource.success(ApiResponse.body?.data)) } + is NetworkResponse.NetworkError -> { _newPostList.postValue(Resource.error(ApiResponse.exception.toString(), null)) } + is NetworkResponse.ApiError -> { _newPostList.postValue(Resource.error(ApiResponse.error.toString(), null)) } + is NetworkResponse.UnknownError -> { _newPostList.postValue(Resource.error(ApiResponse.throwable.toString(), null)) } @@ -80,12 +111,15 @@ class HomeViewModel @Inject constructor( is NetworkResponse.Success -> { _newPostList.postValue(Resource.success(ApiResponse.body?.data)) } + is NetworkResponse.NetworkError -> { _newPostList.postValue(Resource.error(ApiResponse.exception.toString(), null)) } + is NetworkResponse.ApiError -> { _newPostList.postValue(Resource.error(ApiResponse.error.toString(), null)) } + is NetworkResponse.UnknownError -> { _newPostList.postValue(Resource.error(ApiResponse.throwable.toString(), null)) } @@ -99,6 +133,7 @@ class HomeViewModel @Inject constructor( is NetworkResponse.Success -> { _dayoPickPostList.postValue(Resource.success(ApiResponse.body?.data)) } + is NetworkResponse.NetworkError -> { _dayoPickPostList.postValue( Resource.error( @@ -107,9 +142,11 @@ class HomeViewModel @Inject constructor( ) ) } + is NetworkResponse.ApiError -> { _dayoPickPostList.postValue(Resource.error(ApiResponse.error.toString(), null)) } + is NetworkResponse.UnknownError -> { _dayoPickPostList.postValue( Resource.error( @@ -128,6 +165,7 @@ class HomeViewModel @Inject constructor( is NetworkResponse.Success -> { _dayoPickPostList.postValue(Resource.success(ApiResponse.body?.data)) } + is NetworkResponse.NetworkError -> { _dayoPickPostList.postValue( Resource.error( @@ -136,9 +174,11 @@ class HomeViewModel @Inject constructor( ) ) } + is NetworkResponse.ApiError -> { _dayoPickPostList.postValue(Resource.error(ApiResponse.error.toString(), null)) } + is NetworkResponse.UnknownError -> { _dayoPickPostList.postValue( Resource.error( @@ -172,6 +212,7 @@ class HomeViewModel @Inject constructor( ) ) } + is NetworkResponse.NetworkError -> {} is NetworkResponse.ApiError -> {} is NetworkResponse.UnknownError -> {} @@ -200,6 +241,7 @@ class HomeViewModel @Inject constructor( ) ) } + is NetworkResponse.NetworkError -> {} is NetworkResponse.ApiError -> {} is NetworkResponse.UnknownError -> {} diff --git a/presentation/src/main/res/drawable/ic_category_all.xml b/presentation/src/main/res/drawable/ic_category_all.xml new file mode 100644 index 00000000..b5a7c13e --- /dev/null +++ b/presentation/src/main/res/drawable/ic_category_all.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_category_all_checked.xml b/presentation/src/main/res/drawable/ic_category_all_checked.xml new file mode 100644 index 00000000..130900dc --- /dev/null +++ b/presentation/src/main/res/drawable/ic_category_all_checked.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_category_digital.xml b/presentation/src/main/res/drawable/ic_category_digital.xml new file mode 100644 index 00000000..347859d0 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_category_digital.xml @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_category_digital_checked.xml b/presentation/src/main/res/drawable/ic_category_digital_checked.xml new file mode 100644 index 00000000..6174b019 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_category_digital_checked.xml @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_category_etc.xml b/presentation/src/main/res/drawable/ic_category_etc.xml new file mode 100644 index 00000000..23c6e193 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_category_etc.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_category_etc_checked.xml b/presentation/src/main/res/drawable/ic_category_etc_checked.xml new file mode 100644 index 00000000..ca178dec --- /dev/null +++ b/presentation/src/main/res/drawable/ic_category_etc_checked.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_category_pocketbook.xml b/presentation/src/main/res/drawable/ic_category_pocketbook.xml new file mode 100644 index 00000000..4c3916f0 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_category_pocketbook.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_category_pocketbook_checked.xml b/presentation/src/main/res/drawable/ic_category_pocketbook_checked.xml new file mode 100644 index 00000000..0292d8ce --- /dev/null +++ b/presentation/src/main/res/drawable/ic_category_pocketbook_checked.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_category_scheduler.xml b/presentation/src/main/res/drawable/ic_category_scheduler.xml new file mode 100644 index 00000000..0f1c1fd6 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_category_scheduler.xml @@ -0,0 +1,32 @@ + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_category_scheduler_checked.xml b/presentation/src/main/res/drawable/ic_category_scheduler_checked.xml new file mode 100644 index 00000000..3c39e1f8 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_category_scheduler_checked.xml @@ -0,0 +1,32 @@ + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_category_sixholediary.xml b/presentation/src/main/res/drawable/ic_category_sixholediary.xml new file mode 100644 index 00000000..cf24a826 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_category_sixholediary.xml @@ -0,0 +1,36 @@ + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_category_sixholediary_checked.xml b/presentation/src/main/res/drawable/ic_category_sixholediary_checked.xml new file mode 100644 index 00000000..d5426667 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_category_sixholediary_checked.xml @@ -0,0 +1,36 @@ + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_category_studyplanner.xml b/presentation/src/main/res/drawable/ic_category_studyplanner.xml new file mode 100644 index 00000000..7f98c563 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_category_studyplanner.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_category_studyplanner_checked.xml b/presentation/src/main/res/drawable/ic_category_studyplanner_checked.xml new file mode 100644 index 00000000..5ff5a2b8 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_category_studyplanner_checked.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_dayo_pick.xml b/presentation/src/main/res/drawable/ic_dayo_pick.xml new file mode 100644 index 00000000..75510533 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_dayo_pick.xml @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 8f562b3a..aa15561d 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -6,7 +6,7 @@ 피드 글쓰기 알림 - 마이 페이지 + MY 다음 확인 완료 @@ -64,20 +64,23 @@ DAYO PICK New + 지금 인기있는 게시글이에요! 인기있는 다꾸가 아직 없어요 다꾸들을 구경하고 반응을 남겨보세요 구경하러 가기 + 실시간으로 올라온 게시글이에요! 새로운 다꾸가 아직 없어요 여러분의 다꾸를 공유해보세요 공유하러 가기 + 필터 전체 스케줄러 스터디 플래너 포켓북 6공 다이어리 - 굿노트 + 모바일 다이어리 기타 ALL SCHEDULER