From a3857a2302b519a237a06d0cc96fd7d72577defb Mon Sep 17 00:00:00 2001 From: ChuYong Date: Sat, 9 Mar 2024 16:25:39 +0900 Subject: [PATCH 01/26] =?UTF-8?q?feat:=20=EB=8C=93=EA=B8=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EB=82=A0=EC=A7=9C=20=EB=B3=80=EA=B2=BD=EC=8B=9C=20?= =?UTF-8?q?=EC=9D=B4=EC=8A=88=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 2 +- .../feature/view/common/PostCommentDialog.kt | 25 +++++++------- .../view_model/post/PostCommentViewModel.kt | 33 ++++++++++++------- 3 files changed, 33 insertions(+), 27 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 24b7d7f..cb50495 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -63,7 +63,7 @@ android { minSdk = 26 targetSdk = 34 versionCode = 11011 - versionName = "1.1.3" + versionName = "1.2.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/common/PostCommentDialog.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/common/PostCommentDialog.kt index f38c232..998bfd8 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/common/PostCommentDialog.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/common/PostCommentDialog.kt @@ -96,6 +96,15 @@ fun PostCommentDialog( createPostCommentViewModel: CreatePostCommentViewModel = hiltViewModel(), deletePostCommentViewModel: DeletePostCommentViewModel = hiltViewModel(), ) { + LaunchedEffect(postId) { + commentViewModel.invoke( + Arguments( + arguments = mapOf( + "postId" to postId + ) + ) + ) + } if (isEnabled.value) { val snackBarHost = LocalSnackbarHostState.current val focusManager = LocalFocusManager.current @@ -110,20 +119,8 @@ fun PostCommentDialog( mutableStateOf(false) } - val uiState = commentViewModel.uiState.collectAsLazyPagingItems() - LaunchedEffect(Unit) { - if (commentViewModel.isInitialize()) { - commentViewModel.invoke( - Arguments( - arguments = mapOf( - "postId" to postId - ) - ) - ) - } else { - commentViewModel.refresh() - } - } + val uiState = commentViewModel.commentLiveData.collectAsLazyPagingItems() + LaunchedEffect(uiState.loadState.refresh) { if (pauseNextScroll) { diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/PostCommentViewModel.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/PostCommentViewModel.kt index 64c63cd..55ad231 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/PostCommentViewModel.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/PostCommentViewModel.kt @@ -1,5 +1,10 @@ package com.no5ing.bbibbi.presentation.feature.view_model.post +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.asFlow +import androidx.lifecycle.asLiveData +import androidx.lifecycle.switchMap import androidx.lifecycle.viewModelScope import androidx.paging.PagingData import androidx.paging.cachedIn @@ -9,6 +14,7 @@ import com.no5ing.bbibbi.presentation.feature.uistate.post.PostCommentUiState import com.no5ing.bbibbi.presentation.feature.view_model.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.stateIn @@ -18,6 +24,20 @@ import javax.inject.Inject class PostCommentViewModel @Inject constructor( private val getCommentsRepository: GetCommentsRepository, ) : BaseViewModel>() { + private val _currentQuery = MutableLiveData() + val currentQuery: LiveData = _currentQuery + + val commentLiveData: Flow> = currentQuery.switchMap { arguments -> + getCommentsRepository + .fetch(arguments) + .cachedIn(viewModelScope) + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = PagingData.empty() + ).asLiveData(Dispatchers.IO) + }.asFlow() + override fun initState(): PagingData { return PagingData.empty() } @@ -25,18 +45,7 @@ class PostCommentViewModel @Inject constructor( fun refresh() = getCommentsRepository.invalidateSource() override fun invoke(arguments: Arguments) { - withMutexScope(Dispatchers.IO) { - getCommentsRepository - .fetch(arguments) - .cachedIn(viewModelScope) - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = PagingData.empty() - ).collectLatest { - setState(it) - } - } + _currentQuery.postValue(arguments) } override fun release() { From b2411e84bb6588a65d43f76991ca8a48c5ab1f82 Mon Sep 17 00:00:00 2001 From: ChuYong Date: Wed, 20 Mar 2024 14:32:54 +0900 Subject: [PATCH 02/26] =?UTF-8?q?feat:=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cb50495..0f08a6b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -63,7 +63,7 @@ android { minSdk = 26 targetSdk = 34 versionCode = 11011 - versionName = "1.2.0" + versionName = "1.1.4" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -121,30 +121,30 @@ dependencies { implementation("androidx.activity:activity-ktx:1.8.2") implementation("androidx.annotation:annotation-experimental:1.4.0") implementation("androidx.annotation:annotation:1.7.1") - implementation("androidx.browser:browser:1.7.0") + implementation("androidx.browser:browser:1.8.0") implementation("androidx.camera:camera-core:1.4.0-alpha04") implementation("androidx.camera:camera-lifecycle:1.4.0-alpha04") implementation("androidx.camera:camera-camera2:1.4.0-alpha04") - implementation("androidx.compose.animation:animation-core:1.6.1") - implementation("androidx.compose.animation:animation:1.6.1") - implementation("androidx.compose.foundation:foundation-layout:1.6.1") - implementation("androidx.compose.foundation:foundation:1.6.1") - implementation("androidx.compose.material3:material3:1.2.0") - implementation("androidx.compose.material:material-icons-core:1.6.1") - implementation("androidx.compose.material:material:1.6.1") - implementation("androidx.compose.runtime:runtime-livedata:1.6.1") - implementation("androidx.compose.runtime:runtime-saveable:1.6.1") - implementation("androidx.compose.runtime:runtime:1.6.1") + implementation("androidx.compose.animation:animation-core:1.6.3") + implementation("androidx.compose.animation:animation:1.6.3") + implementation("androidx.compose.foundation:foundation-layout:1.6.3") + implementation("androidx.compose.foundation:foundation:1.6.3") + implementation("androidx.compose.material3:material3:1.2.1") + implementation("androidx.compose.material:material-icons-core:1.6.3") + implementation("androidx.compose.material:material:1.6.3") + implementation("androidx.compose.runtime:runtime-livedata:1.6.3") + implementation("androidx.compose.runtime:runtime-saveable:1.6.3") + implementation("androidx.compose.runtime:runtime:1.6.3") implementation("androidx.compose.ui:ui") - implementation("androidx.compose.ui:ui-geometry:1.6.1") + implementation("androidx.compose.ui:ui-geometry:1.6.3") implementation("androidx.compose.ui:ui-graphics") - implementation("androidx.compose.ui:ui-text:1.6.1") + implementation("androidx.compose.ui:ui-text:1.6.3") implementation("androidx.compose.ui:ui-tooling-preview") - implementation("androidx.compose.ui:ui-unit:1.6.1") + implementation("androidx.compose.ui:ui-unit:1.6.3") implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.core:core-splashscreen:1.0.1") implementation("androidx.glance:glance-appwidget:1.0.0") - implementation("androidx.hilt:hilt-navigation-compose:1.2.0-rc01") + implementation("androidx.hilt:hilt-navigation-compose:1.2.0") implementation("androidx.lifecycle:lifecycle-common:2.7.0") implementation("androidx.lifecycle:lifecycle-livedata-core-ktx:2.7.0") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") @@ -154,8 +154,8 @@ dependencies { implementation("androidx.navigation:navigation-common:2.7.7") implementation("androidx.navigation:navigation-compose:2.7.7") implementation("androidx.navigation:navigation-runtime-ktx:2.7.7") - implementation("androidx.paging:paging-common-ktx:3.3.0-alpha03") - implementation("androidx.paging:paging-compose:3.3.0-alpha03") + implementation("androidx.paging:paging-common-ktx:3.3.0-alpha04") + implementation("androidx.paging:paging-compose:3.3.0-alpha04") implementation("androidx.savedstate:savedstate:1.2.1") implementation("androidx.security:security-crypto:1.1.0-alpha06") implementation("com.airbnb.android:lottie-compose:6.3.0") @@ -170,7 +170,7 @@ dependencies { implementation("com.github.skydoves:sandwich-retrofit:2.0.5") implementation("com.github.skydoves:sandwich:2.0.5") implementation("com.google.accompanist:accompanist-permissions:0.33.2-alpha") - implementation("com.google.android.gms:play-services-auth:20.7.0") + implementation("com.google.android.gms:play-services-auth:21.0.0") implementation("com.google.code.gson:gson:2.10.1") implementation("com.google.dagger:dagger:2.49") implementation("com.google.dagger:hilt-android:2.49") From ec177c109f20d6359df57b478deffd1ebe362a61 Mon Sep 17 00:00:00 2001 From: ChuYong Date: Fri, 19 Apr 2024 11:37:38 +0900 Subject: [PATCH 03/26] =?UTF-8?q?feat:=20=EB=B7=B0=EA=B8=B0=EB=B0=98=20?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bbibbi/data/datasource/network/RestAPI.kt | 8 ++ .../no5ing/bbibbi/data/model/post/PostType.kt | 5 + .../bbibbi/data/model/view/MainPageModel.kt | 41 ++++++ .../com/no5ing/bbibbi/di/NetworkModule.kt | 11 ++ .../view/common/PostTypeSwitchButton.kt | 128 ++++++++++++++++++ .../feature/view/main/home/HomePage.kt | 2 + .../feature/view/main/home/HomePageContent.kt | 9 ++ .../feature/view_model/MainPageViewModel.kt | 27 ++++ app/src/main/res/drawable/lock_icon.xml | 18 +++ 9 files changed, 249 insertions(+) create mode 100644 app/src/main/java/com/no5ing/bbibbi/data/model/post/PostType.kt create mode 100644 app/src/main/java/com/no5ing/bbibbi/data/model/view/MainPageModel.kt create mode 100644 app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/common/PostTypeSwitchButton.kt create mode 100644 app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/MainPageViewModel.kt create mode 100644 app/src/main/res/drawable/lock_icon.xml diff --git a/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt b/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt index 72a22d9..f3fe40f 100644 --- a/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt +++ b/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt @@ -36,6 +36,7 @@ import com.no5ing.bbibbi.data.model.post.PostComment import com.no5ing.bbibbi.data.model.post.PostReaction import com.no5ing.bbibbi.data.model.post.PostReactionSummary import com.no5ing.bbibbi.data.model.post.PostRealEmoji +import com.no5ing.bbibbi.data.model.view.MainPageModel import com.skydoves.sandwich.ApiResponse import retrofit2.http.Body import retrofit2.http.DELETE @@ -300,6 +301,12 @@ interface RestAPI { ): ApiResponse } + interface ViewApi { + @GET("v1/view/main") + suspend fun getMainView(): ApiResponse + + } + /** * API 모음 */ @@ -308,4 +315,5 @@ interface RestAPI { fun getPostApi(): PostApi fun getAuthApi(): AuthApi fun getLinkApi(): LinkApi + fun getViewApi(): ViewApi } \ No newline at end of file diff --git a/app/src/main/java/com/no5ing/bbibbi/data/model/post/PostType.kt b/app/src/main/java/com/no5ing/bbibbi/data/model/post/PostType.kt new file mode 100644 index 0000000..2ca7d16 --- /dev/null +++ b/app/src/main/java/com/no5ing/bbibbi/data/model/post/PostType.kt @@ -0,0 +1,5 @@ +package com.no5ing.bbibbi.data.model.post + +enum class PostType { + SURVIVAL, MISSION +} \ No newline at end of file diff --git a/app/src/main/java/com/no5ing/bbibbi/data/model/view/MainPageModel.kt b/app/src/main/java/com/no5ing/bbibbi/data/model/view/MainPageModel.kt new file mode 100644 index 0000000..78485ca --- /dev/null +++ b/app/src/main/java/com/no5ing/bbibbi/data/model/view/MainPageModel.kt @@ -0,0 +1,41 @@ +package com.no5ing.bbibbi.data.model.view + +import android.os.Parcelable +import com.no5ing.bbibbi.data.model.BaseModel +import kotlinx.parcelize.Parcelize +import java.time.ZonedDateTime + +@Parcelize +data class MainPageModel( + val topBarElements: List, + val isMissionUnlocked: Boolean, + val survivalFeeds: List, + val missionFeeds: List, + val pickers: List +) : Parcelable, BaseModel() + +@Parcelize +data class MainPageFeedModel( + val postId: String, + val imageUrl: String, + val authorName: String, + val createdAt: ZonedDateTime, +) : Parcelable + +@Parcelize +data class MainPagePickerModel( + val memberId: String, + val imageUrl: String, + val displayName: String, +) : Parcelable + +@Parcelize +data class MainPageTopBarModel( + val memberId: String, + val imageUrl: String?, + val noImageLetter: String, + val displayName: String, + val displayRank: Int?, + val shouldShowBirthdayMark: Boolean, + val shouldShowPickIcon: Boolean, +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/no5ing/bbibbi/di/NetworkModule.kt b/app/src/main/java/com/no5ing/bbibbi/di/NetworkModule.kt index e92339f..f225d3e 100644 --- a/app/src/main/java/com/no5ing/bbibbi/di/NetworkModule.kt +++ b/app/src/main/java/com/no5ing/bbibbi/di/NetworkModule.kt @@ -199,6 +199,12 @@ object NetworkModule { return retrofit.create(RestAPI.LinkApi::class.java) } + @Provides + @Singleton + fun provideRestViewApi(retrofit: Retrofit): RestAPI.ViewApi { + return retrofit.create(RestAPI.ViewApi::class.java) + } + @Provides @Singleton fun provideRestApi( @@ -207,6 +213,7 @@ object NetworkModule { postApi: RestAPI.PostApi, authApi: RestAPI.AuthApi, linkApi: RestAPI.LinkApi, + viewApi: RestAPI.ViewApi, ): RestAPI { return object : RestAPI { override fun getFamilyApi(): RestAPI.FamilyApi { @@ -228,6 +235,10 @@ object NetworkModule { override fun getLinkApi(): RestAPI.LinkApi { return linkApi } + + override fun getViewApi(): RestAPI.ViewApi { + return viewApi + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/common/PostTypeSwitchButton.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/common/PostTypeSwitchButton.kt new file mode 100644 index 0000000..e24cfd9 --- /dev/null +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/common/PostTypeSwitchButton.kt @@ -0,0 +1,128 @@ +package com.no5ing.bbibbi.presentation.feature.view.common + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.no5ing.bbibbi.R +import com.no5ing.bbibbi.data.model.post.PostType +import com.no5ing.bbibbi.presentation.component.BBiBBiPreviewSurface +import com.no5ing.bbibbi.presentation.theme.bbibbiScheme +import com.no5ing.bbibbi.presentation.theme.bbibbiTypo +import com.no5ing.bbibbi.util.dpToPx +import timber.log.Timber + + +@Composable +fun PostTypeSwitchButton( + isLocked: Boolean = true, + state: MutableState = remember { mutableStateOf(PostType.SURVIVAL) } +) { + val isSurvival = state.value == PostType.SURVIVAL + val widthMax = 138.dp.dpToPx() + val buttonPosition: Dp by animateDpAsState(targetValue = + if(isSurvival) 0.dp else 69.dp, animationSpec = tween( + durationMillis = 130, + easing = LinearEasing, + ), + label = "" + ) + val survivalButtonColor: Color by animateColorAsState( + targetValue =if(isSurvival) MaterialTheme.bbibbiScheme.backgroundPrimary else MaterialTheme.bbibbiScheme.gray500, + label = "", + ) + val missionButtonColor: Color by animateColorAsState( + targetValue = if(isSurvival) MaterialTheme.bbibbiScheme.gray500 else MaterialTheme.bbibbiScheme.backgroundPrimary, + label = "", + ) + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(width = 138.dp, height = 40.dp) + .background(MaterialTheme.bbibbiScheme.backgroundHover, RoundedCornerShape(40.dp)) + .pointerInput(Unit) { + detectTapGestures { offset -> + if (widthMax / 2 > offset.x) { + state.value = PostType.SURVIVAL + } else { + state.value = PostType.MISSION + } + Timber.d("offset: $offset") + } + } + //.padding(vertical = 8.dp, horizontal = 12.dp) + ) { + Box( + modifier = Modifier + .align(Alignment.CenterStart) + .offset(x = buttonPosition) + .size(width = 70.dp, height = 40.dp) + .background(MaterialTheme.bbibbiScheme.iconSelected, RoundedCornerShape(40.dp)) + ) + Row( + modifier = Modifier.fillMaxWidth().padding(start = 24.dp, + end = if(isLocked) 14.dp else 24.dp), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = "생존", + style = MaterialTheme.bbibbiTypo.bodyTwoBold, + color = survivalButtonColor + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(2.dp) + ) { + Text( + text = "미션", + style = MaterialTheme.bbibbiTypo.bodyTwoBold, + color = missionButtonColor, + ) + if (isLocked) { + Image( + modifier = Modifier.size(12.dp), + painter = painterResource(id = R.drawable.lock_icon), + contentDescription = "locked" + ) + } + } + + } + } + +} + +@Preview +@Composable +fun FeedTypeSwitchButtonPreview() { + BBiBBiPreviewSurface { + PostTypeSwitchButton() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePage.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePage.kt index b713cc5..109022a 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePage.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePage.kt @@ -27,6 +27,7 @@ import com.no5ing.bbibbi.presentation.component.BBiBBiPreviewSurface import com.no5ing.bbibbi.presentation.component.BBiBBiSurface import com.no5ing.bbibbi.presentation.component.BackToExitHandler import com.no5ing.bbibbi.presentation.feature.view.common.CustomAlertDialog +import com.no5ing.bbibbi.presentation.feature.view_model.MainPageViewModel import com.no5ing.bbibbi.presentation.feature.view_model.auth.RetrieveMeViewModel import com.no5ing.bbibbi.presentation.feature.view_model.post.DailyFamilyTopViewModel import com.no5ing.bbibbi.presentation.feature.view_model.post.IsMeUploadedTodayViewModel @@ -39,6 +40,7 @@ import kotlinx.coroutines.flow.MutableStateFlow @Composable fun HomePage( + mainPageViewModel: MainPageViewModel = hiltViewModel(), retrieveMeViewModel: RetrieveMeViewModel = hiltViewModel(), isMeUploadedTodayViewModel: IsMeUploadedTodayViewModel = hiltViewModel(), familyPostsViewModel: MainPostFeedViewModel = hiltViewModel(), diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt index 643d50b..99bc5d9 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -32,6 +33,7 @@ import com.no5ing.bbibbi.data.model.member.Member import com.no5ing.bbibbi.data.model.post.Post import com.no5ing.bbibbi.presentation.feature.uistate.family.MainFeedStoryElementUiState import com.no5ing.bbibbi.presentation.feature.uistate.family.MainFeedUiState +import com.no5ing.bbibbi.presentation.feature.view.common.PostTypeSwitchButton import com.no5ing.bbibbi.presentation.theme.bbibbiScheme import com.no5ing.bbibbi.util.gapBetweenNow import kotlinx.coroutines.flow.StateFlow @@ -85,6 +87,13 @@ fun HomePageContent( ) Spacer(modifier = Modifier.height(24.dp)) UploadCountDownBar() + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + PostTypeSwitchButton() + } + Spacer(modifier = Modifier.height(8.dp)) } } diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/MainPageViewModel.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/MainPageViewModel.kt new file mode 100644 index 0000000..bb7d691 --- /dev/null +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/MainPageViewModel.kt @@ -0,0 +1,27 @@ +package com.no5ing.bbibbi.presentation.feature.view_model + +import com.no5ing.bbibbi.data.datasource.network.RestAPI +import com.no5ing.bbibbi.data.model.APIResponse +import com.no5ing.bbibbi.data.model.APIResponse.Companion.wrapToAPIResponse +import com.no5ing.bbibbi.data.model.view.MainPageModel +import com.no5ing.bbibbi.data.repository.Arguments +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import javax.inject.Inject + +@HiltViewModel +class MainPageViewModel @Inject constructor( + private val restAPI: RestAPI, +) : BaseViewModel>() { + override fun initState(): APIResponse { + return APIResponse.idle() + } + + override fun invoke(arguments: Arguments) { + withMutexScope(Dispatchers.IO) { + val mainResult = restAPI.getViewApi().getMainView() + val apiResult = mainResult.wrapToAPIResponse() + setState(apiResult) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/lock_icon.xml b/app/src/main/res/drawable/lock_icon.xml new file mode 100644 index 0000000..cb5ae9e --- /dev/null +++ b/app/src/main/res/drawable/lock_icon.xml @@ -0,0 +1,18 @@ + + + + + + From 06f3fb0f8289466d4e1100ff85e4814dd57cdf93 Mon Sep 17 00:00:00 2001 From: ChuYong Date: Fri, 19 Apr 2024 14:29:03 +0900 Subject: [PATCH 04/26] feat: add mission post fetcher --- .../bbibbi/data/model/view/MainPageModel.kt | 1 + .../presentation/component/ProfileImage.kt | 28 +++++- .../feature/view/main/home/HomePage.kt | 79 ++++++++--------- .../feature/view/main/home/HomePageContent.kt | 56 ++++++------ .../view/main/home/HomePageFeedElement.kt | 1 - .../view/main/home/HomePageStoryBar.kt | 86 ++++++++++--------- .../main/HomePageController.kt | 4 +- .../feature/view_model/MainPageViewModel.kt | 13 +++ 8 files changed, 148 insertions(+), 120 deletions(-) diff --git a/app/src/main/java/com/no5ing/bbibbi/data/model/view/MainPageModel.kt b/app/src/main/java/com/no5ing/bbibbi/data/model/view/MainPageModel.kt index 78485ca..8db4910 100644 --- a/app/src/main/java/com/no5ing/bbibbi/data/model/view/MainPageModel.kt +++ b/app/src/main/java/com/no5ing/bbibbi/data/model/view/MainPageModel.kt @@ -9,6 +9,7 @@ import java.time.ZonedDateTime data class MainPageModel( val topBarElements: List, val isMissionUnlocked: Boolean, + val isMeUploadedToday: Boolean, val survivalFeeds: List, val missionFeeds: List, val pickers: List diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/component/ProfileImage.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/component/ProfileImage.kt index b9fb9e0..b07ff6f 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/component/ProfileImage.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/component/ProfileImage.kt @@ -30,11 +30,31 @@ fun CircleProfileImage( opacity: Float = 1.0f, backgroundColor: Color = MaterialTheme.bbibbiScheme.backgroundSecondary, onTap: () -> Unit = {}, +) { + CircleProfileImage( + size = size, + noImageLetter = member.name.first().toString(), + imageUrl = member.imageUrl, + opacity = opacity, + backgroundColor = backgroundColor, + onTap = onTap + ) +} + +@Composable +fun CircleProfileImage( + modifier: Modifier = Modifier, + size: Dp, + noImageLetter: String, + imageUrl: String?, + opacity: Float = 1.0f, + backgroundColor: Color = MaterialTheme.bbibbiScheme.backgroundSecondary, + onTap: () -> Unit = {}, ) { Box { - if (member.hasProfileImage()) { + if (imageUrl != null) { AsyncImage( - model = asyncImagePainter(source = member.imageUrl), + model = asyncImagePainter(source = imageUrl), contentDescription = null, contentScale = ContentScale.Crop, modifier = modifier @@ -60,7 +80,7 @@ fun CircleProfileImage( ) Box(modifier = Modifier.align(Alignment.Center)) { Text( - text = "${member.name.first()}", + text = noImageLetter, fontSize = 28.sp * (size / 90.dp), color = MaterialTheme.bbibbiScheme.white.copy(alpha = opacity), fontWeight = FontWeight.SemiBold, @@ -69,4 +89,4 @@ fun CircleProfileImage( } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePage.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePage.kt index 109022a..3955334 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePage.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePage.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -17,44 +18,34 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel -import androidx.paging.PagingData import com.no5ing.bbibbi.R import com.no5ing.bbibbi.data.model.APIResponse -import com.no5ing.bbibbi.data.model.member.Member -import com.no5ing.bbibbi.data.model.post.Post +import com.no5ing.bbibbi.data.model.post.PostType import com.no5ing.bbibbi.data.repository.Arguments import com.no5ing.bbibbi.presentation.component.BBiBBiPreviewSurface import com.no5ing.bbibbi.presentation.component.BBiBBiSurface import com.no5ing.bbibbi.presentation.component.BackToExitHandler import com.no5ing.bbibbi.presentation.feature.view.common.CustomAlertDialog import com.no5ing.bbibbi.presentation.feature.view_model.MainPageViewModel -import com.no5ing.bbibbi.presentation.feature.view_model.auth.RetrieveMeViewModel -import com.no5ing.bbibbi.presentation.feature.view_model.post.DailyFamilyTopViewModel -import com.no5ing.bbibbi.presentation.feature.view_model.post.IsMeUploadedTodayViewModel -import com.no5ing.bbibbi.presentation.feature.view_model.post.MainPostFeedViewModel import com.no5ing.bbibbi.presentation.theme.bbibbiScheme import com.no5ing.bbibbi.util.LocalSessionState import com.no5ing.bbibbi.util.gapUntilNext -import com.no5ing.bbibbi.util.todayAsString import kotlinx.coroutines.flow.MutableStateFlow @Composable fun HomePage( mainPageViewModel: MainPageViewModel = hiltViewModel(), - retrieveMeViewModel: RetrieveMeViewModel = hiltViewModel(), - isMeUploadedTodayViewModel: IsMeUploadedTodayViewModel = hiltViewModel(), - familyPostsViewModel: MainPostFeedViewModel = hiltViewModel(), - familyPostTopViewModel: DailyFamilyTopViewModel = hiltViewModel(), + postViewTypeState: MutableState = remember { mutableStateOf(PostType.SURVIVAL) }, onTapLeft: () -> Unit = {}, onTapRight: () -> Unit = {}, - onTapProfile: (Member) -> Unit = {}, - onTapContent: (Post) -> Unit = {}, + onTapProfile: (String) -> Unit = {}, + onTapContent: (String) -> Unit = {}, onTapUpload: () -> Unit = {}, onTapInvite: () -> Unit = {}, onUnsavedPost: (Uri) -> Unit = {}, ) { val memberId = LocalSessionState.current.memberId - val meUploadedState = isMeUploadedTodayViewModel.uiState.collectAsState() + val mainPageState = mainPageViewModel.uiState.collectAsState() val unsavedDialogUri = remember { mutableStateOf(null) } val unsavedDialogEnabled = remember { mutableStateOf(false) } CustomAlertDialog( @@ -67,34 +58,36 @@ fun HomePage( } ) - if (isMeUploadedTodayViewModel.shouldDisplayWidgetPopup) { - isMeUploadedTodayViewModel.shouldDisplayWidgetPopup = false + if (mainPageViewModel.shouldDisplayWidgetPopup) { + mainPageViewModel.shouldDisplayWidgetPopup = false TryWidgetPopup() } BackToExitHandler() LaunchedEffect(Unit) { - isMeUploadedTodayViewModel.invoke(Arguments(arguments = mapOf("memberId" to memberId))) - val tempUri = retrieveMeViewModel.getAndDeleteTemporaryUri() + // isMeUploadedTodayViewModel.invoke(Arguments(arguments = mapOf("memberId" to memberId))) + val tempUri = mainPageViewModel.getAndDeleteTemporaryUri() if (tempUri != null) { unsavedDialogUri.value = tempUri unsavedDialogEnabled.value = true } + mainPageViewModel.invoke(Arguments()) - if (familyPostsViewModel.isInitialize()) { - // familyMembersViewModel.invoke(Arguments()) - retrieveMeViewModel.invoke(Arguments()) - familyPostTopViewModel.invoke(Arguments())// TODO - familyPostsViewModel.invoke( - Arguments( - arguments = mapOf( - "date" to todayAsString(), - ) - ) - ) - } else { - familyPostTopViewModel.invoke(Arguments()) - familyPostsViewModel.refresh() - } +// if (familyPostsViewModel.isInitialize()) { +// // familyMembersViewModel.invoke(Arguments()) +// mainPageViewModel.invoke(Arguments()) +// // retrieveMeViewModel.invoke(Arguments()) +// familyPostTopViewModel.invoke(Arguments())// TODO +// familyPostsViewModel.invoke( +// Arguments( +// arguments = mapOf( +// "date" to todayAsString(), +// ) +// ) +// ) +// } else { +// familyPostTopViewModel.invoke(Arguments()) +// familyPostsViewModel.refresh() +// } } BBiBBiSurface( @@ -113,24 +106,22 @@ fun HomePage( onTapRight = onTapRight ) HomePageContent( - contentState = familyPostsViewModel.uiState, - postTopState = familyPostTopViewModel.uiState, - meState = retrieveMeViewModel.uiState, + mainPageState = mainPageViewModel.uiState, + postViewTypeState = postViewTypeState, onTapContent = onTapContent, onTapProfile = onTapProfile, onTapInvite = onTapInvite, onRefresh = { - familyPostTopViewModel.invoke(Arguments()) - retrieveMeViewModel.invoke(Arguments()) + mainPageViewModel.invoke(Arguments()) } ) } HomePageUploadButton( onTap = onTapUpload, - isLoading = !meUploadedState.value.isReady(), + isLoading = !mainPageState.value.isReady(), isUploadAbleTime = remember { gapUntilNext() > 0 }, - isAlreadyUploaded = !meUploadedState.value.isReady() || - meUploadedState.value.data + isAlreadyUploaded = !mainPageState.value.isReady() || + mainPageState.value.data.isMeUploadedToday ) } } @@ -150,9 +141,7 @@ fun HomePagePreview() { ) { HomePageTopBar() HomePageContent( - contentState = MutableStateFlow(PagingData.empty()), - postTopState = MutableStateFlow(APIResponse.idle()), - meState = MutableStateFlow(APIResponse.success(Member.unknown())) + mainPageState = MutableStateFlow(APIResponse.idle()), ) } HomePageUploadButton() diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt index 99bc5d9..b1a6508 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt @@ -15,6 +15,9 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -31,6 +34,8 @@ import com.no5ing.bbibbi.R import com.no5ing.bbibbi.data.model.APIResponse import com.no5ing.bbibbi.data.model.member.Member import com.no5ing.bbibbi.data.model.post.Post +import com.no5ing.bbibbi.data.model.post.PostType +import com.no5ing.bbibbi.data.model.view.MainPageModel import com.no5ing.bbibbi.presentation.feature.uistate.family.MainFeedStoryElementUiState import com.no5ing.bbibbi.presentation.feature.uistate.family.MainFeedUiState import com.no5ing.bbibbi.presentation.feature.view.common.PostTypeSwitchButton @@ -41,22 +46,21 @@ import kotlinx.coroutines.flow.StateFlow @OptIn(ExperimentalFoundationApi::class) @Composable fun HomePageContent( - contentState: StateFlow>, - //familyListState: StateFlow>, - postTopState: StateFlow>>, - meState: StateFlow>, - onTapContent: (Post) -> Unit = {}, - onTapProfile: (Member) -> Unit = {}, + mainPageState: StateFlow>, + postViewTypeState: MutableState = remember { mutableStateOf(PostType.SURVIVAL) }, + onTapContent: (String) -> Unit = {}, + onTapProfile: (String) -> Unit = {}, onTapInvite: () -> Unit = {}, onRefresh: () -> Unit = {}, ) { - val postItems = contentState.collectAsLazyPagingItems() - // val memberItems = familyListState.collectAsLazyPagingItems() - var isRefreshing by remember { mutableStateOf(false) } - LaunchedEffect(postItems.loadState.refresh) { - if (isRefreshing && - postItems.loadState.refresh is LoadState.NotLoading - ) { + val mainPageModel by mainPageState.collectAsState() + val postItems = if(mainPageModel.isReady()) + if(postViewTypeState.value == PostType.MISSION) mainPageModel.data.missionFeeds + else mainPageModel.data.survivalFeeds + else emptyList() + var isRefreshing by remember { mutableStateOf(true) } + LaunchedEffect(mainPageModel) { + if (mainPageModel.isReady()) { isRefreshing = false } } @@ -66,7 +70,6 @@ fun HomePageContent( onRefresh = { if (isRefreshing) return@HomePageFeedGrid isRefreshing = true - postItems.refresh() onRefresh() } ) { @@ -75,8 +78,7 @@ fun HomePageContent( span = { GridItemSpan(2) }) { Column { HomePageStoryBar( - postTopStateFlow = postTopState, - meStateFlow = meState, + mainPageState = mainPageState, onTapProfile = onTapProfile, onTapInvite = onTapInvite, ) @@ -91,29 +93,31 @@ fun HomePageContent( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center ) { - PostTypeSwitchButton() + PostTypeSwitchButton( + isLocked = mainPageModel.isReady() && !mainPageModel.data.isMissionUnlocked, + state = postViewTypeState + ) } Spacer(modifier = Modifier.height(8.dp)) } } - if (postItems.itemCount > 0) { + if (postItems.isNotEmpty()) { items( - count = postItems.itemCount, + count = postItems.size, key = { - postItems[it]!!.post.postId + postItems[it].postId } ) { - val item = postItems[it] ?: throw RuntimeException() + val item = postItems[it] HomePageFeedElement( modifier = Modifier.animateItemPlacement( animationSpec = tween(300), ), - imageUrl = item.post.imageUrl, - writerName = item.writer.name, - time = gapBetweenNow(time = item.post.createdAt), - onTap = { onTapContent(item.post) }, - postContent = item.post.content, + imageUrl = item.imageUrl, + writerName = item.authorName, + time = gapBetweenNow(time = item.createdAt), + onTap = { onTapContent(item.postId) }, ) } } else { diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageFeedElement.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageFeedElement.kt index 9d92d10..2b8c25f 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageFeedElement.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageFeedElement.kt @@ -32,7 +32,6 @@ fun HomePageFeedElement( modifier: Modifier, imageUrl: String, writerName: String, - postContent: String, time: String, onTap: () -> Unit = {}, ) { diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageStoryBar.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageStoryBar.kt index 7f14ff7..15feb35 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageStoryBar.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageStoryBar.kt @@ -32,6 +32,8 @@ import androidx.compose.ui.unit.dp import com.no5ing.bbibbi.R import com.no5ing.bbibbi.data.model.APIResponse import com.no5ing.bbibbi.data.model.member.Member +import com.no5ing.bbibbi.data.model.view.MainPageModel +import com.no5ing.bbibbi.data.model.view.MainPageTopBarModel import com.no5ing.bbibbi.presentation.component.CircleProfileImage import com.no5ing.bbibbi.presentation.feature.uistate.family.MainFeedStoryElementUiState import com.no5ing.bbibbi.presentation.theme.bbibbiScheme @@ -41,17 +43,18 @@ import kotlinx.coroutines.flow.StateFlow @Composable fun HomePageStoryBar( - postTopStateFlow: StateFlow>>, - meStateFlow: StateFlow>, - onTapProfile: (Member) -> Unit = {}, +// postTopStateFlow: StateFlow>>, +// meStateFlow: StateFlow>, + mainPageState: StateFlow>, + onTapProfile: (String) -> Unit = {}, onTapInvite: () -> Unit = {}, ) { val meId = LocalSessionState.current.memberId - val postTopState by postTopStateFlow.collectAsState() - val meState by meStateFlow.collectAsState() + val mainPageModel by mainPageState.collectAsState() + val items = if (mainPageModel.isReady()) mainPageModel.data.topBarElements else emptyList() //val items = familyListStateFlow.collectAsLazyPagingItems() - if (postTopState.isReady() && postTopState.data.size == 1) { + if (items.size == 1) { HomePageNoFamilyBar( modifier = Modifier .fillMaxWidth() @@ -59,54 +62,52 @@ fun HomePageStoryBar( .padding(horizontal = 16.dp), onTap = onTapInvite, ) - } else if (postTopState.isReady()) { + } else if (items.size > 1) { LazyRow( modifier = Modifier .fillMaxWidth() .padding(top = 24.dp), ) { item { - Spacer(modifier = Modifier.width(20.dp)) + Spacer(modifier = Modifier.width(8.dp)) } - if (meState.isReady() && postTopState.isReady()) { - val item = meState.data - val meData = postTopState.data.indexOfFirst { it.member.memberId == meId } - item { +// if (meState.isReady() && postTopState.isReady()) { +// val item = meState.data +// val meData = postTopState.data.indexOfFirst { it.member.memberId == meId } +// item { +// StoryBarIcon( +// member = item, +// onTap = { +// onTapProfile(item) +// }, +// isMe = true, +// isUploaded = postTopState.data[meData].isUploadedToday, +// rank = meData, +// ) +// } +// } + + + items( + count = items.size, + key = { items[it].memberId } + ) { index -> + val item = items[index] + Row { + Spacer(modifier = Modifier.width(12.dp)) StoryBarIcon( member = item, onTap = { - onTapProfile(item) + onTapProfile(item.memberId) }, - isMe = true, - isUploaded = postTopState.data[meData].isUploadedToday, - rank = meData, + isUploaded = item.displayRank != null, + rank = index, + isMe = item.memberId == meId, ) } } - - items( - count = postTopState.data.size, - key = { postTopState.data[it].member.memberId } - ) { index -> - val item = postTopState.data[index] - if (item.member.memberId != meId) { - Row { - Spacer(modifier = Modifier.width(12.dp)) - StoryBarIcon( - member = item.member, - onTap = { - onTapProfile(item.member) - }, - isUploaded = item.isUploadedToday, - rank = index, - ) - } - } - - } - item { Spacer(modifier = Modifier.width(20.dp)) } @@ -118,7 +119,7 @@ fun HomePageStoryBar( @Composable fun StoryBarIcon( onTap: () -> Unit, - member: Member, + member: MainPageTopBarModel, isMe: Boolean = false, isUploaded: Boolean, rank: Int, @@ -146,7 +147,8 @@ fun StoryBarIcon( .size(64.dp) ) { CircleProfileImage( - member = member, + noImageLetter = member.noImageLetter, + imageUrl = member.imageUrl, size = if (rankColor == null) 64.dp else 62.dp, onTap = onTap, opacity = if (isUploaded) 1.0f else 0.4f @@ -157,7 +159,7 @@ fun StoryBarIcon( modifier = Modifier .size(64.dp) ) { - if (member.isBirthdayToday) { + if (member.shouldShowBirthdayMark) { Image( painter = painterResource(id = R.drawable.birthday_badge), contentDescription = null, @@ -185,7 +187,7 @@ fun StoryBarIcon( } } Text( - text = if (isMe) stringResource(id = R.string.family_me) else member.name, + text = if (isMe) stringResource(id = R.string.family_me) else member.displayName, color = MaterialTheme.bbibbiScheme.textSecondary, overflow = TextOverflow.Ellipsis, maxLines = 1, diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/HomePageController.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/HomePageController.kt index 5e04493..8bcdf3c 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/HomePageController.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/HomePageController.kt @@ -26,10 +26,10 @@ object HomePageController : NavigationDestination( navController.goCalendarPage() }, onTapProfile = { - navController.goProfilePage(it.memberId) + navController.goProfilePage(it) }, onTapContent = { - navController.goPostViewPage(it.postId) + navController.goPostViewPage(it) }, onTapUpload = { navController.goPostUploadPage() diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/MainPageViewModel.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/MainPageViewModel.kt index bb7d691..99c1b35 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/MainPageViewModel.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/MainPageViewModel.kt @@ -1,5 +1,7 @@ package com.no5ing.bbibbi.presentation.feature.view_model +import android.net.Uri +import com.no5ing.bbibbi.data.datasource.local.LocalDataStorage import com.no5ing.bbibbi.data.datasource.network.RestAPI import com.no5ing.bbibbi.data.model.APIResponse import com.no5ing.bbibbi.data.model.APIResponse.Companion.wrapToAPIResponse @@ -12,7 +14,18 @@ import javax.inject.Inject @HiltViewModel class MainPageViewModel @Inject constructor( private val restAPI: RestAPI, + private val localDataStorage: LocalDataStorage, ) : BaseViewModel>() { + var shouldDisplayWidgetPopup = localDataStorage.getAndRemoveWidgetPopupPeriod() + + fun getAndDeleteTemporaryUri(): Uri? { + val uri = localDataStorage.getTemporaryUri() + if (uri != null) { + localDataStorage.clearTemporaryUri() + } + return uri?.let { Uri.parse(it) } + } + override fun initState(): APIResponse { return APIResponse.idle() } From 7f7709df2212ef72228216c67db0853d712f3d2e Mon Sep 17 00:00:00 2001 From: ChuYong Date: Fri, 19 Apr 2024 14:54:22 +0900 Subject: [PATCH 05/26] =?UTF-8?q?feat:=20=EB=9E=9C=EB=94=A9=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EB=B3=80=EA=B2=BD=EC=82=AC=ED=95=AD=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../landing/onboarding/OnBoardingPageState.kt | 2 +- .../onboarding/OnBoardingFourthPage.kt | 51 ++ .../view/landing/onboarding/OnBoardingPage.kt | 5 +- app/src/main/res/drawable/landing_four.xml | 302 +++++++++ app/src/main/res/drawable/landing_one.xml | 605 +++++++++--------- app/src/main/res/drawable/landing_three.xml | 553 ++++++++-------- app/src/main/res/values-en/strings.xml | 5 +- app/src/main/res/values-ja/strings.xml | 5 +- app/src/main/res/values/strings.xml | 7 +- 9 files changed, 929 insertions(+), 606 deletions(-) create mode 100644 app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/landing/onboarding/OnBoardingFourthPage.kt create mode 100644 app/src/main/res/drawable/landing_four.xml diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/state/landing/onboarding/OnBoardingPageState.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/state/landing/onboarding/OnBoardingPageState.kt index b165ac3..614589b 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/state/landing/onboarding/OnBoardingPageState.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/state/landing/onboarding/OnBoardingPageState.kt @@ -19,7 +19,7 @@ fun rememberOnBoardingPageState( currentPageState: MutableState = remember { mutableIntStateOf(0) }, - pagerState: PagerState = rememberPagerState { 3 } + pagerState: PagerState = rememberPagerState { 4 } ): OnBoardingPageState = OnBoardingPageState( currentPageState = currentPageState, pagerState = pagerState, diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/landing/onboarding/OnBoardingFourthPage.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/landing/onboarding/OnBoardingFourthPage.kt new file mode 100644 index 0000000..401ea2c --- /dev/null +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/landing/onboarding/OnBoardingFourthPage.kt @@ -0,0 +1,51 @@ +package com.no5ing.bbibbi.presentation.feature.view.landing.onboarding + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +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.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.no5ing.bbibbi.R +import com.no5ing.bbibbi.presentation.theme.bbibbiScheme +import com.no5ing.bbibbi.presentation.theme.bbibbiTypo + +@Composable +fun OnBoardingFourthPage() { + Column( + modifier = Modifier.fillMaxWidth() + ) { + Spacer(modifier = Modifier.height(40.dp)) + Text( + text = stringResource(id = R.string.onboarding_fourth_title), + style = MaterialTheme.bbibbiTypo.headOne, + color = MaterialTheme.bbibbiScheme.backgroundPrimary, + modifier = Modifier.padding(horizontal = 20.dp) + ) + Spacer(modifier = Modifier.height(129.dp)) + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 42.dp) + ) { + Image( + painter = painterResource(R.drawable.landing_four), + contentDescription = null, // 필수 param + modifier = Modifier + .fillMaxWidth(), + contentScale = ContentScale.FillWidth + ) + } + + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/landing/onboarding/OnBoardingPage.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/landing/onboarding/OnBoardingPage.kt index 3fc8409..408ea4d 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/landing/onboarding/OnBoardingPage.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/landing/onboarding/OnBoardingPage.kt @@ -86,6 +86,7 @@ fun OnBoardingPage( 0 -> OnBoardingFirstPage() 1 -> OnBoardingSecondPage() 2 -> OnBoardingThirdPage() + 3 -> OnBoardingFourthPage() } } } @@ -109,7 +110,7 @@ fun OnBoardingPage( .padding(vertical = 12.dp), contentPadding = PaddingValues(vertical = 18.dp), onClick = { - if (onBoardingPageState.pagerState.currentPage >= 2) { + if (onBoardingPageState.pagerState.currentPage >= 3) { if (!perm.status.isGranted) { perm.launchPermissionRequest() } else { @@ -125,7 +126,7 @@ fun OnBoardingPage( }, buttonColor = MaterialTheme.bbibbiScheme.backgroundPrimary, textColor = MaterialTheme.bbibbiScheme.white, - isActive = onBoardingPageState.pagerState.currentPage == 2, + isActive = onBoardingPageState.pagerState.currentPage == 3, byPassCtaIgnore = true, ) } diff --git a/app/src/main/res/drawable/landing_four.xml b/app/src/main/res/drawable/landing_four.xml new file mode 100644 index 0000000..4279cb6 --- /dev/null +++ b/app/src/main/res/drawable/landing_four.xml @@ -0,0 +1,302 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/landing_one.xml b/app/src/main/res/drawable/landing_one.xml index 2b66a47..f2d12ba 100644 --- a/app/src/main/res/drawable/landing_one.xml +++ b/app/src/main/res/drawable/landing_one.xml @@ -3,302 +3,311 @@ android:height="355dp" android:viewportWidth="297" android:viewportHeight="355"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/landing_three.xml b/app/src/main/res/drawable/landing_three.xml index 24bf8e4..a55fb18 100644 --- a/app/src/main/res/drawable/landing_three.xml +++ b/app/src/main/res/drawable/landing_three.xml @@ -1,302 +1,259 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + - - - - - - - - - - - - - + android:strokeColor="#242427"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 7e255f3..efe250b 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -57,9 +57,10 @@ Invite family to BiBBi Your Family - At 12pm every day, receive a notification and send a photo to your family + At 10am every day, receive a notification and send a photo to your family Check your family\'s daily life at a glance with the home screen widget - Don\'t miss the notifications that come only twice a day, make sure to allow them! + When half of the family reports survival,\nyou can take a new mission photo! + Don\'t miss the notifications that come only twice a day, make sure to allow them! Allow notifications and create a family room Allow notifications and enter Me diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index d31a7b6..d052bce 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -57,9 +57,10 @@ ビープに家族を招待 あなたの家族 - 毎日12時に通知を受け取り、家族に写真を送信してください + 毎日10時に通知を受け取り、家族に写真を送信してください ホームスクリーンのウィジェットで家族の日常を一目で確認できます - 1日に2回しか来ない通知を逃さないように、必ず許可してください! + 家族の半分が生き残って報告すれば,\n新しいミッション写真を撮ることができますよ! + 1日に2回しか来ない通知を逃さないように、必ず許可してください! 通知を許可して家族ルームを作成 通知を許可して入室 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1e4d209..85d3e59 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -57,9 +57,10 @@ 삐삐에 가족 초대하기 당신의 가족 - 매일 낮 12시, 알림을 받으면\n가족에게 사진을 보내세요 - 홈 화면 위젯으로\n가족의 일상을 한 눈에 확인해요 - 하루에 오직 두번 오는 알림,\n놓치지 않으려면 꼭 허용하세요! + 매일 아침 10시, 알림을 받으면\n가족에게 1장의 사진을 보내세요 + 홈 화면에 삐삐 위젯을 추가해\n가족의 일상을 한 눈에 확인해요 + 가족 절반이 생존 신고하면,\n새로운 미션 사진을 찍을 수 있어요! + 하루에 오직 두번 오는 알림,\n놓치지 않으려면 꼭 허용하세요! 알림 허용하고 가족 방 생성하기 알림 허용하고 입장하기 Me From 60b3510d5d81802cd4991c15881c7f53d56593c1 Mon Sep 17 00:00:00 2001 From: ChuYong Date: Fri, 19 Apr 2024 15:51:10 +0900 Subject: [PATCH 06/26] =?UTF-8?q?feat:=20=EC=B0=8C=EB=A5=B4=EA=B8=B0=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bbibbi/data/datasource/network/RestAPI.kt | 5 + .../presentation/component/SnackBarHost.kt | 11 ++ .../view/common/PostTypeSwitchButton.kt | 13 +- .../feature/view/main/home/HomePage.kt | 27 +--- .../feature/view/main/home/HomePageContent.kt | 5 + .../view/main/home/HomePageStoryBar.kt | 45 +++--- .../feature/view/main/home/TryPickPopup.kt | 132 ++++++++++++++++++ .../main/HomePageController.kt | 55 +++++++- .../feature/view_model/MainPageViewModel.kt | 7 + .../view_model/members/PickMemberViewModel.kt | 35 +++++ app/src/main/res/drawable/airplane.xml | 10 ++ app/src/main/res/drawable/lying_bibbi.xml | 128 +++++++++++++++++ app/src/main/res/drawable/pick_icon.xml | 18 +++ app/src/main/res/values-en/strings.xml | 3 + app/src/main/res/values-ja/strings.xml | 3 + app/src/main/res/values/strings.xml | 3 + 16 files changed, 454 insertions(+), 46 deletions(-) create mode 100644 app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/TryPickPopup.kt create mode 100644 app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/members/PickMemberViewModel.kt create mode 100644 app/src/main/res/drawable/airplane.xml create mode 100644 app/src/main/res/drawable/lying_bibbi.xml create mode 100644 app/src/main/res/drawable/pick_icon.xml diff --git a/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt b/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt index f3fe40f..4a78f29 100644 --- a/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt +++ b/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt @@ -145,6 +145,11 @@ interface RestAPI { @Path("realEmojiId") realEmojiId: String, @Body body: UpdateMemberRealEmojiRequest, ): ApiResponse + + @POST("v1/members/{memberId}/pick") + suspend fun pickMember( + @Path("memberId") memberId: String, + ): ApiResponse } /** diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/component/SnackBarHost.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/component/SnackBarHost.kt index 0e7886c..5ae70d2 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/component/SnackBarHost.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/component/SnackBarHost.kt @@ -55,6 +55,7 @@ fun CustomSnackBarHost( snackBarInfo -> InfoIcon() snackBarCamera -> CameraIcon() snackBarFire -> FireIcon() + snackBarPick -> PickIcon() else -> Icon( imageVector = Icons.Default.Email, tint = MaterialTheme.bbibbiScheme.white, @@ -73,6 +74,7 @@ fun CustomSnackBarHost( const val snackBarWarning = "warning" const val snackBarInfo = "info" +const val snackBarPick = "pick" const val snackBarCamera = "camera" const val snackBarSuccess = "info" const val snackBarFire = "fire" @@ -95,6 +97,15 @@ private fun FireIcon() { ) } +@Composable +private fun PickIcon() { + Icon( + painter = painterResource(id = R.drawable.airplane), + tint = MaterialTheme.bbibbiScheme.mainYellow, + contentDescription = null + ) +} + @Composable private fun CameraIcon() { Icon( diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/common/PostTypeSwitchButton.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/common/PostTypeSwitchButton.kt index e24cfd9..37c25bf 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/common/PostTypeSwitchButton.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/common/PostTypeSwitchButton.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -86,12 +87,16 @@ fun PostTypeSwitchButton( .background(MaterialTheme.bbibbiScheme.iconSelected, RoundedCornerShape(40.dp)) ) Row( - modifier = Modifier.fillMaxWidth().padding(start = 24.dp, - end = if(isLocked) 14.dp else 24.dp), + modifier = Modifier + .fillMaxWidth() + .padding( + start = 24.dp, + end = if (isLocked) 14.dp else 24.dp + ), horizontalArrangement = Arrangement.SpaceBetween, ) { Text( - text = "생존", + text = stringResource(id = R.string.post_type_survival), style = MaterialTheme.bbibbiTypo.bodyTwoBold, color = survivalButtonColor ) @@ -100,7 +105,7 @@ fun PostTypeSwitchButton( horizontalArrangement = Arrangement.spacedBy(2.dp) ) { Text( - text = "미션", + text = stringResource(id = R.string.post_type_mission), style = MaterialTheme.bbibbiTypo.bodyTwoBold, color = missionButtonColor, ) diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePage.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePage.kt index 3955334..93feb82 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePage.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePage.kt @@ -21,6 +21,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.no5ing.bbibbi.R import com.no5ing.bbibbi.data.model.APIResponse import com.no5ing.bbibbi.data.model.post.PostType +import com.no5ing.bbibbi.data.model.view.MainPageTopBarModel import com.no5ing.bbibbi.data.repository.Arguments import com.no5ing.bbibbi.presentation.component.BBiBBiPreviewSurface import com.no5ing.bbibbi.presentation.component.BBiBBiSurface @@ -28,7 +29,6 @@ import com.no5ing.bbibbi.presentation.component.BackToExitHandler import com.no5ing.bbibbi.presentation.feature.view.common.CustomAlertDialog import com.no5ing.bbibbi.presentation.feature.view_model.MainPageViewModel import com.no5ing.bbibbi.presentation.theme.bbibbiScheme -import com.no5ing.bbibbi.util.LocalSessionState import com.no5ing.bbibbi.util.gapUntilNext import kotlinx.coroutines.flow.MutableStateFlow @@ -43,8 +43,8 @@ fun HomePage( onTapUpload: () -> Unit = {}, onTapInvite: () -> Unit = {}, onUnsavedPost: (Uri) -> Unit = {}, + onTapPick: (MainPageTopBarModel) -> Unit = {}, ) { - val memberId = LocalSessionState.current.memberId val mainPageState = mainPageViewModel.uiState.collectAsState() val unsavedDialogUri = remember { mutableStateOf(null) } val unsavedDialogEnabled = remember { mutableStateOf(false) } @@ -64,30 +64,12 @@ fun HomePage( } BackToExitHandler() LaunchedEffect(Unit) { - // isMeUploadedTodayViewModel.invoke(Arguments(arguments = mapOf("memberId" to memberId))) val tempUri = mainPageViewModel.getAndDeleteTemporaryUri() if (tempUri != null) { unsavedDialogUri.value = tempUri unsavedDialogEnabled.value = true } mainPageViewModel.invoke(Arguments()) - -// if (familyPostsViewModel.isInitialize()) { -// // familyMembersViewModel.invoke(Arguments()) -// mainPageViewModel.invoke(Arguments()) -// // retrieveMeViewModel.invoke(Arguments()) -// familyPostTopViewModel.invoke(Arguments())// TODO -// familyPostsViewModel.invoke( -// Arguments( -// arguments = mapOf( -// "date" to todayAsString(), -// ) -// ) -// ) -// } else { -// familyPostTopViewModel.invoke(Arguments()) -// familyPostsViewModel.refresh() -// } } BBiBBiSurface( @@ -111,9 +93,11 @@ fun HomePage( onTapContent = onTapContent, onTapProfile = onTapProfile, onTapInvite = onTapInvite, + onTapPick = onTapPick, onRefresh = { mainPageViewModel.invoke(Arguments()) - } + }, + deferredPickStateSet = mainPageViewModel.deferredPickMembersSet ) } HomePageUploadButton( @@ -142,6 +126,7 @@ fun HomePagePreview() { HomePageTopBar() HomePageContent( mainPageState = MutableStateFlow(APIResponse.idle()), + deferredPickStateSet = MutableStateFlow(emptySet()) ) } HomePageUploadButton() diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt index b1a6508..482f743 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt @@ -36,6 +36,7 @@ import com.no5ing.bbibbi.data.model.member.Member import com.no5ing.bbibbi.data.model.post.Post import com.no5ing.bbibbi.data.model.post.PostType import com.no5ing.bbibbi.data.model.view.MainPageModel +import com.no5ing.bbibbi.data.model.view.MainPageTopBarModel import com.no5ing.bbibbi.presentation.feature.uistate.family.MainFeedStoryElementUiState import com.no5ing.bbibbi.presentation.feature.uistate.family.MainFeedUiState import com.no5ing.bbibbi.presentation.feature.view.common.PostTypeSwitchButton @@ -47,9 +48,11 @@ import kotlinx.coroutines.flow.StateFlow @Composable fun HomePageContent( mainPageState: StateFlow>, + deferredPickStateSet: StateFlow>, postViewTypeState: MutableState = remember { mutableStateOf(PostType.SURVIVAL) }, onTapContent: (String) -> Unit = {}, onTapProfile: (String) -> Unit = {}, + onTapPick: (MainPageTopBarModel) -> Unit = {}, onTapInvite: () -> Unit = {}, onRefresh: () -> Unit = {}, ) { @@ -81,6 +84,8 @@ fun HomePageContent( mainPageState = mainPageState, onTapProfile = onTapProfile, onTapInvite = onTapInvite, + onTapPick = onTapPick, + deferredPickStateSet = deferredPickStateSet, ) Spacer(modifier = Modifier.height(24.dp)) HorizontalDivider( diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageStoryBar.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageStoryBar.kt index 15feb35..009ae26 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageStoryBar.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageStoryBar.kt @@ -43,16 +43,16 @@ import kotlinx.coroutines.flow.StateFlow @Composable fun HomePageStoryBar( -// postTopStateFlow: StateFlow>>, -// meStateFlow: StateFlow>, mainPageState: StateFlow>, + deferredPickStateSet: StateFlow>, onTapProfile: (String) -> Unit = {}, + onTapPick: (MainPageTopBarModel) -> Unit = {}, onTapInvite: () -> Unit = {}, ) { val meId = LocalSessionState.current.memberId val mainPageModel by mainPageState.collectAsState() + val deferredPickSet = deferredPickStateSet.collectAsState() val items = if (mainPageModel.isReady()) mainPageModel.data.topBarElements else emptyList() - //val items = familyListStateFlow.collectAsLazyPagingItems() if (items.size == 1) { HomePageNoFamilyBar( @@ -72,23 +72,6 @@ fun HomePageStoryBar( Spacer(modifier = Modifier.width(8.dp)) } -// if (meState.isReady() && postTopState.isReady()) { -// val item = meState.data -// val meData = postTopState.data.indexOfFirst { it.member.memberId == meId } -// item { -// StoryBarIcon( -// member = item, -// onTap = { -// onTapProfile(item) -// }, -// isMe = true, -// isUploaded = postTopState.data[meData].isUploadedToday, -// rank = meData, -// ) -// } -// } - - items( count = items.size, key = { items[it].memberId } @@ -104,6 +87,10 @@ fun HomePageStoryBar( isUploaded = item.displayRank != null, rank = index, isMe = item.memberId == meId, + isInDeferredPickState = deferredPickSet.value.contains(item.memberId), + onTapPick = { + onTapPick(item) + } ) } } @@ -119,7 +106,9 @@ fun HomePageStoryBar( @Composable fun StoryBarIcon( onTap: () -> Unit, + onTapPick: () -> Unit, member: MainPageTopBarModel, + isInDeferredPickState: Boolean = false, isMe: Boolean = false, isUploaded: Boolean, rank: Int, @@ -185,6 +174,22 @@ fun StoryBarIcon( ) } } + if (member.shouldShowPickIcon && !isInDeferredPickState) { + Box( + contentAlignment = Alignment.BottomEnd, + modifier = Modifier + .size(64.dp) + ) { + Image( + painter = painterResource(id = R.drawable.pick_icon), + contentDescription = null, + modifier = Modifier + .height(32.dp) + .clickable { onTapPick() } + .offset(x = 6.dp, y = 4.dp), + ) + } + } } Text( text = if (isMe) stringResource(id = R.string.family_me) else member.displayName, diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/TryPickPopup.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/TryPickPopup.kt new file mode 100644 index 0000000..2e06260 --- /dev/null +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/TryPickPopup.kt @@ -0,0 +1,132 @@ +package com.no5ing.bbibbi.presentation.feature.view.main.home + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +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.shape.RoundedCornerShape +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.AlertDialogDefaults +import androidx.compose.material3.BasicAlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties +import com.no5ing.bbibbi.R +import com.no5ing.bbibbi.presentation.feature.view.common.AlertDialogContent +import com.no5ing.bbibbi.presentation.feature.view.common.AlertDialogFlowRow +import com.no5ing.bbibbi.presentation.feature.view.common.ButtonsCrossAxisSpacing +import com.no5ing.bbibbi.presentation.feature.view.common.ButtonsMainAxisSpacing +import com.no5ing.bbibbi.presentation.theme.bbibbiScheme +import com.no5ing.bbibbi.presentation.theme.bbibbiTypo + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TryPickPopup( + enabledState: Boolean = false, + targetNickname: String = "", + onTapNow: () -> Unit = {}, + onTapLater: () -> Unit = {}, +) { + if (enabledState) { + BasicAlertDialog( + onDismissRequest = onTapLater, + modifier = Modifier, + properties = DialogProperties() + ) { + AlertDialogContent( + textPadding = PaddingValues(0.dp), + buttons = { + AlertDialogFlowRow( + mainAxisSpacing = ButtonsMainAxisSpacing, + crossAxisSpacing = 8.dp + ) { + Button( + onClick = onTapNow, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.bbibbiScheme.mainYellow + ), + shape = RoundedCornerShape(10.dp), + modifier = Modifier + .height(44.dp) + .fillMaxWidth() + ) { + Text( + "지금 하기", + style = MaterialTheme.bbibbiTypo.bodyOneBold, + color = Color(0xff242427) + ) + } + Button( + onClick = onTapLater, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.bbibbiScheme.button + ), + shape = RoundedCornerShape(10.dp), + modifier = Modifier + .height(44.dp) + .fillMaxWidth() + ) { + Text( + "다음에 하기", + style = MaterialTheme.bbibbiTypo.bodyOneBold, + color = MaterialTheme.bbibbiScheme.icon + ) + } + } + }, + icon = null, + title = { + Text( + "생존 확인하기", + color = MaterialTheme.bbibbiScheme.iconSelected, + style = MaterialTheme.bbibbiTypo.headTwoBold, + ) + }, + text = { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth() + ) { + Text( + "${targetNickname}님의 생존 여부를 물어볼까요?\n지금 알림이 전송됩니다.", + color = MaterialTheme.bbibbiScheme.textSecondary, + style = MaterialTheme.bbibbiTypo.bodyTwoRegular, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.height(24.dp)) + Image( + painter = painterResource(id = R.drawable.lying_bibbi), + contentDescription = null, + ) + } + + }, + shape = RoundedCornerShape(14.dp), + containerColor = MaterialTheme.bbibbiScheme.backgroundPrimary, + tonalElevation = AlertDialogDefaults.TonalElevation, + iconContentColor = AlertDialogDefaults.iconContentColor, + titleContentColor = AlertDialogDefaults.titleContentColor, + textContentColor = AlertDialogDefaults.textContentColor, + modifier = Modifier.padding(0.dp) + ) + + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/HomePageController.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/HomePageController.kt index 8bcdf3c..8297374 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/HomePageController.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/HomePageController.kt @@ -1,9 +1,21 @@ package com.no5ing.bbibbi.presentation.feature.view_controller.main import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavBackStackEntry import androidx.navigation.NavHostController +import com.no5ing.bbibbi.data.model.view.MainPageTopBarModel +import com.no5ing.bbibbi.data.repository.Arguments +import com.no5ing.bbibbi.presentation.component.showSnackBarWithDismiss +import com.no5ing.bbibbi.presentation.component.snackBarPick import com.no5ing.bbibbi.presentation.feature.view.main.home.HomePage +import com.no5ing.bbibbi.presentation.feature.view.main.home.TryPickPopup import com.no5ing.bbibbi.presentation.feature.view_controller.CameraViewPageController.goCameraViewPage import com.no5ing.bbibbi.presentation.feature.view_controller.NavigationDestination import com.no5ing.bbibbi.presentation.feature.view_controller.main.CalendarPageController.goCalendarPage @@ -12,12 +24,48 @@ import com.no5ing.bbibbi.presentation.feature.view_controller.main.PostReUploadP import com.no5ing.bbibbi.presentation.feature.view_controller.main.PostUploadPageController.goPostUploadPage import com.no5ing.bbibbi.presentation.feature.view_controller.main.PostViewPageController.goPostViewPage import com.no5ing.bbibbi.presentation.feature.view_controller.main.ProfilePageController.goProfilePage +import com.no5ing.bbibbi.presentation.feature.view_model.MainPageViewModel +import com.no5ing.bbibbi.presentation.feature.view_model.members.PickMemberViewModel +import com.no5ing.bbibbi.util.LocalSnackbarHostState object HomePageController : NavigationDestination( route = mainHomePageRoute, ) { @Composable override fun Render(navController: NavHostController, backStackEntry: NavBackStackEntry) { + val snackBarHost = LocalSnackbarHostState.current + val pickMemberViewModel = hiltViewModel() + val mainPageViewModel = hiltViewModel() + var isPickDialogVisible by remember { mutableStateOf(false) } + var tryPickDialogMember by remember { mutableStateOf(null) } + val pickState = pickMemberViewModel.uiState.collectAsState() + LaunchedEffect(pickState.value.status) { + if (!pickState.value.isIdle()) { + mainPageViewModel.invoke(Arguments()) + } + } + TryPickPopup( + enabledState = isPickDialogVisible, + targetNickname = tryPickDialogMember?.displayName ?: "", + onTapNow = { + isPickDialogVisible = false + mainPageViewModel.addPickMembersSet(tryPickDialogMember?.memberId ?: "") + snackBarHost.showSnackBarWithDismiss( + message = "${tryPickDialogMember?.displayName?:""}님에게 생존신고 알림을 보냈어요", + actionLabel = snackBarPick, + ) + pickMemberViewModel.invoke( + Arguments( + arguments = mapOf( + "memberId" to (tryPickDialogMember?.memberId ?: "") + ) + ) + ) + }, + onTapLater = { + isPickDialogVisible = false + } + ) HomePage( onTapLeft = { navController.goFamilyListPage() @@ -40,7 +88,12 @@ object HomePageController : NavigationDestination( }, onUnsavedPost = { navController.goPostReUploadPage(it.toString()) - } + }, + onTapPick = { + tryPickDialogMember = it + isPickDialogVisible = true + }, + mainPageViewModel = mainPageViewModel, ) } diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/MainPageViewModel.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/MainPageViewModel.kt index 99c1b35..14b611c 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/MainPageViewModel.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/MainPageViewModel.kt @@ -9,6 +9,7 @@ import com.no5ing.bbibbi.data.model.view.MainPageModel import com.no5ing.bbibbi.data.repository.Arguments import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow import javax.inject.Inject @HiltViewModel @@ -17,6 +18,7 @@ class MainPageViewModel @Inject constructor( private val localDataStorage: LocalDataStorage, ) : BaseViewModel>() { var shouldDisplayWidgetPopup = localDataStorage.getAndRemoveWidgetPopupPeriod() + val deferredPickMembersSet = MutableStateFlow(setOf()) fun getAndDeleteTemporaryUri(): Uri? { val uri = localDataStorage.getTemporaryUri() @@ -26,6 +28,10 @@ class MainPageViewModel @Inject constructor( return uri?.let { Uri.parse(it) } } + fun addPickMembersSet(memberId: String) { + deferredPickMembersSet.value = deferredPickMembersSet.value + memberId + } + override fun initState(): APIResponse { return APIResponse.idle() } @@ -34,6 +40,7 @@ class MainPageViewModel @Inject constructor( withMutexScope(Dispatchers.IO) { val mainResult = restAPI.getViewApi().getMainView() val apiResult = mainResult.wrapToAPIResponse() + deferredPickMembersSet.value = emptySet() setState(apiResult) } } diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/members/PickMemberViewModel.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/members/PickMemberViewModel.kt new file mode 100644 index 0000000..f428faa --- /dev/null +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/members/PickMemberViewModel.kt @@ -0,0 +1,35 @@ +package com.no5ing.bbibbi.presentation.feature.view_model.members + +import com.no5ing.bbibbi.data.datasource.network.RestAPI +import com.no5ing.bbibbi.data.datasource.network.response.DefaultResponse +import com.no5ing.bbibbi.data.model.APIResponse +import com.no5ing.bbibbi.data.model.APIResponse.Companion.loading +import com.no5ing.bbibbi.data.model.APIResponse.Companion.wrapToAPIResponse +import com.no5ing.bbibbi.data.repository.Arguments +import com.no5ing.bbibbi.presentation.feature.view_model.BaseViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import javax.inject.Inject + +@HiltViewModel +class PickMemberViewModel @Inject constructor( + private val restAPI: RestAPI, +) : BaseViewModel>() { + override fun initState(): APIResponse { + return APIResponse.idle() + } + + override fun invoke(arguments: Arguments) { + val memberId = arguments.get("memberId") ?: throw RuntimeException() + setState(loading()) + withMutexScope(Dispatchers.IO) { + val result = restAPI + .getMemberApi() + .pickMember( + memberId = memberId, + ).wrapToAPIResponse() + setState(result) + } + } + +} \ No newline at end of file diff --git a/app/src/main/res/drawable/airplane.xml b/app/src/main/res/drawable/airplane.xml new file mode 100644 index 0000000..ad23aaf --- /dev/null +++ b/app/src/main/res/drawable/airplane.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/lying_bibbi.xml b/app/src/main/res/drawable/lying_bibbi.xml new file mode 100644 index 0000000..0f4a78c --- /dev/null +++ b/app/src/main/res/drawable/lying_bibbi.xml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/pick_icon.xml b/app/src/main/res/drawable/pick_icon.xml new file mode 100644 index 0000000..afcfeb6 --- /dev/null +++ b/app/src/main/res/drawable/pick_icon.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index efe250b..6aea245 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -191,4 +191,7 @@ Have you added the widget? With the widget on the home screen\nYou can quickly grasp the news of the family Registered At %1$s + + Survival + Mission \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index d052bce..0b20c64 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -191,4 +191,7 @@ ウィジェットを追加しましたか? ホーム画面のウィジェットで\n家族のニュースをすぐに把握できます %1$s に登録済み + + 生存 + ミッション \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 85d3e59..46e556f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -191,4 +191,7 @@ 위젯 추가하셨나요? 홈 화면에서 위젯으로\n가족의 소식을 한눈에 파악할 수 있어요 %1$s 가입 + + 생존 + 미션 \ No newline at end of file From a3d5d0102d01a772bd37448a02131cada4849642 Mon Sep 17 00:00:00 2001 From: ChuYong Date: Fri, 19 Apr 2024 15:55:09 +0900 Subject: [PATCH 07/26] =?UTF-8?q?chore:=20Sentry=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 18 ------------------ .../com/no5ing/bbibbi/BBiBBiApplication.kt | 11 ----------- .../feature/view/main/home/HomePageTopBar.kt | 1 - .../view_controller/NavigationDestination.kt | 6 +----- 4 files changed, 1 insertion(+), 35 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0f08a6b..921b208 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -9,8 +9,6 @@ plugins { id("com.google.gms.google-services") id("com.google.firebase.crashlytics") id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") - id("io.sentry.android.gradle") version "4.3.0" - id("io.sentry.kotlin.compiler.gradle") version "4.3.0" } java { @@ -30,20 +28,6 @@ val secretProperties = Properties().apply { load(secretFile.inputStream()) } -sentry { - debug = false - org = "bibbi" - projectName = "android" - authToken = secretProperties["sentryAuthToken"]?.toString() - url = secretProperties["sentryUrl"]?.toString() - - includeProguardMapping = true - autoUploadProguardMapping = true - autoInstallation { - enabled = false - } -} - android { namespace = "com.no5ing.bbibbi" @@ -205,6 +189,4 @@ dependencies { kapt("com.google.dagger:dagger-compiler:2.49") kapt("com.google.dagger:hilt-android-compiler:2.49") testImplementation("junit:junit:4.13.2") - implementation("io.sentry:sentry-android:7.4.0") - implementation("io.sentry:sentry-compose-android:7.4.0") } diff --git a/app/src/main/java/com/no5ing/bbibbi/BBiBBiApplication.kt b/app/src/main/java/com/no5ing/bbibbi/BBiBBiApplication.kt index 05cd3cf..7ac53d2 100644 --- a/app/src/main/java/com/no5ing/bbibbi/BBiBBiApplication.kt +++ b/app/src/main/java/com/no5ing/bbibbi/BBiBBiApplication.kt @@ -11,7 +11,6 @@ import com.google.firebase.appcheck.appCheck import com.google.firebase.appcheck.playintegrity.PlayIntegrityAppCheckProviderFactory import com.kakao.sdk.common.KakaoSdk import dagger.hilt.android.HiltAndroidApp -import io.sentry.android.core.SentryAndroid import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import timber.log.Timber @@ -32,16 +31,6 @@ class BBiBBiApplication : Application(), ImageLoaderFactory { PlayIntegrityAppCheckProviderFactory.getInstance(), ) KakaoSdk.init(this, BuildConfig.kakaoApiKey) - SentryAndroid.init(this) { options -> - options.dsn = BuildConfig.sentryDsn - options.isEnableUserInteractionTracing = true - options.isEnableUserInteractionBreadcrumbs = true - options.isAttachViewHierarchy = true - options.sampleRate = 1.0 - options.tracesSampleRate = 0.6 - options.isAttachScreenshot = true - options.environment = BuildConfig.BUILD_TYPE - } } override fun newImageLoader(): ImageLoader { diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageTopBar.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageTopBar.kt index 42ebc42..d5ee088 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageTopBar.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageTopBar.kt @@ -19,7 +19,6 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import com.no5ing.bbibbi.R import com.no5ing.bbibbi.presentation.theme.bbibbiScheme -import io.sentry.Sentry @Composable fun HomePageTopBar( diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/NavigationDestination.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/NavigationDestination.kt index f7670a7..c16e796 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/NavigationDestination.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/NavigationDestination.kt @@ -4,7 +4,6 @@ import android.annotation.SuppressLint import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.ui.ExperimentalComposeUiApi @@ -16,7 +15,6 @@ import androidx.navigation.NavDestination import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.compose.composable -import io.sentry.compose.SentryTraced import timber.log.Timber import java.net.URLEncoder @@ -85,9 +83,7 @@ abstract class NavigationDestination( popEnterTransition = popEnterTransition, popExitTransition = popExitTransition, ) { - SentryTraced(tag = destination::class.java.simpleName) { - destination.Render(controller, it) - } + destination.Render(controller, it) } fun NavHostController.popAll() { From 36bffabacc6875c14497cfcf19504726498aa17b Mon Sep 17 00:00:00 2001 From: ChuYong Date: Wed, 24 Apr 2024 18:28:34 +0900 Subject: [PATCH 08/26] =?UTF-8?q?feat:=20=ED=94=BC=EB=93=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=EA=B0=9C=EB=B0=9C=EC=A4=91..=20[skip-ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/dev.yml | 1 + .github/workflows/stage.yml | 1 + .../bbibbi/data/datasource/network/RestAPI.kt | 5 +- .../bbibbi/data/model/view/MainPageModel.kt | 11 +- .../bbibbi/presentation/component/Grid.kt | 54 +++ .../feature/view/main/home/HomePage.kt | 30 +- .../feature/view/main/home/HomePageContent.kt | 338 ++++++++++++++---- .../view/main/home/HomePageUploadButton.kt | 212 ++++++++++- .../view/main/home/UploadCountDownBar.kt | 25 -- .../com/no5ing/bbibbi/util/KotlinExtension.kt | 2 +- app/src/main/res/drawable/chest_bibbi.xml | 239 +++++++++++++ app/src/main/res/drawable/key_icon.xml | 16 + app/src/main/res/values/strings.xml | 4 +- 13 files changed, 813 insertions(+), 125 deletions(-) create mode 100644 app/src/main/java/com/no5ing/bbibbi/presentation/component/Grid.kt create mode 100644 app/src/main/res/drawable/chest_bibbi.xml create mode 100644 app/src/main/res/drawable/key_icon.xml diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 7f33840..28cf0e2 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -11,6 +11,7 @@ on: jobs: deploy-dev: runs-on: macos-latest + if: ${{ !contains(github.event.head_commit.message, '[skip-ci]') }} environment: development steps: - name: 브랜치 가져오기 diff --git a/.github/workflows/stage.yml b/.github/workflows/stage.yml index 950397d..6dba8b8 100644 --- a/.github/workflows/stage.yml +++ b/.github/workflows/stage.yml @@ -11,6 +11,7 @@ on: jobs: deploy-stage: runs-on: macos-latest + if: ${{ !contains(github.event.head_commit.message, '[skip-ci]') }} environment: internal-test steps: - name: 브랜치 가져오기 diff --git a/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt b/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt index 4a78f29..9cebb70 100644 --- a/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt +++ b/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt @@ -37,6 +37,7 @@ import com.no5ing.bbibbi.data.model.post.PostReaction import com.no5ing.bbibbi.data.model.post.PostReactionSummary import com.no5ing.bbibbi.data.model.post.PostRealEmoji import com.no5ing.bbibbi.data.model.view.MainPageModel +import com.no5ing.bbibbi.data.model.view.NightMainPageModel import com.skydoves.sandwich.ApiResponse import retrofit2.http.Body import retrofit2.http.DELETE @@ -307,9 +308,11 @@ interface RestAPI { } interface ViewApi { - @GET("v1/view/main") + @GET("v1/view/main/daytime-page") suspend fun getMainView(): ApiResponse + @GET("v1/view/main/nighttime-page") + suspend fun getNightMainView(): ApiResponse } /** diff --git a/app/src/main/java/com/no5ing/bbibbi/data/model/view/MainPageModel.kt b/app/src/main/java/com/no5ing/bbibbi/data/model/view/MainPageModel.kt index 8db4910..61f2a76 100644 --- a/app/src/main/java/com/no5ing/bbibbi/data/model/view/MainPageModel.kt +++ b/app/src/main/java/com/no5ing/bbibbi/data/model/view/MainPageModel.kt @@ -12,7 +12,14 @@ data class MainPageModel( val isMeUploadedToday: Boolean, val survivalFeeds: List, val missionFeeds: List, - val pickers: List + val pickers: List, + val leftUploadCountUntilMissionUnlock: Int, + val dailyMissionContent: String, +) : Parcelable, BaseModel() + +@Parcelize +data class NightMainPageModel( + val topBarElements: List, ) : Parcelable, BaseModel() @Parcelize @@ -26,7 +33,7 @@ data class MainPageFeedModel( @Parcelize data class MainPagePickerModel( val memberId: String, - val imageUrl: String, + val imageUrl: String?, val displayName: String, ) : Parcelable diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/component/Grid.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/component/Grid.kt new file mode 100644 index 0000000..eeffe52 --- /dev/null +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/component/Grid.kt @@ -0,0 +1,54 @@ +package com.no5ing.bbibbi.presentation.component + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.unit.dp +import com.no5ing.bbibbi.util.dpToPx + +@Composable +fun VerticalGrid( + modifier: Modifier = Modifier, + columns: Int = 2, + content: @Composable () -> Unit +) { + val gap = 3.dp.dpToPx().toInt() + val verticalGap = 16.dp.dpToPx().toInt() + Layout( + content = content, + modifier = modifier + ) { measurables, constraints -> + val itemWidth = (constraints.maxWidth - gap) / columns + // Keep given height constraints, but set an exact width + val itemConstraints = constraints.copy( + minWidth = itemWidth, + maxWidth = itemWidth + ) + // Measure each item with these constraints + val placeables = measurables.map { it.measure(itemConstraints) } + // Track each columns height so we can calculate the overall height + val columnHeights = Array(columns) { 0 } + placeables.forEachIndexed { index, placeable -> + val column = index % columns + columnHeights[column] += placeable.height + } + val height = (columnHeights.maxOrNull()?.plus(verticalGap) ?: constraints.minHeight) + .coerceAtMost(constraints.maxHeight) + layout( + width = constraints.maxWidth, + height = height + ) { + // Track the Y co-ord per column we have placed up to + val columnY = Array(columns) { 0 } + placeables.forEachIndexed { index, placeable -> + val column = index % columns + val row = index / columns + placeable.placeRelative( + x = column * itemWidth + if(column > 0) gap else 0, + y = columnY[column] + if(row > 0) verticalGap else 0, + ) + columnY[column] += placeable.height + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePage.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePage.kt index 93feb82..8c25e89 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePage.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePage.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier @@ -45,6 +46,7 @@ fun HomePage( onUnsavedPost: (Uri) -> Unit = {}, onTapPick: (MainPageTopBarModel) -> Unit = {}, ) { + val postViewType by postViewTypeState val mainPageState = mainPageViewModel.uiState.collectAsState() val unsavedDialogUri = remember { mutableStateOf(null) } val unsavedDialogEnabled = remember { mutableStateOf(false) } @@ -100,13 +102,25 @@ fun HomePage( deferredPickStateSet = mainPageViewModel.deferredPickMembersSet ) } - HomePageUploadButton( - onTap = onTapUpload, - isLoading = !mainPageState.value.isReady(), - isUploadAbleTime = remember { gapUntilNext() > 0 }, - isAlreadyUploaded = !mainPageState.value.isReady() || - mainPageState.value.data.isMeUploadedToday - ) + if (postViewType == PostType.SURVIVAL) { + HomePageSurvivalUploadButton( + onTap = onTapUpload, + isLoading = !mainPageState.value.isReady(), + isUploadAbleTime = remember { gapUntilNext() > 0 }, + isAlreadyUploaded = !mainPageState.value.isReady() || + mainPageState.value.data.isMeUploadedToday, + pickers = if(mainPageState.value.isReady()) mainPageState.value.data.pickers + else emptyList(), + ) + } else { + HomePageMissionUploadButton( + isLoading = mainPageState.value.isLoading(), + isMeUploadedToday = mainPageState.value.isReady() && mainPageState.value.data.isMeUploadedToday, + isMissionUnlocked = mainPageState.value.isReady() && mainPageState.value.data.isMissionUnlocked, + isMeMissionUploaded = false, + ) + } + } } } @@ -129,7 +143,7 @@ fun HomePagePreview() { deferredPickStateSet = MutableStateFlow(emptySet()) ) } - HomePageUploadButton() + HomePageSurvivalUploadButton() } } diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt index 482f743..7a1fc5a 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt @@ -1,24 +1,32 @@ package com.no5ing.bbibbi.presentation.feature.view.main.home -import androidx.compose.animation.core.tween import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column 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.lazy.grid.GridItemSpan +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState -import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -26,25 +34,25 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp -import androidx.paging.LoadState -import androidx.paging.PagingData -import androidx.paging.compose.collectAsLazyPagingItems import com.no5ing.bbibbi.R import com.no5ing.bbibbi.data.model.APIResponse -import com.no5ing.bbibbi.data.model.member.Member -import com.no5ing.bbibbi.data.model.post.Post import com.no5ing.bbibbi.data.model.post.PostType +import com.no5ing.bbibbi.data.model.view.MainPageFeedModel import com.no5ing.bbibbi.data.model.view.MainPageModel import com.no5ing.bbibbi.data.model.view.MainPageTopBarModel -import com.no5ing.bbibbi.presentation.feature.uistate.family.MainFeedStoryElementUiState -import com.no5ing.bbibbi.presentation.feature.uistate.family.MainFeedUiState +import com.no5ing.bbibbi.presentation.component.VerticalGrid import com.no5ing.bbibbi.presentation.feature.view.common.PostTypeSwitchButton import com.no5ing.bbibbi.presentation.theme.bbibbiScheme +import com.no5ing.bbibbi.presentation.theme.bbibbiTypo import com.no5ing.bbibbi.util.gapBetweenNow import kotlinx.coroutines.flow.StateFlow -@OptIn(ExperimentalFoundationApi::class) +@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class) @Composable fun HomePageContent( mainPageState: StateFlow>, @@ -56,98 +64,272 @@ fun HomePageContent( onTapInvite: () -> Unit = {}, onRefresh: () -> Unit = {}, ) { + val warningState = remember { + mutableIntStateOf(0) + } + val mainPageModel by mainPageState.collectAsState() - val postItems = if(mainPageModel.isReady()) - if(postViewTypeState.value == PostType.MISSION) mainPageModel.data.missionFeeds - else mainPageModel.data.survivalFeeds + + val survivalFeedItems = if (mainPageModel.isReady()) + mainPageModel.data.survivalFeeds + else emptyList() + val missionFeedItems = if (mainPageModel.isReady()) + mainPageModel.data.missionFeeds else emptyList() var isRefreshing by remember { mutableStateOf(true) } + val pagerState = rememberPagerState(pageCount = { 2 }) + val pullRefreshStyle = rememberPullRefreshState( + refreshing = isRefreshing, + onRefresh = { + if (isRefreshing) return@rememberPullRefreshState + isRefreshing = true + onRefresh() + } + ) LaunchedEffect(mainPageModel) { if (mainPageModel.isReady()) { isRefreshing = false } } - - HomePageFeedGrid( - isRefreshing = isRefreshing, - onRefresh = { - if (isRefreshing) return@HomePageFeedGrid - isRefreshing = true - onRefresh() + LaunchedEffect(postViewTypeState.value) { + val page = if (postViewTypeState.value == PostType.SURVIVAL) 0 else 1 + pagerState.animateScrollToPage(page) + } + LaunchedEffect(pagerState.currentPage) { + val type = if (pagerState.currentPage == 0) PostType.SURVIVAL else PostType.MISSION + if (type != postViewTypeState.value) { + postViewTypeState.value = type } - ) { - item( - key = "TopBar", - span = { GridItemSpan(2) }) { - Column { - HomePageStoryBar( - mainPageState = mainPageState, - onTapProfile = onTapProfile, - onTapInvite = onTapInvite, - onTapPick = onTapPick, - deferredPickStateSet = deferredPickStateSet, + } + BoxWithConstraints { + val scrollState = rememberScrollState() + + Column( + modifier = Modifier + // .fillMaxSize() + .pullRefresh(pullRefreshStyle) + .verticalScroll(state = scrollState) + ) { + HomePageStoryBar( + mainPageState = mainPageState, + onTapProfile = onTapProfile, + onTapInvite = onTapInvite, + onTapPick = onTapPick, + deferredPickStateSet = deferredPickStateSet, + ) + Spacer(modifier = Modifier.height(24.dp)) + HorizontalDivider( + thickness = 1.dp, + color = MaterialTheme.bbibbiScheme.backgroundSecondary + ) + Spacer(modifier = Modifier.height(24.dp)) + UploadCountDownBar(warningState = warningState) + if(postViewTypeState.value == PostType.SURVIVAL) { + SurvivalTextDescription(warningState = warningState) + } else { + MissionTextDescription( + warningState = warningState, + isMissionUnlocked = mainPageModel.isReady() && mainPageModel.data.isMissionUnlocked, + missionText = if(mainPageModel.isReady()) mainPageModel.data.dailyMissionContent else "", + remainingMemberCnt = if(mainPageModel.isReady()) mainPageModel.data.leftUploadCountUntilMissionUnlock else 0 ) - Spacer(modifier = Modifier.height(24.dp)) - HorizontalDivider( - thickness = 1.dp, - color = MaterialTheme.bbibbiScheme.backgroundSecondary + } + + Spacer(modifier = Modifier.height(24.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + PostTypeSwitchButton( + isLocked = mainPageModel.isReady() && !mainPageModel.data.isMissionUnlocked, + state = postViewTypeState ) - Spacer(modifier = Modifier.height(24.dp)) - UploadCountDownBar() - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center - ) { - PostTypeSwitchButton( - isLocked = mainPageModel.isReady() && !mainPageModel.data.isMissionUnlocked, - state = postViewTypeState + } + Spacer(modifier = Modifier.height(24.dp)) + HorizontalPager( + state = pagerState, + verticalAlignment = Alignment.Top, + ) { page -> + when (page) { + 0 -> SurvivalFeedTab( + postItems = survivalFeedItems, + onTapContent = onTapContent, + ) + + 1 -> MissionFeedTab( + postItems = missionFeedItems, + isMissionUnlocked = mainPageModel.isReady() && mainPageModel.data.isMissionUnlocked, + onTapContent = onTapContent, ) } - Spacer(modifier = Modifier.height(8.dp)) } + } + PullRefreshIndicator( + refreshing = isRefreshing, + state = pullRefreshStyle, + modifier = Modifier.align(Alignment.TopCenter), + backgroundColor = MaterialTheme.bbibbiScheme.backgroundSecondary, + contentColor = MaterialTheme.bbibbiScheme.iconSelected, + ) + } + + +} + +@Composable +fun SurvivalFeedTab( + postItems: List, + onTapContent: (String) -> Unit = {}, +) { + if (postItems.isEmpty()) { + Column( + modifier = Modifier + .height(300.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Image( + painter = painterResource(R.drawable.bbibbi), + contentDescription = null, // 필수 param + modifier = Modifier + .fillMaxWidth(), + contentScale = ContentScale.FillWidth, + ) } - if (postItems.isNotEmpty()) { - items( - count = postItems.size, - key = { - postItems[it].postId - } - ) { - val item = postItems[it] + } else { + VerticalGrid { + postItems.forEach { item -> HomePageFeedElement( - modifier = Modifier.animateItemPlacement( - animationSpec = tween(300), - ), + modifier = Modifier, imageUrl = item.imageUrl, writerName = item.authorName, time = gapBetweenNow(time = item.createdAt), onTap = { onTapContent(item.postId) }, ) } - } else { - item( - key = "Empty", - span = { GridItemSpan(2) } - ) { - Column( - modifier = Modifier - .fillMaxSize() - .height(300.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Image( - painter = painterResource(R.drawable.bbibbi), - contentDescription = null, // 필수 param - modifier = Modifier - .fillMaxWidth(), - contentScale = ContentScale.FillWidth, - ) - } + + } + } +} + +@Composable +fun MissionFeedTab( + postItems: List, + isMissionUnlocked: Boolean, + onTapContent: (String) -> Unit = {}, +) { + if (postItems.isEmpty()) { + Column( + modifier = Modifier + .height(300.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Image( + painter = painterResource(if(isMissionUnlocked) R.drawable.bbibbi + else R.drawable.chest_bibbi), + contentDescription = null, // 필수 param + modifier = Modifier + .fillMaxWidth(), + contentScale = ContentScale.FillWidth, + ) + + } + } else { + VerticalGrid { + postItems.forEach { item -> + HomePageFeedElement( + modifier = Modifier, + imageUrl = item.imageUrl, + writerName = item.authorName, + time = gapBetweenNow(time = item.createdAt), + onTap = { onTapContent(item.postId) }, + ) } + + } } } +@Composable +fun SurvivalTextDescription(warningState: MutableState) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterHorizontally) + ) { + Text( + text = if (warningState.value == 1) + stringResource(id = R.string.home_time_not_much) + else + stringResource(id = R.string.home_image_on_duration), + color = MaterialTheme.bbibbiScheme.textSecondary, + style = MaterialTheme.bbibbiTypo.bodyTwoRegular, + ) + + if (warningState.value == 1) { + Image( + painter = painterResource(id = R.drawable.fire_icon), + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + } else { + Image( + painter = painterResource(id = R.drawable.smile_icon), + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + } + } +} + +@Composable +fun MissionTextDescription( + warningState: MutableState, + isMissionUnlocked: Boolean, + missionText: String, + remainingMemberCnt: Int, +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterHorizontally) + ) { + val missionWaitingText = buildAnnotatedString { + append("가족 중 ") + withStyle(style = SpanStyle(MaterialTheme.bbibbiScheme.mainYellow)) { + append("${remainingMemberCnt}명") + } + append("만 더 올리면 미션 열쇠를 받아요!") + } + if(isMissionUnlocked) { + Text( + text = missionText, + color = MaterialTheme.bbibbiScheme.textSecondary, + style = MaterialTheme.bbibbiTypo.bodyTwoRegular, + ) + } else { + Text( + text = missionWaitingText, + color = MaterialTheme.bbibbiScheme.textSecondary, + style = MaterialTheme.bbibbiTypo.bodyTwoRegular, + ) + } + + + if (isMissionUnlocked) { + Image( + painter = painterResource(id = R.drawable.smile_icon), + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + } else { + Image( + painter = painterResource(id = R.drawable.key_icon), + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + } + } +} diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageUploadButton.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageUploadButton.kt index cdfe9cf..12dd6e9 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageUploadButton.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageUploadButton.kt @@ -5,41 +5,102 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.Canvas import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape 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.draw.clip import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Outline import androidx.compose.ui.graphics.Paint import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.PathEffect import androidx.compose.ui.graphics.drawOutline import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil.compose.AsyncImage import com.no5ing.bbibbi.R +import com.no5ing.bbibbi.data.model.view.MainPagePickerModel +import com.no5ing.bbibbi.presentation.component.CircleProfileImage import com.no5ing.bbibbi.presentation.component.button.CameraCaptureButton import com.no5ing.bbibbi.presentation.theme.bbibbiScheme import com.no5ing.bbibbi.presentation.theme.bbibbiTypo +import com.no5ing.bbibbi.util.asyncImagePainter @Composable -fun BoxScope.HomePageUploadButton( +fun BoxScope.HomePageSurvivalUploadButton( isLoading: Boolean = false, isUploadAbleTime: Boolean = true, isAlreadyUploaded: Boolean = false, + pickers: List = emptyList(), + onTap: () -> Unit = {}, +) { + Box( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(vertical = 15.dp) + .systemBarsPadding() + ) { + AnimatedVisibility( + visible = !isLoading, + enter = fadeIn(), + exit = fadeOut(), + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + if(pickers.isNotEmpty() && isUploadAbleTime && !isAlreadyUploaded && !isLoading) { + WaitingMembersPop( + pickers = pickers, + ) + } else { + UploadHelperPop( + text = + if (isUploadAbleTime && !isAlreadyUploaded) + stringResource(id = R.string.home_one_image_per_day) + else if (isAlreadyUploaded) + stringResource(id = R.string.home_already_uploaded_today) + else + stringResource(id = R.string.home_not_camera_time) + ) + } + CameraCaptureButton( + onClick = onTap, + isCapturing = !isUploadAbleTime || isAlreadyUploaded, + ) + } + } + } + +} + +@Composable +fun BoxScope.HomePageMissionUploadButton( + isLoading: Boolean, + isMeUploadedToday: Boolean, + isMissionUnlocked: Boolean, + isMeMissionUploaded: Boolean, onTap: () -> Unit = {}, ) { Box( @@ -56,16 +117,18 @@ fun BoxScope.HomePageUploadButton( Column(horizontalAlignment = Alignment.CenterHorizontally) { UploadHelperPop( text = - if (isUploadAbleTime && !isAlreadyUploaded) - stringResource(id = R.string.home_one_image_per_day) - else if (isAlreadyUploaded) - stringResource(id = R.string.home_already_uploaded_today) + if(!isMeUploadedToday) + "생존신고 후 미션 사진을 올릴 수 있어요" + else if (!isMissionUnlocked) + "아직 미션 사진을 찍을 수 없어요" + else if (isMeMissionUploaded) + "오늘의 미션은 완료되었어요" else - stringResource(id = R.string.home_not_camera_time) + "미션 사진을 찍으러 가볼까요?" ) CameraCaptureButton( onClick = onTap, - isCapturing = !isUploadAbleTime || isAlreadyUploaded, + isCapturing = !(!isMeMissionUploaded && isMissionUnlocked && isMeUploadedToday), ) } } @@ -73,6 +136,7 @@ fun BoxScope.HomePageUploadButton( } + @Composable fun UploadHelperPop( text: String, @@ -124,4 +188,136 @@ fun UploadHelperPop( } } } -} \ No newline at end of file +} + +@Composable +fun WaitingMembersPop( + pickers: List = emptyList(), +) { + Box(modifier = Modifier.padding(bottom = 30.dp)) { + Box( + modifier = Modifier + .background(MaterialTheme.bbibbiScheme.mainYellow, shape = RoundedCornerShape(6.dp)) + .padding( + vertical = 10.dp, + horizontal = 14.dp + ) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + pickers.take(3).forEach { + MiniCircledIcon( + noImageLetter = it.displayName.first().toString(), + imageUrl = it.imageUrl, + ) + } + if(pickers.size == 1) { + Text( + text = "${pickers.first().displayName}님이 기다리고 있어요", + style = MaterialTheme.bbibbiTypo.bodyTwoRegular, + color = MaterialTheme.bbibbiScheme.backgroundHover, + ) + } else { + Text( + text = "${pickers.first().displayName}님이 외 ${pickers.size - 1}명이 기다리고 있어요", + style = MaterialTheme.bbibbiTypo.bodyTwoRegular, + color = MaterialTheme.bbibbiScheme.backgroundHover, + ) + } + } + } + Box( + modifier = Modifier + .align(Alignment.Center) + .offset(y = 24.dp) + .size(24.dp, 24.dp) + ) { + val surfaceColor = MaterialTheme.bbibbiScheme.mainYellow + Canvas( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + ) { + val rect = Rect(Offset.Zero, size) + val trianglePath = Path().apply { + moveTo(rect.topLeft.x, rect.topLeft.y) + lineTo(rect.topRight.x, rect.topRight.y) + lineTo(rect.bottomCenter.x, rect.bottomCenter.y) + close() + } + + drawIntoCanvas { canvas -> + canvas.drawOutline( + outline = Outline.Generic(trianglePath), + paint = Paint().apply { + color = surfaceColor + pathEffect = PathEffect.cornerPathEffect(rect.maxDimension / 5) + } + ) + } + } + } + } +} + +@Composable +fun MiniCircledIcon( + modifier: Modifier = Modifier, + noImageLetter: String, + imageUrl: String?, + opacity: Float = 1.0f, + backgroundColor: Color = MaterialTheme.bbibbiScheme.backgroundSecondary, + onTap: () -> Unit = {}, +) { + Box { + if (imageUrl != null) { + AsyncImage( + model = asyncImagePainter(source = imageUrl), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = modifier + .size(24.dp) + .clip(CircleShape) + .background(backgroundColor) + .border( + width = 2.dp, + color = MaterialTheme.bbibbiScheme.mainYellow, + shape = CircleShape + ) + .clickable { onTap() }, + alpha = opacity, + ) + } else { + Box( + modifier = Modifier.clickable { onTap() }, + contentAlignment = Alignment.Center, + ) { + Box( + modifier = modifier + .size(24.dp) + .clip(CircleShape) + .background( + MaterialTheme.bbibbiScheme + .backgroundHover + .copy(alpha = opacity) + ) + .border( + width = 2.dp, + color = MaterialTheme.bbibbiScheme.mainYellow, + shape = CircleShape + ) + ) + Box(modifier = Modifier.align(Alignment.Center)) { + Text( + text = noImageLetter, + fontSize = 8.sp, + color = MaterialTheme.bbibbiScheme.white.copy(alpha = opacity), + fontWeight = FontWeight.SemiBold, + ) + } + } + } + } +} diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/UploadCountDownBar.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/UploadCountDownBar.kt index dfe7941..04c9263 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/UploadCountDownBar.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/UploadCountDownBar.kt @@ -67,31 +67,6 @@ fun UploadCountDownBar( color = if (warningState.value == 1) MaterialTheme.bbibbiScheme.warningRed else MaterialTheme.bbibbiScheme.white, ) Spacer(modifier = Modifier.height(8.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterHorizontally) - ) { - Text( - text = if (warningState.value == 1) stringResource(id = R.string.home_time_not_much) - else stringResource(id = R.string.home_image_on_duration), - color = MaterialTheme.bbibbiScheme.textSecondary, - style = MaterialTheme.bbibbiTypo.bodyTwoRegular, - ) - if (warningState.value == 1) { - Image( - painter = painterResource(id = R.drawable.fire_icon), - contentDescription = null, - modifier = Modifier.size(20.dp) - ) - } else { - Image( - painter = painterResource(id = R.drawable.smile_icon), - contentDescription = null, - modifier = Modifier.size(20.dp) - ) - } - } - Spacer(modifier = Modifier.height(24.dp)) } } \ No newline at end of file diff --git a/app/src/main/java/com/no5ing/bbibbi/util/KotlinExtension.kt b/app/src/main/java/com/no5ing/bbibbi/util/KotlinExtension.kt index e90ac9e..1892fdd 100644 --- a/app/src/main/java/com/no5ing/bbibbi/util/KotlinExtension.kt +++ b/app/src/main/java/com/no5ing/bbibbi/util/KotlinExtension.kt @@ -327,7 +327,7 @@ fun randomBoolean() = (0..1).random() == 1 fun gapUntilNext(): Long { val current = LocalDateTime.now() - if (current.hour < 12) + if (current.hour < 10) return -1 val tomorrow = LocalDateTime .of(current.year, current.month, current.dayOfMonth, 0, 0, 0) diff --git a/app/src/main/res/drawable/chest_bibbi.xml b/app/src/main/res/drawable/chest_bibbi.xml new file mode 100644 index 0000000..3cde289 --- /dev/null +++ b/app/src/main/res/drawable/chest_bibbi.xml @@ -0,0 +1,239 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/key_icon.xml b/app/src/main/res/drawable/key_icon.xml new file mode 100644 index 0000000..8eddc31 --- /dev/null +++ b/app/src/main/res/drawable/key_icon.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 46e556f..08c82a1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -9,9 +9,9 @@ 오늘의 생존신고는 완료되었어요 우리 가족 모두가 사진을 올린 날 - 매일 12-24시에 사진 한 장을 올려요 + 매일 10-24시에 사진 한 장을 올려요 시간이 얼마 남지 않았어요! - 내일 낮 12시부터 사진을 올릴 수 있어요! + 내일 낮 10시부터 사진을 올릴 수 있어요! 계속 완료 From 2f92e3d07ca96316df888f4ea719c9157966f750 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=98=81=EB=AF=BC?= Date: Wed, 24 Apr 2024 21:30:00 +0900 Subject: [PATCH 09/26] =?UTF-8?q?feat:=20=EB=AF=B8=EC=85=98=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EA=B4=80=EB=A0=A8=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bbibbi/data/datasource/network/RestAPI.kt | 1 + .../repository/post/GetFeedsRepository.kt | 3 +- .../repository/post/GetPostsRepository.kt | 3 +- .../feature/view/main/home/HomePageContent.kt | 4 +- .../view/main/home/HomePageFeedElement.kt | 25 ++- .../view/main/home/HomePageStoryBar.kt | 5 +- .../view/main/home/HomePageUploadButton.kt | 66 ++++---- .../feature/view/main/profile/ProfilePage.kt | 7 +- .../view/main/profile/ProfilePageContent.kt | 146 +++++++++++++++++- .../post/FamilyMissionPostsViewModel.kt | 44 ++++++ app/src/main/res/drawable/mission_diamond.png | Bin 0 -> 2941 bytes 11 files changed, 263 insertions(+), 41 deletions(-) create mode 100644 app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/FamilyMissionPostsViewModel.kt create mode 100644 app/src/main/res/drawable/mission_diamond.png diff --git a/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt b/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt index 9cebb70..b38adf0 100644 --- a/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt +++ b/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt @@ -163,6 +163,7 @@ interface RestAPI { @Query("size") size: Int?, @Query("date") date: String?, @Query("memberId") memberId: String?, + @Query("type") type: String? = null, @Query("sort") sort: String? = "DESC", ): ApiResponse> diff --git a/app/src/main/java/com/no5ing/bbibbi/data/repository/post/GetFeedsRepository.kt b/app/src/main/java/com/no5ing/bbibbi/data/repository/post/GetFeedsRepository.kt index ae7058d..989904e 100644 --- a/app/src/main/java/com/no5ing/bbibbi/data/repository/post/GetFeedsRepository.kt +++ b/app/src/main/java/com/no5ing/bbibbi/data/repository/post/GetFeedsRepository.kt @@ -61,7 +61,8 @@ class GetFeedPageSource @Inject constructor( date = arguments.get("date"), memberId = arguments.get("memberId"), page = loadParams.key ?: 1, - size = loadParams.loadSize + size = loadParams.loadSize, + type = null, ) return posts.mapSuccess { Pagination( diff --git a/app/src/main/java/com/no5ing/bbibbi/data/repository/post/GetPostsRepository.kt b/app/src/main/java/com/no5ing/bbibbi/data/repository/post/GetPostsRepository.kt index f21aeb5..391a58b 100644 --- a/app/src/main/java/com/no5ing/bbibbi/data/repository/post/GetPostsRepository.kt +++ b/app/src/main/java/com/no5ing/bbibbi/data/repository/post/GetPostsRepository.kt @@ -49,7 +49,8 @@ class GetPostPagingSource @Inject constructor( date = null, memberId = arguments.get("memberId"), page = loadParams.key ?: 1, - size = loadParams.loadSize + size = loadParams.loadSize, + type = arguments.get("type"), ) } diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt index 7a1fc5a..82e2e5b 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt @@ -206,10 +206,9 @@ fun SurvivalFeedTab( writerName = item.authorName, time = gapBetweenNow(time = item.createdAt), onTap = { onTapContent(item.postId) }, + isMission = false, ) } - - } } } @@ -246,6 +245,7 @@ fun MissionFeedTab( writerName = item.authorName, time = gapBetweenNow(time = item.createdAt), onTap = { onTapContent(item.postId) }, + isMission = true, ) } diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageFeedElement.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageFeedElement.kt index 2b8c25f..35850c8 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageFeedElement.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageFeedElement.kt @@ -1,5 +1,6 @@ package com.no5ing.bbibbi.presentation.feature.view.main.home +import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -9,6 +10,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.shape.RoundedCornerShape @@ -19,9 +21,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil.compose.AsyncImage +import com.no5ing.bbibbi.R import com.no5ing.bbibbi.presentation.component.MicroTextBubbleBox import com.no5ing.bbibbi.presentation.theme.bbibbiScheme import com.no5ing.bbibbi.presentation.theme.bbibbiTypo @@ -33,6 +37,7 @@ fun HomePageFeedElement( imageUrl: String, writerName: String, time: String, + isMission: Boolean, onTap: () -> Unit = {}, ) { Column( @@ -41,7 +46,9 @@ fun HomePageFeedElement( .clickable { onTap() }, horizontalAlignment = Alignment.CenterHorizontally ) { - Box { + Box( + contentAlignment = Alignment.TopStart, + ) { AsyncImage( model = asyncImagePainter(source = imageUrl), contentDescription = null, @@ -51,11 +58,17 @@ fun HomePageFeedElement( .aspectRatio(1.0f) .clip(RoundedCornerShape(24.dp)) ) -// MicroTextBubbleBox( -// text = postContent, -// alignment = Alignment.BottomCenter, -// modifier = Modifier.padding(bottom = 10.dp) -// ) + if (isMission) { + Box( + modifier = Modifier.padding(12.dp) + ) { + Image( + painter = painterResource(id = R.drawable.mission_diamond), + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + } + } } Row( diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageStoryBar.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageStoryBar.kt index 009ae26..6861cc8 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageStoryBar.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageStoryBar.kt @@ -39,6 +39,8 @@ import com.no5ing.bbibbi.presentation.feature.uistate.family.MainFeedStoryElemen import com.no5ing.bbibbi.presentation.theme.bbibbiScheme import com.no5ing.bbibbi.presentation.theme.bbibbiTypo import com.no5ing.bbibbi.util.LocalSessionState +import com.no5ing.bbibbi.util.gapBetweenNow +import com.no5ing.bbibbi.util.gapUntilNext import kotlinx.coroutines.flow.StateFlow @Composable @@ -113,6 +115,7 @@ fun StoryBarIcon( isUploaded: Boolean, rank: Int, ) { + val canUpload = gapUntilNext() > 0 Column( modifier = Modifier .width(64.dp) @@ -174,7 +177,7 @@ fun StoryBarIcon( ) } } - if (member.shouldShowPickIcon && !isInDeferredPickState) { + if (member.shouldShowPickIcon && !isInDeferredPickState && canUpload) { Box( contentAlignment = Alignment.BottomEnd, modifier = Modifier diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageUploadButton.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageUploadButton.kt index 12dd6e9..2a50adc 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageUploadButton.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageUploadButton.kt @@ -12,12 +12,14 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope 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.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme @@ -197,7 +199,10 @@ fun WaitingMembersPop( Box(modifier = Modifier.padding(bottom = 30.dp)) { Box( modifier = Modifier - .background(MaterialTheme.bbibbiScheme.mainYellow, shape = RoundedCornerShape(6.dp)) + .background( + MaterialTheme.bbibbiScheme.mainYellow, + shape = RoundedCornerShape(12.dp) + ) .padding( vertical = 10.dp, horizontal = 14.dp @@ -207,11 +212,20 @@ fun WaitingMembersPop( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp) ) { - pickers.take(3).forEach { - MiniCircledIcon( - noImageLetter = it.displayName.first().toString(), - imageUrl = it.imageUrl, - ) + val pickersShattered = pickers.take(3) + Row { + Box { + pickersShattered.reversed().forEachIndexed { rawIdx, it -> + MiniCircledIcon( + noImageLetter = it.displayName.first().toString(), + imageUrl = it.imageUrl, + modifier = Modifier.offset( + x = 16.dp * (pickersShattered.size - 1 - rawIdx) + ) + ) + } + } + Spacer(modifier = Modifier.width(16.dp.times(pickersShattered.size - 1))) } if(pickers.size == 1) { Text( @@ -267,27 +281,28 @@ fun MiniCircledIcon( modifier: Modifier = Modifier, noImageLetter: String, imageUrl: String?, - opacity: Float = 1.0f, - backgroundColor: Color = MaterialTheme.bbibbiScheme.backgroundSecondary, onTap: () -> Unit = {}, ) { - Box { + Box( + modifier = modifier, + contentAlignment = Alignment.Center, + ) { + Box( + modifier = Modifier + .size(24.dp) + .background(MaterialTheme.bbibbiScheme.mainYellow, CircleShape) + ) { + + } if (imageUrl != null) { AsyncImage( model = asyncImagePainter(source = imageUrl), contentDescription = null, contentScale = ContentScale.Crop, - modifier = modifier - .size(24.dp) + modifier = Modifier + .size(20.dp) .clip(CircleShape) - .background(backgroundColor) - .border( - width = 2.dp, - color = MaterialTheme.bbibbiScheme.mainYellow, - shape = CircleShape - ) .clickable { onTap() }, - alpha = opacity, ) } else { Box( @@ -295,25 +310,20 @@ fun MiniCircledIcon( contentAlignment = Alignment.Center, ) { Box( - modifier = modifier - .size(24.dp) + modifier = Modifier + .size(20.dp) .clip(CircleShape) .background( MaterialTheme.bbibbiScheme - .backgroundHover - .copy(alpha = opacity) - ) - .border( - width = 2.dp, - color = MaterialTheme.bbibbiScheme.mainYellow, - shape = CircleShape + .backgroundHover, + CircleShape ) ) Box(modifier = Modifier.align(Alignment.Center)) { Text( text = noImageLetter, fontSize = 8.sp, - color = MaterialTheme.bbibbiScheme.white.copy(alpha = opacity), + color = MaterialTheme.bbibbiScheme.white, fontWeight = FontWeight.SemiBold, ) } diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/profile/ProfilePage.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/profile/ProfilePage.kt index 1c91518..c8277a4 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/profile/ProfilePage.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/profile/ProfilePage.kt @@ -31,6 +31,7 @@ import com.no5ing.bbibbi.presentation.component.snackBarWarning import com.no5ing.bbibbi.presentation.feature.view.common.AlbumCameraSelectDialog import com.no5ing.bbibbi.presentation.feature.view_model.members.ChangeProfileImageViewModel import com.no5ing.bbibbi.presentation.feature.view_model.members.FamilyMemberViewModel +import com.no5ing.bbibbi.presentation.feature.view_model.post.FamilyMissionPostsViewModel import com.no5ing.bbibbi.presentation.feature.view_model.post.FamilyPostsViewModel import com.no5ing.bbibbi.util.LocalSessionState import com.no5ing.bbibbi.util.LocalSnackbarHostState @@ -51,6 +52,7 @@ fun ProfilePage( familyMemberViewModel: FamilyMemberViewModel = hiltViewModel(), profileImageChangeViewModel: ChangeProfileImageViewModel = hiltViewModel(), familyPostsViewModel: FamilyPostsViewModel = hiltViewModel(), + familyMissionPostsViewModel: FamilyMissionPostsViewModel = hiltViewModel(), memberState: State> = familyMemberViewModel.uiState.collectAsState(), ) { val sessionState = LocalSessionState.current @@ -60,6 +62,7 @@ fun ProfilePage( val snackBarHost = LocalSnackbarHostState.current LaunchedEffect(Unit) { familyPostsViewModel.invoke(Arguments(arguments = mapOf("memberId" to memberId))) + familyMissionPostsViewModel.invoke(Arguments(arguments = mapOf("memberId" to memberId))) familyMemberViewModel.invoke(Arguments(resourceId = memberId)) } LaunchedEffect(changeableUriState.value) { @@ -146,6 +149,7 @@ fun ProfilePage( ProfilePageContent( onTapContent = onTapPost, postItemsState = familyPostsViewModel.uiState, + missionItemState = familyMissionPostsViewModel.uiState, ) } } @@ -171,7 +175,8 @@ fun ProfilePagePreview() { ) ProfilePageContent( onTapContent = {}, - postItemsState = MutableStateFlow(PagingData.empty()) + postItemsState = MutableStateFlow(PagingData.empty()), + missionItemState = MutableStateFlow(PagingData.empty()) ) } } diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/profile/ProfilePageContent.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/profile/ProfilePageContent.kt index d332ca5..a5bcd9b 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/profile/ProfilePageContent.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/profile/ProfilePageContent.kt @@ -1,5 +1,6 @@ package com.no5ing.bbibbi.presentation.feature.view.main.profile +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -17,6 +18,8 @@ import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.pullrefresh.PullRefreshIndicator @@ -26,6 +29,10 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -39,18 +46,75 @@ import androidx.paging.compose.collectAsLazyPagingItems import coil.compose.AsyncImage import com.no5ing.bbibbi.R import com.no5ing.bbibbi.data.model.post.Post +import com.no5ing.bbibbi.data.model.post.PostType import com.no5ing.bbibbi.presentation.component.MicroTextBubbleBox +import com.no5ing.bbibbi.presentation.feature.view.common.PostTypeSwitchButton import com.no5ing.bbibbi.presentation.theme.bbibbiScheme import com.no5ing.bbibbi.presentation.theme.bbibbiTypo import com.no5ing.bbibbi.util.asyncImagePainter import com.no5ing.bbibbi.util.toLocalizedDate import kotlinx.coroutines.flow.StateFlow -@OptIn(ExperimentalMaterialApi::class) +@OptIn(ExperimentalFoundationApi::class) @Composable fun ProfilePageContent( onTapContent: (Post) -> Unit = {}, postItemsState: StateFlow>, + missionItemState: StateFlow>, + postViewTypeState: MutableState = remember { mutableStateOf(PostType.SURVIVAL) } +) { + val pagerState = rememberPagerState { 2 } + LaunchedEffect(pagerState.currentPage) { + val type = if (pagerState.currentPage == 0) PostType.SURVIVAL else PostType.MISSION + if (type != postViewTypeState.value) { + postViewTypeState.value = type + } + } + LaunchedEffect(postViewTypeState.value) { + val page = if (postViewTypeState.value == PostType.SURVIVAL) 0 else 1 + pagerState.animateScrollToPage(page) + } + Column { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + PostTypeSwitchButton( + isLocked = false, + state = postViewTypeState + ) + } + Spacer(modifier = Modifier.height(16.dp)) + HorizontalPager( + state = pagerState, + verticalAlignment = Alignment.Top, + ) { + when(it) { + 0 -> { + SurvivalProfilePageFeed( + postItemsState = postItemsState, + onTapContent = onTapContent + ) + } + 1 -> { + MissionProfilePageFeed( + postItemsState = missionItemState, + onTapContent = onTapContent + ) + } + } + + } + + } + +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun SurvivalProfilePageFeed( + postItemsState: StateFlow>, + onTapContent: (Post) -> Unit = {}, ) { val postItems = postItemsState.collectAsLazyPagingItems() val pullRefreshStyle = rememberPullRefreshState( @@ -98,6 +162,7 @@ fun ProfilePageContent( time = toLocalizedDate(time = item.createdAt), onTap = { onTapContent(item) }, postContent = item.content, + isMission = false, ) } } @@ -113,6 +178,73 @@ fun ProfilePageContent( } } +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun MissionProfilePageFeed( + postItemsState: StateFlow>, + onTapContent: (Post) -> Unit = {}, +) { + val postItems = postItemsState.collectAsLazyPagingItems() + val pullRefreshStyle = rememberPullRefreshState( + refreshing = postItems.loadState.refresh is LoadState.Loading, + onRefresh = { + postItems.refresh() + } + ) + Box(modifier = Modifier.pullRefresh(pullRefreshStyle)) { + if (postItems.itemCount == 0) { + Column( + modifier = Modifier + .fillMaxSize() + .systemBarsPadding(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Image( + painter = painterResource(R.drawable.no_uploaded_image), + contentDescription = null, // 필수 param + modifier = Modifier + .size(width = 126.dp, height = 118.dp), + contentScale = ContentScale.FillWidth, + ) + Spacer(modifier = Modifier.height(20.dp)) + Text( + text = stringResource(id = R.string.profile_image_not_exists), + color = MaterialTheme.bbibbiScheme.textSecondary, + style = MaterialTheme.bbibbiTypo.bodyOneRegular, + ) + + } + } else { + LazyVerticalGrid( + modifier = Modifier.fillMaxSize(), + columns = GridCells.Fixed(count = 2), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + items(postItems.itemCount) { + val item = postItems[it] ?: throw RuntimeException() + ProfilePageContentItem( + imageUrl = item.imageUrl, + emojiCnt = item.emojiCount, + commentCnt = item.commentCount, + time = toLocalizedDate(time = item.createdAt), + onTap = { onTapContent(item) }, + postContent = item.content, + isMission = true, + ) + } + } + } + + PullRefreshIndicator( + refreshing = postItems.loadState.refresh is LoadState.Loading, + state = pullRefreshStyle, + modifier = Modifier.align(Alignment.TopCenter), + backgroundColor = MaterialTheme.bbibbiScheme.backgroundSecondary, + contentColor = MaterialTheme.bbibbiScheme.iconSelected, + ) + } +} @Composable fun ProfilePageContentItem( imageUrl: String, @@ -120,6 +252,7 @@ fun ProfilePageContentItem( emojiCnt: Int, commentCnt: Int, time: String, + isMission: Boolean, onTap: () -> Unit = {}, ) { Column( @@ -143,6 +276,17 @@ fun ProfilePageContentItem( alignment = Alignment.BottomCenter, modifier = Modifier.padding(bottom = 10.dp) ) + if (isMission) { + Box( + modifier = Modifier.padding(12.dp) + ) { + Image( + painter = painterResource(id = R.drawable.mission_diamond), + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + } + } } Column( modifier = Modifier.padding(vertical = 8.dp, horizontal = 20.dp), diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/FamilyMissionPostsViewModel.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/FamilyMissionPostsViewModel.kt new file mode 100644 index 0000000..d94bdba --- /dev/null +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/FamilyMissionPostsViewModel.kt @@ -0,0 +1,44 @@ +package com.no5ing.bbibbi.presentation.feature.view_model.post + +import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import androidx.paging.cachedIn +import com.no5ing.bbibbi.data.model.post.Post +import com.no5ing.bbibbi.data.repository.Arguments +import com.no5ing.bbibbi.data.repository.post.GetPostsRepository +import com.no5ing.bbibbi.presentation.feature.view_model.BaseViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject + +@HiltViewModel +class FamilyMissionPostsViewModel @Inject constructor( + private val getPostsRepository: GetPostsRepository, +) : BaseViewModel>() { + override fun initState(): PagingData { + return PagingData.empty() + } + + override fun invoke(arguments: Arguments) { + withMutexScope(Dispatchers.IO) { + getPostsRepository + .fetch(arguments.copy(arguments = arguments.arguments + mapOf("type" to "MISSION"))) + .cachedIn(viewModelScope) + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = PagingData.empty() + ).collectLatest { + setState(it) + } + } + } + + override fun release() { + super.release() + getPostsRepository.closeResources() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/mission_diamond.png b/app/src/main/res/drawable/mission_diamond.png new file mode 100644 index 0000000000000000000000000000000000000000..dc7c6e4edded147f33979a7eef707974cb302752 GIT binary patch literal 2941 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 Date: Wed, 24 Apr 2024 22:55:28 +0900 Subject: [PATCH 10/26] =?UTF-8?q?feat:=20=EB=AA=85=EC=98=88=EC=9D=98=20?= =?UTF-8?q?=EC=A0=84=EB=8B=B9=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?[skip-ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bbibbi/data/model/view/MainPageModel.kt | 20 + .../component/button/FlatButton.kt | 60 +++ .../view/main/calendar/MainCalendarPage.kt | 1 - .../feature/view/main/home/HomePage.kt | 51 ++- .../feature/view/main/home/HomePageContent.kt | 2 +- .../view/main/home/HomePageStoryBar.kt | 9 +- .../view/main/home/NightHomePageContent.kt | 403 ++++++++++++++++++ .../main/HomePageController.kt | 4 + .../view_model/MainPageNightViewModel.kt | 30 ++ app/src/main/res/drawable/first_ranking.png | Bin 0 -> 2977 bytes .../main/res/drawable/first_ranking_empty.png | Bin 0 -> 3107 bytes app/src/main/res/drawable/second_ranking.png | Bin 0 -> 2924 bytes .../res/drawable/second_ranking_empty.png | Bin 0 -> 2871 bytes app/src/main/res/drawable/third_ranking.png | Bin 0 -> 3024 bytes .../main/res/drawable/third_ranking_empty.png | Bin 0 -> 2985 bytes 15 files changed, 561 insertions(+), 19 deletions(-) create mode 100644 app/src/main/java/com/no5ing/bbibbi/presentation/component/button/FlatButton.kt create mode 100644 app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/NightHomePageContent.kt create mode 100644 app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/MainPageNightViewModel.kt create mode 100644 app/src/main/res/drawable/first_ranking.png create mode 100644 app/src/main/res/drawable/first_ranking_empty.png create mode 100644 app/src/main/res/drawable/second_ranking.png create mode 100644 app/src/main/res/drawable/second_ranking_empty.png create mode 100644 app/src/main/res/drawable/third_ranking.png create mode 100644 app/src/main/res/drawable/third_ranking_empty.png diff --git a/app/src/main/java/com/no5ing/bbibbi/data/model/view/MainPageModel.kt b/app/src/main/java/com/no5ing/bbibbi/data/model/view/MainPageModel.kt index 61f2a76..85a8da0 100644 --- a/app/src/main/java/com/no5ing/bbibbi/data/model/view/MainPageModel.kt +++ b/app/src/main/java/com/no5ing/bbibbi/data/model/view/MainPageModel.kt @@ -20,6 +20,7 @@ data class MainPageModel( @Parcelize data class NightMainPageModel( val topBarElements: List, + val familyMemberMonthlyRanking: MainPageMonthlyRankingModel, ) : Parcelable, BaseModel() @Parcelize @@ -46,4 +47,23 @@ data class MainPageTopBarModel( val displayRank: Int?, val shouldShowBirthdayMark: Boolean, val shouldShowPickIcon: Boolean, +) : Parcelable + +@Parcelize +data class MainPageMonthlyRankingModel( + val month: Int, + val firstRanker: MainPageMonthlyRankerModel?, + val secondRanker: MainPageMonthlyRankerModel?, + val thirdRanker: MainPageMonthlyRankerModel?, +) : Parcelable { + fun isAllRankersNull(): Boolean { + return firstRanker == null && secondRanker == null && thirdRanker == null + } +} + +@Parcelize +data class MainPageMonthlyRankerModel( + val profileImageUrl: String?, + val name: String, + val survivalCount: Int, ) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/component/button/FlatButton.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/component/button/FlatButton.kt new file mode 100644 index 0000000..7af3b16 --- /dev/null +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/component/button/FlatButton.kt @@ -0,0 +1,60 @@ +package com.no5ing.bbibbi.presentation.component.button + +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.no5ing.bbibbi.presentation.theme.bbibbiScheme +import com.no5ing.bbibbi.presentation.theme.bbibbiTypo + +@Composable +fun FlatButton( + text: String, + modifier: Modifier = Modifier, + buttonColor: Color = MaterialTheme.bbibbiScheme.mainYellow, + textColor: Color = MaterialTheme.bbibbiScheme.backgroundPrimary, + contentPadding: PaddingValues = PaddingValues( + start = 24.dp, + top = 12.dp, + end = 24.dp, + bottom = 12.dp, + ), + onClick: () -> Unit = {}, + isActive: Boolean = true, + byPassCtaIgnore: Boolean = false, +) { + val opacityAlpha: Float by animateFloatAsState( + targetValue = if (isActive) 1f else 0.2f, + animationSpec = tween( + durationMillis = 130, + easing = LinearEasing, + ), label = "" + ) + Button( + shape = RoundedCornerShape(8.dp), + onClick = { if (isActive || byPassCtaIgnore) onClick() }, + colors = ButtonDefaults.buttonColors( + containerColor = buttonColor.copy( + alpha = opacityAlpha + ) + ), + modifier = modifier, + contentPadding = contentPadding, + ) { + Text( + text = text, + color = textColor, + style = MaterialTheme.bbibbiTypo.bodyOneBold, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/calendar/MainCalendarPage.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/calendar/MainCalendarPage.kt index df6bba3..5c0aacd 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/calendar/MainCalendarPage.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/calendar/MainCalendarPage.kt @@ -312,7 +312,6 @@ fun MainCalendarYearMonthBar( .size(20.dp) .clickable { it.showAlignBottom() } ) - } } if (statistics.isReady()) { diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePage.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePage.kt index 8c25e89..edfe02f 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePage.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePage.kt @@ -28,14 +28,17 @@ import com.no5ing.bbibbi.presentation.component.BBiBBiPreviewSurface import com.no5ing.bbibbi.presentation.component.BBiBBiSurface import com.no5ing.bbibbi.presentation.component.BackToExitHandler import com.no5ing.bbibbi.presentation.feature.view.common.CustomAlertDialog +import com.no5ing.bbibbi.presentation.feature.view_model.MainPageNightViewModel import com.no5ing.bbibbi.presentation.feature.view_model.MainPageViewModel import com.no5ing.bbibbi.presentation.theme.bbibbiScheme import com.no5ing.bbibbi.util.gapUntilNext import kotlinx.coroutines.flow.MutableStateFlow +import java.time.LocalDate @Composable fun HomePage( mainPageViewModel: MainPageViewModel = hiltViewModel(), + mainPageNightViewModel: MainPageNightViewModel = hiltViewModel(), postViewTypeState: MutableState = remember { mutableStateOf(PostType.SURVIVAL) }, onTapLeft: () -> Unit = {}, onTapRight: () -> Unit = {}, @@ -44,12 +47,14 @@ fun HomePage( onTapUpload: () -> Unit = {}, onTapInvite: () -> Unit = {}, onUnsavedPost: (Uri) -> Unit = {}, + onTapViewPost: (LocalDate) -> Unit = {}, onTapPick: (MainPageTopBarModel) -> Unit = {}, ) { val postViewType by postViewTypeState val mainPageState = mainPageViewModel.uiState.collectAsState() val unsavedDialogUri = remember { mutableStateOf(null) } val unsavedDialogEnabled = remember { mutableStateOf(false) } + val isDayTime = gapUntilNext() > 0 CustomAlertDialog( enabledState = unsavedDialogEnabled, title = stringResource(id = R.string.unsaved_post_dialog_title), @@ -71,7 +76,11 @@ fun HomePage( unsavedDialogUri.value = tempUri unsavedDialogEnabled.value = true } - mainPageViewModel.invoke(Arguments()) + if(isDayTime) { + mainPageViewModel.invoke(Arguments()) + } else { + mainPageNightViewModel.invoke(Arguments()) + } } BBiBBiSurface( @@ -89,18 +98,34 @@ fun HomePage( onTapLeft = onTapLeft, onTapRight = onTapRight ) - HomePageContent( - mainPageState = mainPageViewModel.uiState, - postViewTypeState = postViewTypeState, - onTapContent = onTapContent, - onTapProfile = onTapProfile, - onTapInvite = onTapInvite, - onTapPick = onTapPick, - onRefresh = { - mainPageViewModel.invoke(Arguments()) - }, - deferredPickStateSet = mainPageViewModel.deferredPickMembersSet - ) + if (isDayTime) { + HomePageContent( + mainPageState = mainPageViewModel.uiState, + postViewTypeState = postViewTypeState, + onTapContent = onTapContent, + onTapProfile = onTapProfile, + onTapInvite = onTapInvite, + onTapPick = onTapPick, + onRefresh = { + mainPageViewModel.invoke(Arguments()) + }, + deferredPickStateSet = mainPageViewModel.deferredPickMembersSet + ) + } else { + NightHomePageContent( + mainPageState = mainPageNightViewModel.uiState, + postViewTypeState = postViewTypeState, + onTapViewPost = onTapViewPost, + onTapProfile = onTapProfile, + onTapInvite = onTapInvite, + onTapPick = onTapPick, + onRefresh = { + mainPageNightViewModel.invoke(Arguments()) + }, + deferredPickStateSet = mainPageViewModel.deferredPickMembersSet + ) + } + } if (postViewType == PostType.SURVIVAL) { HomePageSurvivalUploadButton( diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt index 82e2e5b..c935a29 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt @@ -111,7 +111,7 @@ fun HomePageContent( .verticalScroll(state = scrollState) ) { HomePageStoryBar( - mainPageState = mainPageState, + items = if (mainPageModel.isReady()) mainPageModel.data.topBarElements else emptyList(), onTapProfile = onTapProfile, onTapInvite = onTapInvite, onTapPick = onTapPick, diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageStoryBar.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageStoryBar.kt index 6861cc8..e3db64a 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageStoryBar.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageStoryBar.kt @@ -45,16 +45,17 @@ import kotlinx.coroutines.flow.StateFlow @Composable fun HomePageStoryBar( - mainPageState: StateFlow>, + //mainPageState: StateFlow>, + items: List, deferredPickStateSet: StateFlow>, onTapProfile: (String) -> Unit = {}, onTapPick: (MainPageTopBarModel) -> Unit = {}, onTapInvite: () -> Unit = {}, ) { val meId = LocalSessionState.current.memberId - val mainPageModel by mainPageState.collectAsState() + // val mainPageModel by mainPageState.collectAsState() val deferredPickSet = deferredPickStateSet.collectAsState() - val items = if (mainPageModel.isReady()) mainPageModel.data.topBarElements else emptyList() + // val items = if (mainPageModel.isReady()) mainPageModel.data.topBarElements else emptyList() if (items.size == 1) { HomePageNoFamilyBar( @@ -206,7 +207,7 @@ fun StoryBarIcon( } @Composable -private fun getRankColor(rank: Int) = when (rank) { +fun getRankColor(rank: Int) = when (rank) { 0 -> MaterialTheme.bbibbiScheme.mainYellow 1 -> Color(0xff7FEC93) 2 -> Color(0xffFFC98D) diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/NightHomePageContent.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/NightHomePageContent.kt new file mode 100644 index 0000000..2ddd34f --- /dev/null +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/NightHomePageContent.kt @@ -0,0 +1,403 @@ +package com.no5ing.bbibbi.presentation.feature.view.main.home + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +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.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Icon +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +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.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.no5ing.bbibbi.R +import com.no5ing.bbibbi.data.model.APIResponse +import com.no5ing.bbibbi.data.model.post.PostType +import com.no5ing.bbibbi.data.model.view.MainPageMonthlyRankerModel +import com.no5ing.bbibbi.data.model.view.MainPageTopBarModel +import com.no5ing.bbibbi.data.model.view.NightMainPageModel +import com.no5ing.bbibbi.presentation.component.CircleProfileImage +import com.no5ing.bbibbi.presentation.component.button.FlatButton +import com.no5ing.bbibbi.presentation.theme.bbibbiScheme +import com.no5ing.bbibbi.presentation.theme.bbibbiTypo +import com.skydoves.balloon.ArrowPositionRules +import com.skydoves.balloon.BalloonAnimation +import com.skydoves.balloon.BalloonSizeSpec +import com.skydoves.balloon.compose.Balloon +import com.skydoves.balloon.compose.rememberBalloonBuilder +import com.skydoves.balloon.compose.setBackgroundColor +import kotlinx.coroutines.flow.StateFlow +import java.time.LocalDate + +@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class) +@Composable +fun NightHomePageContent( + mainPageState: StateFlow>, + deferredPickStateSet: StateFlow>, + postViewTypeState: MutableState = remember { mutableStateOf(PostType.SURVIVAL) }, + onTapViewPost: (LocalDate) -> Unit = {}, + onTapProfile: (String) -> Unit = {}, + onTapPick: (MainPageTopBarModel) -> Unit = {}, + onTapInvite: () -> Unit = {}, + onRefresh: () -> Unit = {}, +) { + val warningState = remember { + mutableIntStateOf(0) + } + val balloonColor = MaterialTheme.bbibbiScheme.button + val balloonText = "생존신고 횟수가 동일한 경우\n이모지, 댓글 수를 합산해서 등수를 정해요" + val builder = rememberBalloonBuilder { + setArrowSize(10) + setArrowPosition(0.5f) + setArrowPositionRules(ArrowPositionRules.ALIGN_ANCHOR) + setWidth(BalloonSizeSpec.WRAP) + setHeight(BalloonSizeSpec.WRAP) + setMarginTop(12) + setPaddingVertical(10) + setPaddingHorizontal(16) + setMarginHorizontal(12) + setCornerRadius(12f) + setBackgroundColor(balloonColor) + // setBackgroundColorResource(balloonColor) + setBalloonAnimation(BalloonAnimation.ELASTIC) + } + + val mainPageModel by mainPageState.collectAsState() + var isRefreshing by remember { mutableStateOf(true) } + val pagerState = rememberPagerState(pageCount = { 2 }) + val pullRefreshStyle = rememberPullRefreshState( + refreshing = isRefreshing, + onRefresh = { + if (isRefreshing) return@rememberPullRefreshState + isRefreshing = true + onRefresh() + } + ) + LaunchedEffect(mainPageModel) { + if (mainPageModel.isReady()) { + isRefreshing = false + } + } + LaunchedEffect(postViewTypeState.value) { + val page = if (postViewTypeState.value == PostType.SURVIVAL) 0 else 1 + pagerState.animateScrollToPage(page) + } + LaunchedEffect(pagerState.currentPage) { + val type = if (pagerState.currentPage == 0) PostType.SURVIVAL else PostType.MISSION + if (type != postViewTypeState.value) { + postViewTypeState.value = type + } + } + BoxWithConstraints { + val scrollState = rememberScrollState() + + Column( + modifier = Modifier + // .fillMaxSize() + .pullRefresh(pullRefreshStyle) + .verticalScroll(state = scrollState) + ) { + HomePageStoryBar( + items = if (mainPageModel.isReady()) mainPageModel.data.topBarElements else emptyList(), + onTapProfile = onTapProfile, + onTapInvite = onTapInvite, + onTapPick = onTapPick, + deferredPickStateSet = deferredPickStateSet, + ) + Spacer(modifier = Modifier.height(24.dp)) + HorizontalDivider( + thickness = 1.dp, + color = MaterialTheme.bbibbiScheme.backgroundSecondary + ) + Spacer(modifier = Modifier.height(24.dp)) + UploadCountDownBar(warningState = warningState) + SurvivalTextDescription(warningState = warningState) + Spacer(modifier = Modifier.height(24.dp)) + if(mainPageModel.isReady()) { + val ranking = mainPageModel.data.familyMemberMonthlyRanking + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + .background( + MaterialTheme.bbibbiScheme.backgroundSecondary, + RoundedCornerShape(24.dp) + ) + ){ + Column( + modifier = Modifier.padding(vertical = 20.dp, horizontal = 20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxWidth() + // .padding(horizontal = 20.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = "이번달 최고 기여자", + style = MaterialTheme.bbibbiTypo.headTwoBold, + color = MaterialTheme.bbibbiScheme.textPrimary, + ) + Balloon( + builder = builder, + balloonContent = { + Text( + text = balloonText, + textAlign = TextAlign.Center, + color = MaterialTheme.bbibbiScheme.white, + style = MaterialTheme.bbibbiTypo.bodyTwoRegular, + ) + } + ) { + Icon( + painter = painterResource(id = R.drawable.warning_circle_icon), + tint = MaterialTheme.bbibbiScheme.textSecondary, + contentDescription = null, + modifier = Modifier + .size(20.dp) + .clickable { it.showAlignBottom() } + ) + } + } + + Text( + text = "${ranking.month}월 생존신고 횟수", + style = MaterialTheme.bbibbiTypo.bodyTwoRegular, + color = MaterialTheme.bbibbiScheme.textSecondary, + ) + } + Spacer(modifier = Modifier.height(24.dp)) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 4.dp), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Box( + Modifier.padding(top = 36.dp) + ) { + RankIcon( + model = ranking.secondRanker, + outerCircleSize = 72.dp, + innerCircleSize = 64.dp, + borderColor = getRankColor(rank = 1)!!, + noRankImage = painterResource(id = R.drawable.second_ranking_empty), + rankImage = painterResource(id = R.drawable.second_ranking), + badgeHeight = 27.dp + ) + } + + RankIcon( + model = ranking.firstRanker, + outerCircleSize = 90.dp, + innerCircleSize = 82.dp, + borderColor = getRankColor(rank = 0)!!, + noRankImage = painterResource(id = R.drawable.first_ranking_empty), + rankImage = painterResource(id = R.drawable.first_ranking), + badgeHeight = 32.dp + ) + Box( + Modifier.padding(top = 36.dp) + ) { + RankIcon( + model = ranking.thirdRanker, + outerCircleSize = 72.dp, + innerCircleSize = 64.dp, + borderColor = getRankColor(rank = 2)!!, + noRankImage = painterResource(id = R.drawable.third_ranking_empty), + rankImage = painterResource(id = R.drawable.third_ranking), + badgeHeight = 27.dp + ) + } + } + Spacer(modifier = Modifier.height(36.dp)) + if(ranking.isAllRankersNull()) { + Text( + text = "아직 활동한 가족이 없어요", + style = MaterialTheme.bbibbiTypo.bodyOneBold, + color = MaterialTheme.bbibbiScheme.textSecondary, + ) + Spacer(modifier = Modifier.height(8.dp)) + } else { + FlatButton( + text = "지난 날 생존신고 보기", + modifier = Modifier.fillMaxWidth(), + onClick = { + onTapViewPost(LocalDate.now()) + }, + ) + } + } + } + } + } + PullRefreshIndicator( + refreshing = isRefreshing, + state = pullRefreshStyle, + modifier = Modifier.align(Alignment.TopCenter), + backgroundColor = MaterialTheme.bbibbiScheme.backgroundSecondary, + contentColor = MaterialTheme.bbibbiScheme.iconSelected, + ) + } +} + +@Composable +fun RankIcon( + model: MainPageMonthlyRankerModel?, + outerCircleSize: Dp, + innerCircleSize: Dp, + borderColor: Color, + noRankImage: Painter, + rankImage: Painter, + badgeHeight: Dp, +) { + if(model == null) { + Column( + verticalArrangement = Arrangement.spacedBy(9.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box(contentAlignment = Alignment.Center, modifier = Modifier.padding(bottom = 12.dp)) { + Box( + Modifier + .size(outerCircleSize) + .background(MaterialTheme.bbibbiScheme.gray600, CircleShape) + ) + Box( + Modifier + .size(innerCircleSize) + .background(MaterialTheme.bbibbiScheme.backgroundHover, CircleShape), + contentAlignment = Alignment.Center, + ) { + Text(text = "?", style = TextStyle( + fontSize = 33.75.sp, + fontWeight = FontWeight.SemiBold + ), color = MaterialTheme.colorScheme.onSurface) + } + Image( + painter = noRankImage, + contentDescription = null, + modifier = Modifier + .height(badgeHeight) + .align(Alignment.BottomCenter) + .offset(y = 12.dp) + ) + } + CommonEmptyBlock() + } + } else { + Column( + verticalArrangement = Arrangement.spacedBy(9.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box(contentAlignment = Alignment.Center, modifier = Modifier.padding(bottom = 12.dp)) { + Box( + Modifier + .size(outerCircleSize) + .background(borderColor, CircleShape) + ) + CircleProfileImage( + size = innerCircleSize, + noImageLetter = model.name.first().toString(), + imageUrl = model.profileImageUrl + ) + Image( + painter = rankImage, + contentDescription = null, + modifier = Modifier + .height(badgeHeight) + .align(Alignment.BottomCenter) + .offset(y = 12.dp) + ) + } + CommonTextBlock( + name = model.name, + count = model.survivalCount + ) + } + } + + +} + +@Composable +fun CommonEmptyBlock() { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box(modifier = Modifier + .size(width = 53.dp, height = 16.dp) + .background(MaterialTheme.bbibbiScheme.gray600, RoundedCornerShape(4.dp)) + ) + Box(modifier = Modifier + .size(width = 29.dp, height = 12.dp) + .background(MaterialTheme.bbibbiScheme.button, RoundedCornerShape(4.dp)) + ) + } +} + +@Composable +private fun CommonTextBlock( + name: String, + count: Int, +) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = name, + style = MaterialTheme.bbibbiTypo.bodyTwoBold, + color = MaterialTheme.bbibbiScheme.textPrimary, + ) + Text( + text = "${count}회", + style = MaterialTheme.bbibbiTypo.bodyTwoRegular, + color = MaterialTheme.bbibbiScheme.textSecondary, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/HomePageController.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/HomePageController.kt index 8297374..b4c09e6 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/HomePageController.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/HomePageController.kt @@ -18,6 +18,7 @@ import com.no5ing.bbibbi.presentation.feature.view.main.home.HomePage import com.no5ing.bbibbi.presentation.feature.view.main.home.TryPickPopup import com.no5ing.bbibbi.presentation.feature.view_controller.CameraViewPageController.goCameraViewPage import com.no5ing.bbibbi.presentation.feature.view_controller.NavigationDestination +import com.no5ing.bbibbi.presentation.feature.view_controller.main.CalendarDetailPageController.goCalendarDetailPage import com.no5ing.bbibbi.presentation.feature.view_controller.main.CalendarPageController.goCalendarPage import com.no5ing.bbibbi.presentation.feature.view_controller.main.FamilyListPageController.goFamilyListPage import com.no5ing.bbibbi.presentation.feature.view_controller.main.PostReUploadPageController.goPostReUploadPage @@ -94,6 +95,9 @@ object HomePageController : NavigationDestination( isPickDialogVisible = true }, mainPageViewModel = mainPageViewModel, + onTapViewPost = { date -> + navController.goCalendarDetailPage(date) + } ) } diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/MainPageNightViewModel.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/MainPageNightViewModel.kt new file mode 100644 index 0000000..a9cd409 --- /dev/null +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/MainPageNightViewModel.kt @@ -0,0 +1,30 @@ +package com.no5ing.bbibbi.presentation.feature.view_model + +import com.no5ing.bbibbi.data.datasource.local.LocalDataStorage +import com.no5ing.bbibbi.data.datasource.network.RestAPI +import com.no5ing.bbibbi.data.model.APIResponse +import com.no5ing.bbibbi.data.model.APIResponse.Companion.wrapToAPIResponse +import com.no5ing.bbibbi.data.model.view.NightMainPageModel +import com.no5ing.bbibbi.data.repository.Arguments +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import javax.inject.Inject + +@HiltViewModel +class MainPageNightViewModel @Inject constructor( + private val restAPI: RestAPI, + private val localDataStorage: LocalDataStorage, +) : BaseViewModel>() { + + override fun initState(): APIResponse { + return APIResponse.idle() + } + + override fun invoke(arguments: Arguments) { + withMutexScope(Dispatchers.IO) { + val mainResult = restAPI.getViewApi().getNightMainView() + val apiResult = mainResult.wrapToAPIResponse() + setState(apiResult) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/first_ranking.png b/app/src/main/res/drawable/first_ranking.png new file mode 100644 index 0000000000000000000000000000000000000000..763cdb1f0b72d4bbbaf5abe2f907d33c2595fdc7 GIT binary patch literal 2977 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 VS+2)nQv?72002ovPDHLkV1h<~fH(jE literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/third_ranking.png b/app/src/main/res/drawable/third_ranking.png new file mode 100644 index 0000000000000000000000000000000000000000..4f9a8758b1cec483eaa569bf0545f8b9d615bd2f GIT binary patch literal 3024 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 literal 0 HcmV?d00001 From 72403e4e035db6f191959fde1b6f3d472eafdf5c Mon Sep 17 00:00:00 2001 From: ChuYong Date: Thu, 25 Apr 2024 16:11:29 +0900 Subject: [PATCH 11/26] =?UTF-8?q?feat:=20=EC=B6=94=EA=B0=80=EB=90=9C=20API?= =?UTF-8?q?=20=EB=8C=80=EC=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/no5ing/bbibbi/data/model/view/MainPageModel.kt | 5 ++++- .../presentation/feature/view/main/home/HomePage.kt | 8 +++++--- .../feature/view/main/home/NightHomePageContent.kt | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/no5ing/bbibbi/data/model/view/MainPageModel.kt b/app/src/main/java/com/no5ing/bbibbi/data/model/view/MainPageModel.kt index 85a8da0..2be9924 100644 --- a/app/src/main/java/com/no5ing/bbibbi/data/model/view/MainPageModel.kt +++ b/app/src/main/java/com/no5ing/bbibbi/data/model/view/MainPageModel.kt @@ -3,13 +3,15 @@ package com.no5ing.bbibbi.data.model.view import android.os.Parcelable import com.no5ing.bbibbi.data.model.BaseModel import kotlinx.parcelize.Parcelize +import java.time.LocalDate import java.time.ZonedDateTime @Parcelize data class MainPageModel( val topBarElements: List, val isMissionUnlocked: Boolean, - val isMeUploadedToday: Boolean, + val isMeSurvivalUploadedToday: Boolean, + val isMeMissionUploadedToday: Boolean, val survivalFeeds: List, val missionFeeds: List, val pickers: List, @@ -55,6 +57,7 @@ data class MainPageMonthlyRankingModel( val firstRanker: MainPageMonthlyRankerModel?, val secondRanker: MainPageMonthlyRankerModel?, val thirdRanker: MainPageMonthlyRankerModel?, + val mostRecentSurvivalPostDate: LocalDate?, ) : Parcelable { fun isAllRankersNull(): Boolean { return firstRanker == null && secondRanker == null && thirdRanker == null diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePage.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePage.kt index edfe02f..a172dc3 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePage.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePage.kt @@ -133,16 +133,18 @@ fun HomePage( isLoading = !mainPageState.value.isReady(), isUploadAbleTime = remember { gapUntilNext() > 0 }, isAlreadyUploaded = !mainPageState.value.isReady() || - mainPageState.value.data.isMeUploadedToday, + mainPageState.value.data.isMeSurvivalUploadedToday, pickers = if(mainPageState.value.isReady()) mainPageState.value.data.pickers else emptyList(), ) } else { HomePageMissionUploadButton( isLoading = mainPageState.value.isLoading(), - isMeUploadedToday = mainPageState.value.isReady() && mainPageState.value.data.isMeUploadedToday, + isMeUploadedToday = mainPageState.value.isReady() + && mainPageState.value.data.isMeSurvivalUploadedToday, isMissionUnlocked = mainPageState.value.isReady() && mainPageState.value.data.isMissionUnlocked, - isMeMissionUploaded = false, + isMeMissionUploaded = mainPageState.value.isReady() + && mainPageState.value.data.isMeMissionUploadedToday, ) } diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/NightHomePageContent.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/NightHomePageContent.kt index 2ddd34f..25a387c 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/NightHomePageContent.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/NightHomePageContent.kt @@ -267,7 +267,7 @@ fun NightHomePageContent( text = "지난 날 생존신고 보기", modifier = Modifier.fillMaxWidth(), onClick = { - onTapViewPost(LocalDate.now()) + ranking.mostRecentSurvivalPostDate?.apply(onTapViewPost) }, ) } From cbb84f1ff551eae9fbd48dbf2f123202d3e415af Mon Sep 17 00:00:00 2001 From: ChuYong Date: Thu, 25 Apr 2024 17:19:14 +0900 Subject: [PATCH 12/26] =?UTF-8?q?feat:=20=EB=AF=B8=EC=85=98=20=EC=97=85?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/datasource/local/LocalDataStorage.kt | 14 + .../bbibbi/data/datasource/network/RestAPI.kt | 5 + .../bbibbi/data/model/mission/Mission.kt | 13 + .../component/button/CameraCaptureButton.kt | 3 +- .../feature/view/common/GenericPopup.kt | 137 +++++++++ .../feature/view/main/home/HomePage.kt | 2 + .../view/main/home/HomePageUploadButton.kt | 1 + .../main/mission_upload/MissionUploadPage.kt | 260 +++++++++++++++++ .../UploadMissionCamera.kt | 276 ++++++++++++++++++ .../UploadMissionCameraBar.kt | 52 ++++ .../UploadMissionDisplayBar.kt | 39 +++ .../UploadMissionPreviewBox.kt | 60 ++++ .../UploadMissionTopBar.kt | 16 + .../view_controller/NavigationDestination.kt | 2 + .../main/HomePageController.kt | 75 +++++ .../main/MissionUploadPageController.kt | 30 ++ .../main/UploadMissionPageController.kt | 31 ++ .../feature/view_model/MainPageViewModel.kt | 11 + .../mission/GetTodayMissionViewModel.kt | 28 ++ .../view_model/post/CreatePostViewModel.kt | 3 +- .../navigation/graph/MainNavGraph.kt | 10 + app/src/main/res/drawable/mission_badge.png | Bin 0 -> 2625 bytes app/src/main/res/drawable/mission_key.png | Bin 0 -> 37922 bytes .../res/drawable/mission_require_survival.png | Bin 0 -> 45929 bytes app/src/main/res/values/strings.xml | 2 + 25 files changed, 1068 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/no5ing/bbibbi/data/model/mission/Mission.kt create mode 100644 app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/common/GenericPopup.kt create mode 100644 app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload/MissionUploadPage.kt create mode 100644 app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload_camera/UploadMissionCamera.kt create mode 100644 app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload_camera/UploadMissionCameraBar.kt create mode 100644 app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload_camera/UploadMissionDisplayBar.kt create mode 100644 app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload_camera/UploadMissionPreviewBox.kt create mode 100644 app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload_camera/UploadMissionTopBar.kt create mode 100644 app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/MissionUploadPageController.kt create mode 100644 app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/UploadMissionPageController.kt create mode 100644 app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/mission/GetTodayMissionViewModel.kt create mode 100644 app/src/main/res/drawable/mission_badge.png create mode 100644 app/src/main/res/drawable/mission_key.png create mode 100644 app/src/main/res/drawable/mission_require_survival.png diff --git a/app/src/main/java/com/no5ing/bbibbi/data/datasource/local/LocalDataStorage.kt b/app/src/main/java/com/no5ing/bbibbi/data/datasource/local/LocalDataStorage.kt index a8a95d1..484c089 100644 --- a/app/src/main/java/com/no5ing/bbibbi/data/datasource/local/LocalDataStorage.kt +++ b/app/src/main/java/com/no5ing/bbibbi/data/datasource/local/LocalDataStorage.kt @@ -9,6 +9,7 @@ import com.google.gson.Gson import com.no5ing.bbibbi.data.model.auth.AuthResult import com.no5ing.bbibbi.data.model.member.MemberRealEmoji import timber.log.Timber +import java.time.LocalDate import javax.inject.Inject import javax.inject.Singleton @@ -41,6 +42,7 @@ class LocalDataStorage @Inject constructor(val context: Context) { const val REAL_EMOJI_KEY = "real_emoji" const val TEMPORARY_POST_URI = "temporary_post_uri" const val WIDGET_POPUP_PERIOD_KEY = "widget_popup_period" + const val MISSION_WIDGET_PERIOD_KEY = "mission_widget_period" } fun logOut() { @@ -140,4 +142,16 @@ class LocalDataStorage @Inject constructor(val context: Context) { } return false } + + fun getLastWidgetPopupSeenDate(): LocalDate? { + val date = preferences.getString(MISSION_WIDGET_PERIOD_KEY, null) + return date?.let(LocalDate::parse) + } + + fun setLastWidgetPopupSeenDate(date: LocalDate) { + val editor = preferences.edit() + editor.putString(MISSION_WIDGET_PERIOD_KEY, date.toString()) + editor.apply() + editor.commit() + } } \ No newline at end of file diff --git a/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt b/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt index b38adf0..642110d 100644 --- a/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt +++ b/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt @@ -29,6 +29,7 @@ import com.no5ing.bbibbi.data.model.member.ImageUploadLink import com.no5ing.bbibbi.data.model.member.Member import com.no5ing.bbibbi.data.model.member.MemberRealEmoji import com.no5ing.bbibbi.data.model.member.MemberRealEmojiList +import com.no5ing.bbibbi.data.model.mission.Mission import com.no5ing.bbibbi.data.model.post.CalendarBanner import com.no5ing.bbibbi.data.model.post.CalendarElement import com.no5ing.bbibbi.data.model.post.Post @@ -175,6 +176,7 @@ interface RestAPI { @POST("v1/posts") suspend fun createPost( @Body body: CreatePostRequest, + @Query("type") type: String? = null, ): ApiResponse @POST("v1/posts/image-upload-request") @@ -269,6 +271,9 @@ interface RestAPI { @Path("postRealEmojiId") postRealEmojiId: String, ): ApiResponse + @GET("v1/missions/today") + suspend fun getDailyMission(): ApiResponse + } /** diff --git a/app/src/main/java/com/no5ing/bbibbi/data/model/mission/Mission.kt b/app/src/main/java/com/no5ing/bbibbi/data/model/mission/Mission.kt new file mode 100644 index 0000000..1d4f081 --- /dev/null +++ b/app/src/main/java/com/no5ing/bbibbi/data/model/mission/Mission.kt @@ -0,0 +1,13 @@ +package com.no5ing.bbibbi.data.model.mission + +import android.os.Parcelable +import com.no5ing.bbibbi.data.model.BaseModel +import kotlinx.parcelize.Parcelize +import java.time.LocalDate + +@Parcelize +data class Mission( + val id: String, + val date: LocalDate, + val content: String, +) : Parcelable, BaseModel() diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/component/button/CameraCaptureButton.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/component/button/CameraCaptureButton.kt index 4e012ef..562caf2 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/component/button/CameraCaptureButton.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/component/button/CameraCaptureButton.kt @@ -15,6 +15,7 @@ fun CameraCaptureButton( modifier: Modifier = Modifier, onClick: () -> Unit = {}, isCapturing: Boolean, + ignoreDisabledState: Boolean = false, ) { val mixPanel = LocalMixpanelProvider.current Image( @@ -24,7 +25,7 @@ fun CameraCaptureButton( .size(80.dp) .clickable { mixPanel.track("Click_btn_camera") - if (!isCapturing) { + if (!isCapturing || ignoreDisabledState) { onClick() } }, diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/common/GenericPopup.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/common/GenericPopup.kt new file mode 100644 index 0000000..e8d1c1b --- /dev/null +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/common/GenericPopup.kt @@ -0,0 +1,137 @@ +package com.no5ing.bbibbi.presentation.feature.view.common + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +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.shape.RoundedCornerShape +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.AlertDialogDefaults +import androidx.compose.material3.BasicAlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties +import com.no5ing.bbibbi.R +import com.no5ing.bbibbi.presentation.feature.view.common.AlertDialogContent +import com.no5ing.bbibbi.presentation.feature.view.common.AlertDialogFlowRow +import com.no5ing.bbibbi.presentation.feature.view.common.ButtonsCrossAxisSpacing +import com.no5ing.bbibbi.presentation.feature.view.common.ButtonsMainAxisSpacing +import com.no5ing.bbibbi.presentation.theme.bbibbiScheme +import com.no5ing.bbibbi.presentation.theme.bbibbiTypo + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun GenericPopup( + enabledState: Boolean = false, + title: String, + description: String, + image: Painter, + confirmText: String, + cancelText: String, + onTapConfirm: () -> Unit = {}, + onTapCancel: () -> Unit = {}, +) { + if (enabledState) { + BasicAlertDialog( + onDismissRequest = onTapCancel, + modifier = Modifier, + properties = DialogProperties() + ) { + AlertDialogContent( + textPadding = PaddingValues(0.dp), + buttons = { + AlertDialogFlowRow( + mainAxisSpacing = ButtonsMainAxisSpacing, + crossAxisSpacing = 8.dp + ) { + Button( + onClick = onTapConfirm, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.bbibbiScheme.mainYellow + ), + shape = RoundedCornerShape(10.dp), + modifier = Modifier + .height(44.dp) + .fillMaxWidth() + ) { + Text( + confirmText, + style = MaterialTheme.bbibbiTypo.bodyOneBold, + color = Color(0xff242427) + ) + } + Button( + onClick = onTapCancel, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.bbibbiScheme.button + ), + shape = RoundedCornerShape(10.dp), + modifier = Modifier + .height(44.dp) + .fillMaxWidth() + ) { + Text( + cancelText, + style = MaterialTheme.bbibbiTypo.bodyOneBold, + color = MaterialTheme.bbibbiScheme.icon + ) + } + } + }, + icon = null, + title = { + Text( + title, + color = MaterialTheme.bbibbiScheme.iconSelected, + style = MaterialTheme.bbibbiTypo.headTwoBold, + ) + }, + text = { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth() + ) { + Text( + description, + color = MaterialTheme.bbibbiScheme.textSecondary, + style = MaterialTheme.bbibbiTypo.bodyTwoRegular, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.height(24.dp)) + Image( + painter = image, + contentDescription = null, + ) + } + + }, + shape = RoundedCornerShape(14.dp), + containerColor = MaterialTheme.bbibbiScheme.backgroundPrimary, + tonalElevation = AlertDialogDefaults.TonalElevation, + iconContentColor = AlertDialogDefaults.iconContentColor, + titleContentColor = AlertDialogDefaults.titleContentColor, + textContentColor = AlertDialogDefaults.textContentColor, + modifier = Modifier.padding(0.dp) + ) + + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePage.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePage.kt index a172dc3..3f47a7d 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePage.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePage.kt @@ -45,6 +45,7 @@ fun HomePage( onTapProfile: (String) -> Unit = {}, onTapContent: (String) -> Unit = {}, onTapUpload: () -> Unit = {}, + onTapMissionUpload: () -> Unit = {}, onTapInvite: () -> Unit = {}, onUnsavedPost: (Uri) -> Unit = {}, onTapViewPost: (LocalDate) -> Unit = {}, @@ -139,6 +140,7 @@ fun HomePage( ) } else { HomePageMissionUploadButton( + onTap = onTapMissionUpload, isLoading = mainPageState.value.isLoading(), isMeUploadedToday = mainPageState.value.isReady() && mainPageState.value.data.isMeSurvivalUploadedToday, diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageUploadButton.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageUploadButton.kt index 2a50adc..b5ed452 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageUploadButton.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageUploadButton.kt @@ -131,6 +131,7 @@ fun BoxScope.HomePageMissionUploadButton( CameraCaptureButton( onClick = onTap, isCapturing = !(!isMeMissionUploaded && isMissionUnlocked && isMeUploadedToday), + ignoreDisabledState = true, ) } } diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload/MissionUploadPage.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload/MissionUploadPage.kt new file mode 100644 index 0000000..8989ee1 --- /dev/null +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload/MissionUploadPage.kt @@ -0,0 +1,260 @@ +package com.no5ing.bbibbi.presentation.feature.view.main.mission_upload + +import android.graphics.ImageDecoder +import android.net.Uri +import android.os.Build +import android.provider.MediaStore +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +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.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +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.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.no5ing.bbibbi.R +import com.no5ing.bbibbi.data.repository.Arguments +import com.no5ing.bbibbi.presentation.component.BBiBBiPreviewSurface +import com.no5ing.bbibbi.presentation.component.BBiBBiSurface +import com.no5ing.bbibbi.presentation.component.showSnackBarWithDismiss +import com.no5ing.bbibbi.presentation.component.snackBarCamera +import com.no5ing.bbibbi.presentation.component.snackBarWarning +import com.no5ing.bbibbi.presentation.feature.view.main.mission_upload_camera.UploadMissionDisplayBar +import com.no5ing.bbibbi.presentation.feature.view.main.post_upload.PostUploadPageImagePreview +import com.no5ing.bbibbi.presentation.feature.view.main.post_upload.PostUploadPageTextOverlay +import com.no5ing.bbibbi.presentation.feature.view.main.post_upload.PostUploadPageTopBar +import com.no5ing.bbibbi.presentation.feature.view.main.post_upload.PostUploadPageUploadBar +import com.no5ing.bbibbi.presentation.feature.view_model.mission.GetTodayMissionViewModel +import com.no5ing.bbibbi.presentation.feature.view_model.post.CreatePostViewModel +import com.no5ing.bbibbi.util.LocalMixpanelProvider +import com.no5ing.bbibbi.util.LocalSnackbarHostState +import com.no5ing.bbibbi.util.codePointLength +import com.no5ing.bbibbi.util.getErrorMessage +import kotlinx.coroutines.launch + + +@Composable +fun MissionUploadPage( + onDispose: () -> Unit, + todayMissionViewModel: GetTodayMissionViewModel = hiltViewModel(), + imageUrl: State, + imageText: MutableState = remember { + mutableStateOf("") + }, + textOverlayShown: MutableState = remember { + mutableStateOf(false) + }, + createPostViewModel: CreatePostViewModel = hiltViewModel(), +) { + LaunchedEffect(Unit) { + if (todayMissionViewModel.uiState.value.isIdle()) { + todayMissionViewModel.invoke(Arguments()) + } + } + + val coroutineScope = rememberCoroutineScope() + val context = LocalContext.current + val snackBarHost = LocalSnackbarHostState.current + val maxWord = 8 + val snackWarningMessage = stringResource(id = R.string.snack_bar_word_limit, maxWord) + val snackNoSpaceMessage = stringResource(id = R.string.snack_bar_no_space) + val snackSavedMessage = stringResource(id = R.string.snack_bar_saved) + LaunchedEffect(Unit) { + if (imageUrl.value == null) { + onDispose() + } + } + val mixPanel = LocalMixpanelProvider.current + val missionModel by todayMissionViewModel.uiState.collectAsState() + val uploadResult = createPostViewModel.uiState.collectAsState() + val snackErrorMessage = getErrorMessage(errorCode = uploadResult.value.errorCode) + LaunchedEffect(uploadResult.value) { + if (uploadResult.value.isReady()) { + onDispose() + } else if (uploadResult.value.isFailed()) { + snackBarHost.showSnackBarWithDismiss( + message = snackErrorMessage, + actionLabel = snackBarWarning + ) + createPostViewModel.resetState() + } + } + BBiBBiSurface( + modifier = Modifier.fillMaxSize(), + ) { + Box { + Box(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier + .padding(vertical = 10.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + PostUploadPageTopBar( + onDispose = onDispose, + ) + Spacer(modifier = Modifier.height(48.dp)) + UploadMissionDisplayBar( + missionText = if(missionModel.isReady()) missionModel.data.content else "" + ) + Spacer(modifier = Modifier.height(16.dp)) + PostUploadPageImagePreview( + previewImgUrl = imageUrl.value, + imageTextState = imageText, + onTapImageTextButton = { + mixPanel.track("Click_PhotoText") + textOverlayShown.value = true + } + ) + Spacer(modifier = Modifier.height(48.dp)) + PostUploadPageUploadBar( + isIdle = uploadResult.value.isIdle(), + onClickUpload = { + mixPanel.track("Click_UploadPhoto") + createPostViewModel.invoke( + Arguments( + arguments = mapOf( + "imageUri" to imageUrl.value.toString(), + "content" to imageText.value, + "type" to "MISSION" + ) + ) + ) + }, + onClickSave = { + val bitmap = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) + ImageDecoder.decodeBitmap( + ImageDecoder.createSource( + context.contentResolver, + imageUrl.value!! + ) + ) + else + MediaStore.Images.Media.getBitmap( + context.contentResolver, + imageUrl.value + ) + MediaStore.Images.Media.insertImage( + context.contentResolver, + bitmap, + imageText.value, + "bbibbi" + ) + coroutineScope.launch { + snackBarHost.showSnackBarWithDismiss( + message = snackSavedMessage, + actionLabel = snackBarCamera + ) + } + } + ) + } + } + AnimatedVisibility( + textOverlayShown.value, + enter = fadeIn(), + exit = fadeOut(), + ) { + PostUploadPageTextOverlay( + imageText = imageText, + onDispose = { + textOverlayShown.value = false + }, + onTextInputChanged = { nextValue -> + if (nextValue.codePointLength() <= 8) { + if (nextValue.contains(" ")) { + coroutineScope.launch { + snackBarHost.showSnackBarWithDismiss( + message = snackNoSpaceMessage, + actionLabel = snackBarWarning + ) + } + return@PostUploadPageTextOverlay + } + imageText.value = nextValue + } else { + coroutineScope.launch { + snackBarHost.showSnackBarWithDismiss( + message = snackWarningMessage, + actionLabel = snackBarWarning + ) + } + } + }, + onClearTextInput = { + imageText.value = "" + } + ) + } + + } + } + +} + +@Preview( + showBackground = true, + name = "MissionUploadPagePreview", + showSystemUi = true +) +@Composable +fun MissionUploadPagePreview() { + var isActive by remember { mutableStateOf(false) } + val imageText = remember { + mutableStateOf("글자테스트") + } + BBiBBiPreviewSurface { + Box { + Column( + modifier = Modifier.fillMaxSize(), + ) { + PostUploadPageTopBar() + Spacer(modifier = Modifier.height(48.dp)) + PostUploadPageImagePreview( + previewImgUrl = null, + imageTextState = imageText, + onTapImageTextButton = { + isActive = true + } + ) + Spacer(modifier = Modifier.height(48.dp)) + PostUploadPageUploadBar( + isIdle = true + ) + } + AnimatedVisibility( + isActive, + enter = fadeIn(), + exit = fadeOut(), + ) { + PostUploadPageTextOverlay( + imageText = imageText, + onDispose = { + isActive = false + } + ) + } + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload_camera/UploadMissionCamera.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload_camera/UploadMissionCamera.kt new file mode 100644 index 0000000..a690a6e --- /dev/null +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload_camera/UploadMissionCamera.kt @@ -0,0 +1,276 @@ +package com.no5ing.bbibbi.presentation.feature.view.main.mission_upload_camera + +import android.net.Uri +import android.util.Rational +import android.view.Surface.ROTATION_0 +import androidx.camera.core.Camera +import androidx.camera.core.CameraSelector +import androidx.camera.core.ImageCapture +import androidx.camera.core.Preview +import androidx.camera.core.UseCaseGroup +import androidx.camera.core.ViewPort +import androidx.camera.view.PreviewView +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +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.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +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.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState +import com.no5ing.bbibbi.data.model.APIResponse +import com.no5ing.bbibbi.data.repository.Arguments +import com.no5ing.bbibbi.presentation.component.BBiBBiPreviewSurface +import com.no5ing.bbibbi.presentation.component.BBiBBiSurface +import com.no5ing.bbibbi.presentation.component.showSnackBarWithDismiss +import com.no5ing.bbibbi.presentation.component.snackBarWarning +import com.no5ing.bbibbi.presentation.feature.view_model.mission.GetTodayMissionViewModel +import com.no5ing.bbibbi.presentation.feature.view_model.post.MemberRealEmojiListViewModel +import com.no5ing.bbibbi.presentation.feature.view_model.post.UpdateMemberPostRealEmojiViewModel +import com.no5ing.bbibbi.util.LocalSnackbarHostState +import com.no5ing.bbibbi.util.getCameraProvider +import com.no5ing.bbibbi.util.getErrorMessage +import com.no5ing.bbibbi.util.localResources +import com.no5ing.bbibbi.util.takePhotoWithImage +import kotlinx.coroutines.launch + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun UploadMissionCamera( + todayMissionViewModel: GetTodayMissionViewModel = hiltViewModel(), + updateMemberPostRealEmojiViewModel: UpdateMemberPostRealEmojiViewModel = hiltViewModel(), + memberRealEmojiListViewModel: MemberRealEmojiListViewModel = hiltViewModel(), + onDispose: () -> Unit = {}, + onImageCaptured: (Uri?) -> Unit = {}, +) { + val lifecycleOwner = LocalLifecycleOwner.current + val context = LocalContext.current + val snackBarHost = LocalSnackbarHostState.current + val haptic = LocalHapticFeedback.current + + val uploadState by updateMemberPostRealEmojiViewModel.uiState.collectAsState() + val emojiMap by memberRealEmojiListViewModel.uiState.collectAsState() + val coroutineScope = rememberCoroutineScope() + val torchState = remember { mutableStateOf(false) } + val isPermissionGranted = remember { mutableStateOf(false) } + val cameraDirection = remember { mutableStateOf(CameraSelector.DEFAULT_FRONT_CAMERA) } + val cameraState = remember { mutableStateOf(null) } + val captureState = remember { mutableStateOf(ImageCapture.Builder().build()) } + var isCapturing by remember { mutableStateOf(false) } + var isZoomed by remember { mutableStateOf(false) } + val previewView = remember { + PreviewView( + context, + ).apply { + implementationMode = PreviewView.ImplementationMode.COMPATIBLE + scaleType = PreviewView.ScaleType.FIT_CENTER + } + } + + LaunchedEffect(Unit) { + if (todayMissionViewModel.uiState.value.isIdle()) { + todayMissionViewModel.invoke(Arguments()) + } + } + + val resources = localResources() + LaunchedEffect(uploadState) { + when (uploadState.status) { + is APIResponse.Status.SUCCESS -> { + updateMemberPostRealEmojiViewModel.resetState() + memberRealEmojiListViewModel.invoke(Arguments()) + } + + is APIResponse.Status.ERROR -> { + snackBarHost.showSnackBarWithDismiss( + message = resources.getErrorMessage(errorCode = uploadState.errorCode), + actionLabel = snackBarWarning + ) + updateMemberPostRealEmojiViewModel.resetState() + } + + else -> {} + } + } + + LaunchedEffect(Unit) { + memberRealEmojiListViewModel.invoke(Arguments()) + } + + LaunchedEffect(cameraDirection.value, isPermissionGranted.value) { + if (!isPermissionGranted.value) return@LaunchedEffect + val cameraProvider = context.getCameraProvider() + cameraProvider.unbindAll() + + val preview = Preview + .Builder() + .build() + val capturer = ImageCapture + .Builder() + .build() + + captureState.value = capturer + + val useCaseGroup = UseCaseGroup + .Builder() + .addUseCase(capturer) + .addUseCase(preview) + .setViewPort( + ViewPort.Builder( + Rational( + previewView.width, + previewView.height + ), + ROTATION_0 + ).build() + ) + .build() + cameraState.value = cameraProvider.bindToLifecycle( + lifecycleOwner, + cameraDirection.value, + useCaseGroup + ) + preview.setSurfaceProvider(previewView.surfaceProvider) + } + + val perm = + rememberPermissionState(permission = android.Manifest.permission.CAMERA) { isGranted -> + if (!isGranted) { + onDispose() + } else { + isPermissionGranted.value = true + } + } + LaunchedEffect(Unit) { + if (perm.status.isGranted) { + isPermissionGranted.value = true + } else { + perm.launchPermissionRequest() + } + } + val zoomValue: Float by animateFloatAsState( + targetValue = if (isZoomed) 0.5f else 0.0f, + animationSpec = tween( + durationMillis = 150, + easing = LinearEasing, + ), label = "" + ) + LaunchedEffect(zoomValue) { + cameraState.value?.cameraControl?.setLinearZoom( + zoomValue + ) + } + val missionModel by todayMissionViewModel.uiState.collectAsState() + BBiBBiSurface( + modifier = Modifier.fillMaxSize(), + ) { + Column( + modifier = Modifier + .padding(vertical = 10.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + UploadMissionTopBar( + onDispose = onDispose, + ) + Spacer(modifier = Modifier.height(48.dp)) + UploadMissionDisplayBar( + missionText = if(missionModel.isReady()) missionModel.data.content else "" + ) + Spacer(modifier = Modifier.height(16.dp)) + UploadMissionPreviewBox( + viewFactory = { previewView }, + onTapZoom = { + isZoomed = !isZoomed + } + ) + Spacer(modifier = Modifier.height(36.dp)) + UploadMissionCameraBar( + isCapturing = isCapturing || !uploadState.isIdle(), + onClickTorch = { + torchState.value = !torchState.value + cameraState.value?.cameraControl?.enableTorch(torchState.value) + }, + onClickCapture = { + if (isCapturing) return@UploadMissionCameraBar + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + coroutineScope.launch { + isCapturing = true + val image = captureState.value.takePhotoWithImage( + context, + requiredFlip = cameraDirection.value == CameraSelector.DEFAULT_FRONT_CAMERA + ) + isCapturing = false + onImageCaptured(image) + } + }, + onClickRotate = { + cameraDirection.value = run { + if (cameraDirection.value == CameraSelector.DEFAULT_BACK_CAMERA) { + CameraSelector.DEFAULT_FRONT_CAMERA + } else { + CameraSelector.DEFAULT_BACK_CAMERA + } + } + } + ) + Spacer(modifier = Modifier.height(36.dp)) + } + + } + +} + +@androidx.compose.ui.tooling.preview.Preview( + showBackground = true, + name = "CreateRealEmojiPagePreview", + showSystemUi = true +) +@Composable +fun CreateRealEmojiPagePreview() { + val context = LocalContext.current + BBiBBiPreviewSurface { + Box { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + UploadMissionTopBar() + Spacer(modifier = Modifier.height(48.dp)) + UploadMissionDisplayBar( + missionText = "오늘 입고 간 옷이 잘 나오도록 찍어주세요" + ) + Spacer(modifier = Modifier.height(16.dp)) + UploadMissionPreviewBox( + viewFactory = { PreviewView(context) }, + ) + Spacer(modifier = Modifier.height(36.dp)) + UploadMissionCameraBar( + isCapturing = false, + ) + Spacer(modifier = Modifier.height(36.dp)) + } + } + } +} diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload_camera/UploadMissionCameraBar.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload_camera/UploadMissionCameraBar.kt new file mode 100644 index 0000000..36ab808 --- /dev/null +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload_camera/UploadMissionCameraBar.kt @@ -0,0 +1,52 @@ +package com.no5ing.bbibbi.presentation.feature.view.main.mission_upload_camera + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.no5ing.bbibbi.R +import com.no5ing.bbibbi.presentation.component.button.CameraCaptureButton + +@Composable +fun UploadMissionCameraBar( + isCapturing: Boolean, + onClickTorch: () -> Unit = {}, + onClickCapture: () -> Unit = {}, + onClickRotate: () -> Unit = {}, +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceAround, + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + painter = painterResource(R.drawable.toggle_flash_button), + contentDescription = null, // 필수 param + modifier = Modifier + .size(48.dp) + .clickable { + onClickTorch() + } + ) + CameraCaptureButton( + onClick = onClickCapture, + isCapturing = isCapturing, + ) + Image( + painter = painterResource(R.drawable.rorate_button), + contentDescription = null, // 필수 param + modifier = Modifier + .size(48.dp) + .clickable { + onClickRotate() + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload_camera/UploadMissionDisplayBar.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload_camera/UploadMissionDisplayBar.kt new file mode 100644 index 0000000..2bc37a2 --- /dev/null +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload_camera/UploadMissionDisplayBar.kt @@ -0,0 +1,39 @@ +package com.no5ing.bbibbi.presentation.feature.view.main.mission_upload_camera + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +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.res.painterResource +import androidx.compose.ui.unit.dp +import com.no5ing.bbibbi.R +import com.no5ing.bbibbi.presentation.theme.bbibbiScheme +import com.no5ing.bbibbi.presentation.theme.bbibbiTypo + +@Composable +fun UploadMissionDisplayBar( + missionText: String, +) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Image( + painter = painterResource(id = R.drawable.mission_badge), + contentDescription = null, + modifier = Modifier.size(width = 40.dp, height = 18.dp) + ) + Text( + text = missionText, + style = MaterialTheme.bbibbiTypo.bodyTwoBold, + color = MaterialTheme.bbibbiScheme.mainYellow, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload_camera/UploadMissionPreviewBox.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload_camera/UploadMissionPreviewBox.kt new file mode 100644 index 0000000..7249961 --- /dev/null +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload_camera/UploadMissionPreviewBox.kt @@ -0,0 +1,60 @@ +package com.no5ing.bbibbi.presentation.feature.view.main.mission_upload_camera + +import androidx.camera.view.PreviewView +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.ClipOp +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.drawscope.clipPath +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import com.no5ing.bbibbi.R + +@Composable +fun UploadMissionPreviewBox( + viewFactory: () -> PreviewView, + onTapZoom: () -> Unit = {}, +) { + Box { + AndroidView( + { viewFactory() }, + modifier = Modifier + .aspectRatio(1.0f) + .fillMaxWidth() + .clip(RoundedCornerShape(48.dp)), + ) + Box( + modifier = Modifier + .aspectRatio(1.0f) + .fillMaxWidth() + .padding(bottom = 20.dp), + contentAlignment = Alignment.BottomCenter + ) { + Image( + painter = painterResource(id = R.drawable.zoom_button), + contentDescription = null, + modifier = Modifier + .size(43.dp) + .clickable { + onTapZoom() + } + ) + + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload_camera/UploadMissionTopBar.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload_camera/UploadMissionTopBar.kt new file mode 100644 index 0000000..5d10c61 --- /dev/null +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload_camera/UploadMissionTopBar.kt @@ -0,0 +1,16 @@ +package com.no5ing.bbibbi.presentation.feature.view.main.mission_upload_camera + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.no5ing.bbibbi.R +import com.no5ing.bbibbi.presentation.component.ClosableTopBar + +@Composable +fun UploadMissionTopBar( + onDispose: () -> Unit = {}, +) { + ClosableTopBar( + onDispose = onDispose, + title = stringResource(id = R.string.mission_camera_title), + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/NavigationDestination.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/NavigationDestination.kt index c16e796..f10b03c 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/NavigationDestination.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/NavigationDestination.kt @@ -65,6 +65,8 @@ abstract class NavigationDestination( internal const val settingWebViewPageRoute = "setting/webview" internal const val settingQuitPageRoute = "setting/quit" internal const val cameraViewRoute = "common/camera" + internal const val uploadMissionPageRoute = "main/upload-mission" + internal const val uploadMissionPreviewPageRoute = "main/upload-mission-preview" @OptIn(ExperimentalComposeUiApi::class) diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/HomePageController.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/HomePageController.kt index b4c09e6..e972870 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/HomePageController.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/HomePageController.kt @@ -7,13 +7,17 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.res.painterResource import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavBackStackEntry import androidx.navigation.NavHostController +import com.no5ing.bbibbi.R +import com.no5ing.bbibbi.data.model.post.PostType import com.no5ing.bbibbi.data.model.view.MainPageTopBarModel import com.no5ing.bbibbi.data.repository.Arguments import com.no5ing.bbibbi.presentation.component.showSnackBarWithDismiss import com.no5ing.bbibbi.presentation.component.snackBarPick +import com.no5ing.bbibbi.presentation.feature.view.common.GenericPopup import com.no5ing.bbibbi.presentation.feature.view.main.home.HomePage import com.no5ing.bbibbi.presentation.feature.view.main.home.TryPickPopup import com.no5ing.bbibbi.presentation.feature.view_controller.CameraViewPageController.goCameraViewPage @@ -21,13 +25,16 @@ import com.no5ing.bbibbi.presentation.feature.view_controller.NavigationDestinat import com.no5ing.bbibbi.presentation.feature.view_controller.main.CalendarDetailPageController.goCalendarDetailPage import com.no5ing.bbibbi.presentation.feature.view_controller.main.CalendarPageController.goCalendarPage import com.no5ing.bbibbi.presentation.feature.view_controller.main.FamilyListPageController.goFamilyListPage +import com.no5ing.bbibbi.presentation.feature.view_controller.main.MissionUploadPageController.goMissionUploadPage import com.no5ing.bbibbi.presentation.feature.view_controller.main.PostReUploadPageController.goPostReUploadPage import com.no5ing.bbibbi.presentation.feature.view_controller.main.PostUploadPageController.goPostUploadPage import com.no5ing.bbibbi.presentation.feature.view_controller.main.PostViewPageController.goPostViewPage import com.no5ing.bbibbi.presentation.feature.view_controller.main.ProfilePageController.goProfilePage +import com.no5ing.bbibbi.presentation.feature.view_controller.main.UploadMissionPageController.goMissionCameraPage import com.no5ing.bbibbi.presentation.feature.view_model.MainPageViewModel import com.no5ing.bbibbi.presentation.feature.view_model.members.PickMemberViewModel import com.no5ing.bbibbi.util.LocalSnackbarHostState +import com.no5ing.bbibbi.util.gapUntilNext object HomePageController : NavigationDestination( route = mainHomePageRoute, @@ -38,13 +45,29 @@ object HomePageController : NavigationDestination( val pickMemberViewModel = hiltViewModel() val mainPageViewModel = hiltViewModel() var isPickDialogVisible by remember { mutableStateOf(false) } + var isRequireSurvivalDialogVisible by remember { mutableStateOf(false) } + var isTryMissionPictureDialogVisible by remember { mutableStateOf(false) } var tryPickDialogMember by remember { mutableStateOf(null) } val pickState = pickMemberViewModel.uiState.collectAsState() + val postViewTypeState = remember { mutableStateOf(PostType.SURVIVAL) } LaunchedEffect(pickState.value.status) { if (!pickState.value.isIdle()) { mainPageViewModel.invoke(Arguments()) } } + LaunchedEffect(postViewTypeState.value) { + if(postViewTypeState.value == PostType.MISSION) { + //미션 피드 진입 + val mainPageState = mainPageViewModel.uiState.value + if(mainPageState.isReady() + && mainPageState.data.isMissionUnlocked + && mainPageState.data.isMeSurvivalUploadedToday + && !mainPageState.data.isMeMissionUploadedToday) { + if(mainPageViewModel.isMissionPopupShowable()) + isTryMissionPictureDialogVisible = true + } + } + } TryPickPopup( enabledState = isPickDialogVisible, targetNickname = tryPickDialogMember?.displayName ?: "", @@ -67,6 +90,38 @@ object HomePageController : NavigationDestination( isPickDialogVisible = false } ) + GenericPopup( + enabledState = isRequireSurvivalDialogVisible, + title = "생존신고 사진을 먼저 찍으세요!", + description = "미션 사진을 올리려면\n생존신고 사진을 먼저 업로드해야해요.", + image = painterResource(id = R.drawable.mission_require_survival), + confirmText = "생존신고 먼저 하기", + cancelText = "다음에 하기", + onTapConfirm = { + isRequireSurvivalDialogVisible = false + navController.goPostUploadPage() + navController.goCameraViewPage() + }, + onTapCancel = { + isRequireSurvivalDialogVisible = false + } + ) + GenericPopup( + enabledState = isTryMissionPictureDialogVisible, + title = "미션 열쇠 획득!", + description = "열쇠를 획득해 잠금이 해제되었어요.\n미션 사진을 찍을 수 있어요!", + image = painterResource(id = R.drawable.mission_key), + confirmText = "미션 사진 찍기", + cancelText = "닫기", + onTapConfirm = { + isTryMissionPictureDialogVisible = false + navController.goMissionUploadPage() + navController.goMissionCameraPage() + }, + onTapCancel = { + isTryMissionPictureDialogVisible = false + } + ) HomePage( onTapLeft = { navController.goFamilyListPage() @@ -94,7 +149,27 @@ object HomePageController : NavigationDestination( tryPickDialogMember = it isPickDialogVisible = true }, + onTapMissionUpload = { + val uiValue = mainPageViewModel.uiState.value + if (uiValue.isReady() + && uiValue.data.isMissionUnlocked + && !uiValue.data.isMeSurvivalUploadedToday + && !uiValue.data.isMeMissionUploadedToday + ) { + isRequireSurvivalDialogVisible = true + } else if(uiValue.isReady() + && !uiValue.data.isMeMissionUploadedToday + && uiValue.data.isMeSurvivalUploadedToday + && uiValue.data.isMissionUnlocked + && gapUntilNext() > 0 + ) { + //MISSION UPLOAD PAGE + navController.goMissionUploadPage() + navController.goMissionCameraPage() + } + }, mainPageViewModel = mainPageViewModel, + postViewTypeState = postViewTypeState, onTapViewPost = { date -> navController.goCalendarDetailPage(date) } diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/MissionUploadPageController.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/MissionUploadPageController.kt new file mode 100644 index 0000000..d33d02b --- /dev/null +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/MissionUploadPageController.kt @@ -0,0 +1,30 @@ +package com.no5ing.bbibbi.presentation.feature.view_controller.main + +import android.net.Uri +import androidx.compose.runtime.Composable +import androidx.compose.runtime.livedata.observeAsState +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavHostController +import com.no5ing.bbibbi.presentation.feature.view.main.mission_upload.MissionUploadPage +import com.no5ing.bbibbi.presentation.feature.view_controller.NavigationDestination + +object MissionUploadPageController : NavigationDestination( + route = uploadMissionPreviewPageRoute, +) { + @Composable + override fun Render(navController: NavHostController, backStackEntry: NavBackStackEntry) { + val imageCaptureState = backStackEntry.savedStateHandle + .getLiveData("imageUrl") + .observeAsState() + MissionUploadPage( + imageUrl = imageCaptureState, + onDispose = { + navController.popBackStack() + }, + ) + } + + fun NavHostController.goMissionUploadPage() { + navigate(MissionUploadPageController) + } +} diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/UploadMissionPageController.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/UploadMissionPageController.kt new file mode 100644 index 0000000..9057916 --- /dev/null +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/UploadMissionPageController.kt @@ -0,0 +1,31 @@ +package com.no5ing.bbibbi.presentation.feature.view_controller.main + +import androidx.compose.runtime.Composable +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavHostController +import com.no5ing.bbibbi.presentation.feature.view.main.mission_upload_camera.UploadMissionCamera +import com.no5ing.bbibbi.presentation.feature.view_controller.NavigationDestination + +object UploadMissionPageController : NavigationDestination( + route = uploadMissionPageRoute, +) { + @Composable + override fun Render(navController: NavHostController, backStackEntry: NavBackStackEntry) { + UploadMissionCamera( + onDispose = { + navController.popBackStack() + }, + onImageCaptured = { image -> + navController.previousBackStackEntry?.savedStateHandle?.set( + "imageUrl", + image + ) + navController.popBackStack() + } + ) + } + + fun NavHostController.goMissionCameraPage() { + navigate(UploadMissionPageController) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/MainPageViewModel.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/MainPageViewModel.kt index 14b611c..0d93db2 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/MainPageViewModel.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/MainPageViewModel.kt @@ -10,6 +10,7 @@ import com.no5ing.bbibbi.data.repository.Arguments import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow +import java.time.LocalDate import javax.inject.Inject @HiltViewModel @@ -20,6 +21,16 @@ class MainPageViewModel @Inject constructor( var shouldDisplayWidgetPopup = localDataStorage.getAndRemoveWidgetPopupPeriod() val deferredPickMembersSet = MutableStateFlow(setOf()) + fun isMissionPopupShowable(): Boolean { + val today = LocalDate.now() + val lastSeen = localDataStorage.getLastWidgetPopupSeenDate() + if(lastSeen == null || lastSeen.isBefore(today)) { + localDataStorage.setLastWidgetPopupSeenDate(today) + return true + } + return false + } + fun getAndDeleteTemporaryUri(): Uri? { val uri = localDataStorage.getTemporaryUri() if (uri != null) { diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/mission/GetTodayMissionViewModel.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/mission/GetTodayMissionViewModel.kt new file mode 100644 index 0000000..ea2cee9 --- /dev/null +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/mission/GetTodayMissionViewModel.kt @@ -0,0 +1,28 @@ +package com.no5ing.bbibbi.presentation.feature.view_model.mission + +import com.no5ing.bbibbi.data.datasource.network.RestAPI +import com.no5ing.bbibbi.data.model.APIResponse +import com.no5ing.bbibbi.data.model.APIResponse.Companion.wrapToAPIResponse +import com.no5ing.bbibbi.data.model.mission.Mission +import com.no5ing.bbibbi.data.repository.Arguments +import com.no5ing.bbibbi.presentation.feature.view_model.BaseViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import javax.inject.Inject + +@HiltViewModel +class GetTodayMissionViewModel @Inject constructor( + private val restAPI: RestAPI, +) : BaseViewModel>() { + override fun initState(): APIResponse { + return APIResponse.idle() + } + + override fun invoke(arguments: Arguments) { + withMutexScope(Dispatchers.IO) { + val meResult = restAPI.getPostApi().getDailyMission() + val apiResult = meResult.wrapToAPIResponse() + setState(apiResult) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/CreatePostViewModel.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/CreatePostViewModel.kt index d858dd1..54d26ba 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/CreatePostViewModel.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/CreatePostViewModel.kt @@ -67,7 +67,8 @@ class CreatePostViewModel @Inject constructor( imageUrl = imageUploadResult, content = content, uploadTime = getZonedDateTimeString(), - ) + ), + type = arguments.get("type") ).wrapToAPIResponse() setState(postResult) } diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/navigation/graph/MainNavGraph.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/navigation/graph/MainNavGraph.kt index c3be181..186d54d 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/navigation/graph/MainNavGraph.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/navigation/graph/MainNavGraph.kt @@ -15,12 +15,14 @@ import com.no5ing.bbibbi.presentation.feature.view_controller.main.CreateRealEmo import com.no5ing.bbibbi.presentation.feature.view_controller.main.FamilyListPageController import com.no5ing.bbibbi.presentation.feature.view_controller.main.HomePageController import com.no5ing.bbibbi.presentation.feature.view_controller.main.ImagePreviewPageController +import com.no5ing.bbibbi.presentation.feature.view_controller.main.MissionUploadPageController import com.no5ing.bbibbi.presentation.feature.view_controller.main.PostReUploadPageController import com.no5ing.bbibbi.presentation.feature.view_controller.main.PostUploadPageController import com.no5ing.bbibbi.presentation.feature.view_controller.main.PostViewPageController import com.no5ing.bbibbi.presentation.feature.view_controller.main.ProfilePageController import com.no5ing.bbibbi.presentation.feature.view_controller.main.QuitPageController import com.no5ing.bbibbi.presentation.feature.view_controller.main.SettingHomePageController +import com.no5ing.bbibbi.presentation.feature.view_controller.main.UploadMissionPageController import com.no5ing.bbibbi.presentation.feature.view_controller.main.WebViewPageController import com.no5ing.bbibbi.presentation.navigation.animation.fullHorizontalSlideInToLeft import com.no5ing.bbibbi.presentation.navigation.animation.fullHorizontalSlideInToRight @@ -115,6 +117,14 @@ fun NavGraphBuilder.mainGraph( controller = navController, destination = CreateRealEmojiPageController ) + composable( + controller = navController, + destination = UploadMissionPageController + ) + composable( + controller = navController, + destination = MissionUploadPageController + ) composable( controller = navController, destination = ImagePreviewPageController diff --git a/app/src/main/res/drawable/mission_badge.png b/app/src/main/res/drawable/mission_badge.png new file mode 100644 index 0000000000000000000000000000000000000000..e5a77e04869273c7df266fb1546843bd39cfaa5b GIT binary patch literal 2625 zcmV-H3cmG;P)00009a7bBm001mY z001mY0i`{bsQ>@~0drDELIAGL9O(c600d`2O+f$vv5yPBpB({UZKqM+s97(h#QW9^=%Q9zHT$)_&mD-!B`2hr1%jIeb{`sFX zbLOlMOp?pxk{rif@%#N3+wJx_pU;b?&Q zvc0{XW+(q6d;Fe6M;L|aXM;u2*X6G0S35eN&z~a!5*8zx8fvR|r=k;*0}?jV=ZFJNh+{w! zAYn72$)VQpj{y;Ol8`hIguSGr(I{%IKOA?$a1IE^f@6*qu&IAa1bWKH@Z$( zC=`-B&li9ooTb=xLN>*61qi};QhTgK#+rB_;&vYLOuarNF`txn#~4Woh(8k*aok{suNJJ*9Z1bPfvrJG#U-X z>tcmwunI>+cxNvF#aauJK`>3V-hj28eQ2~=a3MMlk&yY5{ub&iQ{`uB>%RhHR@P~OG!hYjP z%GTD_z!y3!UUVY2u}y>@7XspVP)@bc)D__-3%MA2)i4^CkE(<2h1c}o!WjJgdrcso z@yTR4N6NlPT5LOQjMkxtP!9?L?{G z=LIYmp;VI5LM=hjb2m8{mbaf4VD^PUG1PR*K_4@x{%NW?bOC#A?nTIE&#Jz@wzlr7 z2$ckPYD9=GsE1O5Vl)iPdGO61#8?qK9tn5%;kmIks0WshuFZ{R$p&X&=BC4zY`l(=0DE6k znhHE~<|Wnl*eya!6O&tc<5OzA35wY;Ea#}o!-K6oaT^`5W@2XM))W15(es9U`mdL* zt#800ltAGLZDFV|EC)+ASj29FxogJJw0CY)qIMYr=)tA($o3SvMw&h0V8lEwgK$4h}ya^%5vRB^Q*74c+j= z4?hC0U}<7Dy%lb6-iRqz6q z*9lRG^|lZGSB@KgPc+PCSgI#0q)tT4Icj#iJn{lW2SfqlGlpP!lCidyW3JeMrKR(5 z?b>e@&y8bDrKVOZv1RUfk*h*BV!Z1l9yxhP=7Pq5Pbfe$*XLER^?qe#MRgpwxVX>_ z#mmde#b_+q+_-Ve_FAA8wzV3Y`J(+kpBR>hCB+3;NHtqeLAi-FVFzUac}7mrSej%XH4NPGePtawxeh+t?9 zsdSd(l-%XZzl2~=`E?v4sx@9n<431)WaZCs5>VAWF3y=Ru|y5HASNKAB0vO%a`tR? zU_XZ6OP7A3DnJPo8y?pyI1Jxf^T&4L$dXTFId;5YX`PR#9)k&r*(^C4)=p|rfXhSF zFTD^7B!BPA$C8xRs< zzh**$1@uuApaiyGEi4pHv_RfX2b6GBgviB-1GssZghEp}WSB`%wC0FWfR_{YO~>gq ztzu`j-Cz@f+jnTN0>?!d^0nDqZBbQ-Qi391B_{_Hl0Z<|QKSBrD8Qi4zT19aS;MLo zcvf=r=D!Di0N4fYX1LYEm0J>^5MPTQiegd1NP-d$DAyH_1z5CIfL;a#DE&aB2i6t6 zjN?uyA&KS2K@!SBzo3jHD2ef~>N(ZKO_&EuL3WpIToWOpCPcji<+$m=r+6+q1UNT0 zXW1PXo;UEb{4fyB%<8=_f$ghbL}RC5b51b88N@85B%N zdiw@(>_;K^`NlK%N<$H0hqEL^6rfUq(wmSR3$Q=}e9mz_9QT5?Jrv^2!<4vuGr}BC zo}fqxi6lUL5220H9lET*s)WT;B1FVx0?|WQIZ8l?h2Gu-r8gmYHF`t>Gzd=Z*|F49 ztr6BNp1gJGT!`;36NrSK{Z6EWWR%+S2@g7<#F>uB{?m@bL8UIvR(npU+nWf^dP}?05qR!Ueh_6a?WjAP5)eb;8>~5H8RaA;)poX)?Lbwk2FHaZ#;QEcTxi}d^bSAm2`SdXYJ z)R?qG^EOzWMCAjZ&`el^=zzTfL^PXP@!)Vc{0b#2#7RL7Rjbuk4&u=M-uX`MrB$vx2+3d;{w)q;1?LTJEX=brWAWl7<8Mh7g_>?){ jO08DAp2=i%XW_mBBmOo#PRC|>00000NkvXXu0mjfw<*2v literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/mission_key.png b/app/src/main/res/drawable/mission_key.png new file mode 100644 index 0000000000000000000000000000000000000000..43d6b9f8ebf45d752ce447e24443c47f6294b0b2 GIT binary patch literal 37922 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℘{(->Eg|)J1_tL^-_;b%4Q2DBxkqGG6J2h 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/mission_require_survival.png b/app/src/main/res/drawable/mission_require_survival.png new file mode 100644 index 0000000000000000000000000000000000000000..86b4657a29b203aa01079f547fcd453c26cb7014 GIT binary patch literal 45929 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

OixV|?3uW)4o;{`=3mEQ5fohwu>&>u(gX-aQb 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 z%0A-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 H_V51!xi@3= literal 0 HcmV?d00001 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 08c82a1..97744ec 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -34,6 +34,8 @@ 리얼 이모지 표정을 따라해보세요 + 미션 카메라 + 설정 및 권한 %1$s 버전 버전 정보 조회중... From e84a79fe907c33a6fd6b9aafcbe3357d9ad25991 Mon Sep 17 00:00:00 2001 From: ChuYong Date: Thu, 25 Apr 2024 21:58:31 +0900 Subject: [PATCH 13/26] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=EC=8B=9C=20=EB=AF=B8=EC=85=98=20=EB=AA=85=20?= =?UTF-8?q?=EB=85=B8=EC=B6=9C=20[skip-ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../datasource/local/MissionCacheProvider.kt | 40 +++++++++++++++++++ .../bbibbi/data/datasource/network/RestAPI.kt | 4 ++ .../bbibbi/data/model/mission/Mission.kt | 2 +- .../com/no5ing/bbibbi/data/model/post/Post.kt | 2 + .../com/no5ing/bbibbi/di/NetworkModule.kt | 2 + .../feature/state/main/home/HomePageState.kt | 3 ++ .../view/main/post_view/PostViewContent.kt | 35 ++++++++++++++++ .../view/main/post_view/PostViewPage.kt | 12 ++++++ .../mission/GetMissionByIdViewModel.kt | 25 ++++++++++++ 9 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/no5ing/bbibbi/data/datasource/local/MissionCacheProvider.kt create mode 100644 app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/mission/GetMissionByIdViewModel.kt diff --git a/app/src/main/java/com/no5ing/bbibbi/data/datasource/local/MissionCacheProvider.kt b/app/src/main/java/com/no5ing/bbibbi/data/datasource/local/MissionCacheProvider.kt new file mode 100644 index 0000000..17978cc --- /dev/null +++ b/app/src/main/java/com/no5ing/bbibbi/data/datasource/local/MissionCacheProvider.kt @@ -0,0 +1,40 @@ +package com.no5ing.bbibbi.data.datasource.local + +import com.google.common.cache.CacheBuilder +import com.google.common.cache.CacheLoader +import com.no5ing.bbibbi.data.datasource.network.RestAPI +import com.no5ing.bbibbi.data.model.APIResponse.Companion.wrapToAPIResponse +import com.no5ing.bbibbi.data.model.mission.Mission +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class MissionCacheProvider @Inject constructor( + private val restApi: RestAPI, +) { + private val cache = CacheBuilder.newBuilder() + .maximumSize(100) + .expireAfterWrite(10, TimeUnit.MINUTES) + .build(object : CacheLoader() { + override fun load(key: String): Mission { + return runBlocking { + restApi.getPostApi().getMissionById(key).wrapToAPIResponse().data + } + } + }) + + suspend fun getMission(missionId: String): Mission? { + return coroutineScope { + withContext(Dispatchers.IO) { + runCatching { + return@runCatching cache.get(missionId) + }.getOrNull() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt b/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt index 642110d..3c13805 100644 --- a/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt +++ b/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt @@ -274,6 +274,10 @@ interface RestAPI { @GET("v1/missions/today") suspend fun getDailyMission(): ApiResponse + @GET("v1/missions/{missionId}") + suspend fun getMissionById( + @Path("missionId") missionId: String, + ): ApiResponse } /** diff --git a/app/src/main/java/com/no5ing/bbibbi/data/model/mission/Mission.kt b/app/src/main/java/com/no5ing/bbibbi/data/model/mission/Mission.kt index 1d4f081..8410119 100644 --- a/app/src/main/java/com/no5ing/bbibbi/data/model/mission/Mission.kt +++ b/app/src/main/java/com/no5ing/bbibbi/data/model/mission/Mission.kt @@ -8,6 +8,6 @@ import java.time.LocalDate @Parcelize data class Mission( val id: String, - val date: LocalDate, + val date: LocalDate?, val content: String, ) : Parcelable, BaseModel() diff --git a/app/src/main/java/com/no5ing/bbibbi/data/model/post/Post.kt b/app/src/main/java/com/no5ing/bbibbi/data/model/post/Post.kt index e2cb8a7..bca794a 100644 --- a/app/src/main/java/com/no5ing/bbibbi/data/model/post/Post.kt +++ b/app/src/main/java/com/no5ing/bbibbi/data/model/post/Post.kt @@ -9,6 +9,8 @@ import java.time.ZonedDateTime data class Post( val postId: String, val authorId: String, + val type: PostType, + val missionId: String?, val commentCount: Int, val emojiCount: Int, val imageUrl: String, diff --git a/app/src/main/java/com/no5ing/bbibbi/di/NetworkModule.kt b/app/src/main/java/com/no5ing/bbibbi/di/NetworkModule.kt index f225d3e..dc3b04b 100644 --- a/app/src/main/java/com/no5ing/bbibbi/di/NetworkModule.kt +++ b/app/src/main/java/com/no5ing/bbibbi/di/NetworkModule.kt @@ -1,6 +1,7 @@ package com.no5ing.bbibbi.di import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.MapperFeature import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.kotlinModule @@ -162,6 +163,7 @@ object NetworkModule { JacksonConverterFactory.create( jacksonObjectMapper() .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS, true) .registerModule(kotlinModule()) .registerModule(JavaTimeModule()) ) diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/state/main/home/HomePageState.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/state/main/home/HomePageState.kt index 4e80a81..bcb8040 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/state/main/home/HomePageState.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/state/main/home/HomePageState.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import com.no5ing.bbibbi.data.model.member.Member import com.no5ing.bbibbi.data.model.post.Post +import com.no5ing.bbibbi.data.model.post.PostType import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import java.time.ZonedDateTime @@ -27,6 +28,8 @@ fun rememberHomePageState( imageUrl = "https://picsum.photos/300/300?random=01", emojiCount = 0, createdAt = ZonedDateTime.now(), + missionId = null, + type = PostType.SURVIVAL, ) ) }, diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/post_view/PostViewContent.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/post_view/PostViewContent.kt index 8e78eeb..bfc33b7 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/post_view/PostViewContent.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/post_view/PostViewContent.kt @@ -1,6 +1,8 @@ package com.no5ing.bbibbi.presentation.feature.view.main.post_view +import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -9,6 +11,8 @@ import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState @@ -19,10 +23,13 @@ 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.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import coil.compose.AsyncImage +import com.no5ing.bbibbi.R import com.no5ing.bbibbi.data.model.post.Post import com.no5ing.bbibbi.data.repository.Arguments import com.no5ing.bbibbi.presentation.component.MiniTextBubbleBox @@ -32,12 +39,15 @@ import com.no5ing.bbibbi.presentation.feature.view_model.post.AddRealEmojiViewMo import com.no5ing.bbibbi.presentation.feature.view_model.post.MemberRealEmojiListViewModel import com.no5ing.bbibbi.presentation.feature.view_model.post.PostReactionBarViewModel import com.no5ing.bbibbi.presentation.feature.view_model.post.RemovePostReactionViewModel +import com.no5ing.bbibbi.presentation.theme.bbibbiScheme +import com.no5ing.bbibbi.presentation.theme.bbibbiTypo import com.no5ing.bbibbi.util.LocalSessionState import com.no5ing.bbibbi.util.asyncImagePainter @Composable fun PostViewContent( post: Post, + missionText: String? = null, modifier: Modifier = Modifier, onTapRealEmojiCreate: (String) -> Unit, familyPostReactionBarViewModel: PostReactionBarViewModel = hiltViewModel(), @@ -118,6 +128,31 @@ fun PostViewContent( contentScale = ContentScale.Crop ) } + if (missionText != null) { + Box( + modifier = Modifier + .align(Alignment.TopCenter) + .padding(top = 16.dp) + .background(Color.Black.copy(alpha = 0.3f), RoundedCornerShape(26.dp)) + .padding(horizontal = 16.dp, vertical = 8.dp) + + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Image( + painter = painterResource(id = R.drawable.mission_badge), + contentDescription = null, + ) + Text( + text = missionText, + style = MaterialTheme.bbibbiTypo.bodyTwoRegular, + color = MaterialTheme.bbibbiScheme.white, + ) + } + } + } MiniTextBubbleBox( text = post.content, alignment = Alignment.BottomCenter, diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/post_view/PostViewPage.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/post_view/PostViewPage.kt index e10bbfa..811e870 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/post_view/PostViewPage.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/post_view/PostViewPage.kt @@ -46,6 +46,7 @@ import com.no5ing.bbibbi.presentation.component.CircleProfileImage import com.no5ing.bbibbi.presentation.feature.state.post.view.PostViewPageState import com.no5ing.bbibbi.presentation.feature.state.post.view.rememberPostViewPageState import com.no5ing.bbibbi.presentation.feature.uistate.family.MainFeedUiState +import com.no5ing.bbibbi.presentation.feature.view_model.mission.GetMissionByIdViewModel import com.no5ing.bbibbi.presentation.feature.view_model.post.AddPostReactionViewModel import com.no5ing.bbibbi.presentation.feature.view_model.post.FamilyPostViewModel import com.no5ing.bbibbi.presentation.feature.view_model.post.FamilySwipePostsViewModel @@ -72,6 +73,7 @@ fun PostViewPage( familyPostReactionBarViewModel: PostReactionBarViewModel = hiltViewModel(), removePostReactionViewModel: RemovePostReactionViewModel = hiltViewModel(), addPostReactionViewModel: AddPostReactionViewModel = hiltViewModel(), + getMissionByIdViewModel: GetMissionByIdViewModel = hiltViewModel(), postCommentDialogState: MutableState = remember { mutableStateOf(false) }, ) { LaunchedEffect(Unit) { @@ -91,6 +93,7 @@ fun PostViewPage( } ) } + val missionTextState by getMissionByIdViewModel.uiState.collectAsState() LaunchedEffect(postState) { if (postState.isReady()) { val currentPost = postState.data.post @@ -133,6 +136,11 @@ fun PostViewPage( ) ) ) + currentPost.post.missionId?.apply { + getMissionByIdViewModel.invoke(Arguments(resourceId = this)) + } ?: Unit.apply { + getMissionByIdViewModel.resetState() + } } } BBiBBiSurface(modifier = Modifier.fillMaxSize()) { @@ -183,6 +191,7 @@ fun PostViewPage( addPostReactionViewModel = addPostReactionViewModel, postData = postData, postCommentDialogState = postCommentDialogState, + missionText = missionTextState?.content ) } } else { @@ -195,6 +204,7 @@ fun PostViewPage( addPostReactionViewModel = addPostReactionViewModel, postData = postState.data, postCommentDialogState = postCommentDialogState, + missionText = missionTextState?.content ) } @@ -211,6 +221,7 @@ fun PostViewBody( onTapProfile: (Member) -> Unit, onTapRealEmojiCreate: (String) -> Unit, postData: MainFeedUiState, + missionText: String? = null, familyPostReactionBarViewModel: PostReactionBarViewModel, removePostReactionViewModel: RemovePostReactionViewModel, addPostReactionViewModel: AddPostReactionViewModel, @@ -233,6 +244,7 @@ fun PostViewBody( addPostReactionViewModel = addPostReactionViewModel, onTapRealEmojiCreate = onTapRealEmojiCreate, postCommentDialogState = postCommentDialogState, + missionText = missionText, ) } } diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/mission/GetMissionByIdViewModel.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/mission/GetMissionByIdViewModel.kt new file mode 100644 index 0000000..7c78b0e --- /dev/null +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/mission/GetMissionByIdViewModel.kt @@ -0,0 +1,25 @@ +package com.no5ing.bbibbi.presentation.feature.view_model.mission + +import com.no5ing.bbibbi.data.datasource.local.MissionCacheProvider +import com.no5ing.bbibbi.data.model.mission.Mission +import com.no5ing.bbibbi.data.repository.Arguments +import com.no5ing.bbibbi.presentation.feature.view_model.BaseViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import javax.inject.Inject + +@HiltViewModel +class GetMissionByIdViewModel @Inject constructor( + private val missionCacheProvider: MissionCacheProvider, +) : BaseViewModel() { + override fun initState(): Mission? { + return null + } + + override fun invoke(arguments: Arguments) { + val missionId = arguments.resourceId ?: throw RuntimeException() + withMutexScope(Dispatchers.IO) { + setState(missionCacheProvider.getMission(missionId)) + } + } +} \ No newline at end of file From c54e6d129e5351f37f63cde87e2aaecfbf082c0a Mon Sep 17 00:00:00 2001 From: ChuYong Date: Mon, 29 Apr 2024 17:50:59 +0900 Subject: [PATCH 14/26] feat: add missiontext on calendar detail --- .../view/main/calendar/detail/CalendarDetailPage.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/calendar/detail/CalendarDetailPage.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/calendar/detail/CalendarDetailPage.kt index df72635..c3b77c3 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/calendar/detail/CalendarDetailPage.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/calendar/detail/CalendarDetailPage.kt @@ -56,6 +56,7 @@ import com.no5ing.bbibbi.presentation.component.snackBarWarning import com.no5ing.bbibbi.presentation.feature.uistate.family.MainFeedUiState import com.no5ing.bbibbi.presentation.feature.view.main.calendar.MainCalendarDay import com.no5ing.bbibbi.presentation.feature.view.main.post_view.PostViewContent +import com.no5ing.bbibbi.presentation.feature.view_model.mission.GetMissionByIdViewModel import com.no5ing.bbibbi.presentation.feature.view_model.post.AddPostReactionViewModel import com.no5ing.bbibbi.presentation.feature.view_model.post.CalendarWeekViewModel import com.no5ing.bbibbi.presentation.feature.view_model.post.FamilySwipePostsViewModel @@ -93,6 +94,7 @@ fun CalendarDetailPage( addPostReactionViewModel: AddPostReactionViewModel = hiltViewModel(), calendarWeekViewModel: CalendarWeekViewModel = hiltViewModel(), familyPostsViewModel: FamilySwipePostsViewModel = hiltViewModel(), + getMissionByIdViewModel: GetMissionByIdViewModel = hiltViewModel(), ) { // val postState = familyPostViewModel.uiState.collectAsState() val resources = localResources() @@ -199,6 +201,11 @@ fun CalendarDetailPage( if (currentPostState.data.size <= pagerState.currentPage) { return@LaunchedEffect } + currentPostState.data[pagerState.currentPage].post.missionId?.apply { + getMissionByIdViewModel.invoke(Arguments(resourceId = this)) + } ?: Unit.apply { + getMissionByIdViewModel.resetState() + } familyPostReactionBarViewModel.invoke( Arguments( arguments = mapOf( @@ -239,6 +246,7 @@ fun CalendarDetailPage( } val currentYearMonth = currentCalendarState.weekState.currentWeek.yearMonth + val mission by getMissionByIdViewModel.uiState.collectAsState() BBiBBiSurface(modifier = Modifier.fillMaxSize()) { Box { @@ -320,6 +328,7 @@ fun CalendarDetailPage( familyPostReactionBarViewModel = familyPostReactionBarViewModel, removePostReactionViewModel = removePostReactionViewModel, addPostReactionViewModel = addPostReactionViewModel, + missionText = mission?.content ) } @@ -352,6 +361,7 @@ fun CalendarDetailPage( @Composable fun CalendarDetailBody( + missionText: String? = null, onTapProfile: (Member) -> Unit, onTapRealEmojiCreate: (String) -> Unit, item: MainFeedUiState, @@ -372,6 +382,7 @@ fun CalendarDetailBody( removePostReactionViewModel = removePostReactionViewModel, addPostReactionViewModel = addPostReactionViewModel, onTapRealEmojiCreate = onTapRealEmojiCreate, + missionText = missionText, ) } } From f483cff448c4e8532e14cdb1d2c5cae97902c054 Mon Sep 17 00:00:00 2001 From: ChuYong Date: Mon, 29 Apr 2024 17:52:39 +0900 Subject: [PATCH 15/26] feat: update version code [skip ci] --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 921b208..8582075 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -47,7 +47,7 @@ android { minSdk = 26 targetSdk = 34 versionCode = 11011 - versionName = "1.1.4" + versionName = "1.2.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From 590e1b08c39189db9b2927bc59ba2ad655147212 Mon Sep 17 00:00:00 2001 From: ChuYong Date: Thu, 2 May 2024 13:03:54 +0900 Subject: [PATCH 16/26] feat: fix ui height --- .../bbibbi/data/datasource/network/RestAPI.kt | 15 +++++++------ .../data/model/post/DailyCalendarElement.kt | 22 +++++++++++++++++++ .../feature/view/main/home/HomePageContent.kt | 10 +++++---- 3 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/com/no5ing/bbibbi/data/model/post/DailyCalendarElement.kt diff --git a/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt b/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt index 3c13805..6c63de4 100644 --- a/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt +++ b/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt @@ -32,6 +32,7 @@ import com.no5ing.bbibbi.data.model.member.MemberRealEmojiList import com.no5ing.bbibbi.data.model.mission.Mission import com.no5ing.bbibbi.data.model.post.CalendarBanner import com.no5ing.bbibbi.data.model.post.CalendarElement +import com.no5ing.bbibbi.data.model.post.DailyCalendarElement import com.no5ing.bbibbi.data.model.post.Post import com.no5ing.bbibbi.data.model.post.PostComment import com.no5ing.bbibbi.data.model.post.PostReaction @@ -48,6 +49,7 @@ import retrofit2.http.POST import retrofit2.http.PUT import retrofit2.http.Path import retrofit2.http.Query +import java.time.LocalDate interface RestAPI { /** @@ -206,17 +208,11 @@ interface RestAPI { @Body body: CreatePostReactionRequest, ): ApiResponse - @GET("v1/calendar?type=MONTHLY") + @GET("v1/calendar/monthly") suspend fun getMonthlyCalendar( @Query("yearMonth") yearMonth: String, ): ApiResponse> - @GET("v1/calendar?type=WEEKLY") - suspend fun getWeeklyCalendar( - @Query("yearMonth") yearMonth: String, - @Query("week") week: Int, - ): ApiResponse> - @GET("v1/calendar/banner") suspend fun getCalendarBanner( @Query("yearMonth") yearMonth: String, @@ -227,6 +223,11 @@ interface RestAPI { @Query("yearMonth") yearMonth: String, ): ApiResponse + @GET("v1/calendar/daily") + suspend fun getDailyCalendar( + @Query("yearMonthDay") date: LocalDate, + ): ApiResponse + @GET("v1/posts/{postId}/comments") suspend fun getPostComments( @Path("postId") postId: String, diff --git a/app/src/main/java/com/no5ing/bbibbi/data/model/post/DailyCalendarElement.kt b/app/src/main/java/com/no5ing/bbibbi/data/model/post/DailyCalendarElement.kt new file mode 100644 index 0000000..bd97281 --- /dev/null +++ b/app/src/main/java/com/no5ing/bbibbi/data/model/post/DailyCalendarElement.kt @@ -0,0 +1,22 @@ +package com.no5ing.bbibbi.data.model.post + +import android.os.Parcelable +import com.no5ing.bbibbi.data.model.BaseModel +import kotlinx.parcelize.Parcelize +import java.time.LocalDate +import java.time.ZonedDateTime + +@Parcelize +data class DailyCalendarElement( + val postId: String, + val type: PostType, + val date: LocalDate, + val postImgUrl: String, + val postContent: String, + val missionContent: String?, + val authorId: String, + val commentCount: Int, + val emojiCount: Int, + val allFamilyMembersUploaded: Boolean, + val createdAt: ZonedDateTime, +) : Parcelable, BaseModel() diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt index c935a29..31c3bff 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt @@ -257,8 +257,9 @@ fun MissionFeedTab( @Composable fun SurvivalTextDescription(warningState: MutableState) { Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterHorizontally) + modifier = Modifier.fillMaxWidth().height(20.dp), + horizontalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically, ) { Text( text = if (warningState.value == 1) @@ -293,8 +294,9 @@ fun MissionTextDescription( remainingMemberCnt: Int, ) { Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterHorizontally) + modifier = Modifier.fillMaxWidth().height(20.dp), + horizontalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically, ) { val missionWaitingText = buildAnnotatedString { append("가족 중 ") From 79c9d4b3a13e7e8ea5e804a56f39acac7ad31354 Mon Sep 17 00:00:00 2001 From: ChuYong Date: Thu, 2 May 2024 15:19:31 +0900 Subject: [PATCH 17/26] feat: fix calendar fetch [skip ci] --- .../bbibbi/data/datasource/network/RestAPI.kt | 2 +- .../data/model/post/DailyCalendarElement.kt | 14 +++++++++++- .../uistate/post/CalendarFeedUiState.kt | 11 ++++++++++ .../calendar/detail/CalendarDetailPage.kt | 13 +++-------- .../view/main/post_view/PostViewPage.kt | 22 +++++++------------ .../post/FamilySwipePostsViewModel.kt | 15 ++++++------- 6 files changed, 43 insertions(+), 34 deletions(-) create mode 100644 app/src/main/java/com/no5ing/bbibbi/presentation/feature/uistate/post/CalendarFeedUiState.kt diff --git a/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt b/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt index 6c63de4..6e70ec1 100644 --- a/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt +++ b/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt @@ -226,7 +226,7 @@ interface RestAPI { @GET("v1/calendar/daily") suspend fun getDailyCalendar( @Query("yearMonthDay") date: LocalDate, - ): ApiResponse + ): ApiResponse> @GET("v1/posts/{postId}/comments") suspend fun getPostComments( diff --git a/app/src/main/java/com/no5ing/bbibbi/data/model/post/DailyCalendarElement.kt b/app/src/main/java/com/no5ing/bbibbi/data/model/post/DailyCalendarElement.kt index bd97281..5c193f5 100644 --- a/app/src/main/java/com/no5ing/bbibbi/data/model/post/DailyCalendarElement.kt +++ b/app/src/main/java/com/no5ing/bbibbi/data/model/post/DailyCalendarElement.kt @@ -19,4 +19,16 @@ data class DailyCalendarElement( val emojiCount: Int, val allFamilyMembersUploaded: Boolean, val createdAt: ZonedDateTime, -) : Parcelable, BaseModel() +) : Parcelable, BaseModel() { + fun toPost() = Post( + postId = postId, + authorId = authorId, + type = type, + missionId = null, + commentCount = commentCount, + emojiCount = emojiCount, + imageUrl = postImgUrl, + content = postContent, + createdAt = createdAt, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/uistate/post/CalendarFeedUiState.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/uistate/post/CalendarFeedUiState.kt new file mode 100644 index 0000000..5ef2594 --- /dev/null +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/uistate/post/CalendarFeedUiState.kt @@ -0,0 +1,11 @@ +package com.no5ing.bbibbi.presentation.feature.uistate.post + +import com.no5ing.bbibbi.data.model.BaseModel +import com.no5ing.bbibbi.data.model.member.Member +import com.no5ing.bbibbi.data.model.post.DailyCalendarElement +import com.no5ing.bbibbi.data.model.post.Post + +data class CalendarFeedUiState( + val post: DailyCalendarElement, + val writer: Member, +) : BaseModel() diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/calendar/detail/CalendarDetailPage.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/calendar/detail/CalendarDetailPage.kt index c3b77c3..ae9b47c 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/calendar/detail/CalendarDetailPage.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/calendar/detail/CalendarDetailPage.kt @@ -94,7 +94,6 @@ fun CalendarDetailPage( addPostReactionViewModel: AddPostReactionViewModel = hiltViewModel(), calendarWeekViewModel: CalendarWeekViewModel = hiltViewModel(), familyPostsViewModel: FamilySwipePostsViewModel = hiltViewModel(), - getMissionByIdViewModel: GetMissionByIdViewModel = hiltViewModel(), ) { // val postState = familyPostViewModel.uiState.collectAsState() val resources = localResources() @@ -201,11 +200,6 @@ fun CalendarDetailPage( if (currentPostState.data.size <= pagerState.currentPage) { return@LaunchedEffect } - currentPostState.data[pagerState.currentPage].post.missionId?.apply { - getMissionByIdViewModel.invoke(Arguments(resourceId = this)) - } ?: Unit.apply { - getMissionByIdViewModel.resetState() - } familyPostReactionBarViewModel.invoke( Arguments( arguments = mapOf( @@ -246,7 +240,6 @@ fun CalendarDetailPage( } val currentYearMonth = currentCalendarState.weekState.currentWeek.yearMonth - val mission by getMissionByIdViewModel.uiState.collectAsState() BBiBBiSurface(modifier = Modifier.fillMaxSize()) { Box { @@ -260,7 +253,7 @@ fun CalendarDetailPage( model = asyncImagePainter( source = currentPostState.data.getOrNull( pagerState.currentPage - )?.post?.imageUrl + )?.post?.postImgUrl ), contentDescription = null, modifier = Modifier @@ -324,11 +317,11 @@ fun CalendarDetailPage( CalendarDetailBody( onTapProfile = onTapProfile, onTapRealEmojiCreate = onTapRealEmojiCreate, - item = item, + item = MainFeedUiState(item.post.toPost(), item.writer), familyPostReactionBarViewModel = familyPostReactionBarViewModel, removePostReactionViewModel = removePostReactionViewModel, addPostReactionViewModel = addPostReactionViewModel, - missionText = mission?.content + missionText = item.post.missionContent, ) } diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/post_view/PostViewPage.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/post_view/PostViewPage.kt index 811e870..32a1251 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/post_view/PostViewPage.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/post_view/PostViewPage.kt @@ -73,7 +73,6 @@ fun PostViewPage( familyPostReactionBarViewModel: PostReactionBarViewModel = hiltViewModel(), removePostReactionViewModel: RemovePostReactionViewModel = hiltViewModel(), addPostReactionViewModel: AddPostReactionViewModel = hiltViewModel(), - getMissionByIdViewModel: GetMissionByIdViewModel = hiltViewModel(), postCommentDialogState: MutableState = remember { mutableStateOf(false) }, ) { LaunchedEffect(Unit) { @@ -93,7 +92,6 @@ fun PostViewPage( } ) } - val missionTextState by getMissionByIdViewModel.uiState.collectAsState() LaunchedEffect(postState) { if (postState.isReady()) { val currentPost = postState.data.post @@ -125,22 +123,18 @@ fun PostViewPage( } LaunchedEffect(postState, pagerState.currentPage) { if (postState.isReady()) { - val currentPost = - (if (siblingPostState.isReady()) siblingPostState.data.getOrNull(pagerState.currentPage) else postState.data) + val currentPostId = + (if (siblingPostState.isReady()) siblingPostState.data.getOrNull(pagerState.currentPage)?.post?.postId + else postState.data.post.postId) ?: return@LaunchedEffect familyPostReactionBarViewModel.invoke( Arguments( arguments = mapOf( - "postId" to currentPost.post.postId, + "postId" to currentPostId, "memberId" to memberId ) ) ) - currentPost.post.missionId?.apply { - getMissionByIdViewModel.invoke(Arguments(resourceId = this)) - } ?: Unit.apply { - getMissionByIdViewModel.resetState() - } } } BBiBBiSurface(modifier = Modifier.fillMaxSize()) { @@ -157,7 +151,7 @@ fun PostViewPage( model = asyncImagePainter( source = if (postState.isReady()) - if (siblingPostState.isReady()) siblingPostState.data.getOrNull(pagerState.currentPage)?.post?.imageUrl + if (siblingPostState.isReady()) siblingPostState.data.getOrNull(pagerState.currentPage)?.post?.postImgUrl else postState.data.post.imageUrl else null ), @@ -189,9 +183,9 @@ fun PostViewPage( familyPostReactionBarViewModel = familyPostReactionBarViewModel, removePostReactionViewModel = removePostReactionViewModel, addPostReactionViewModel = addPostReactionViewModel, - postData = postData, + postData = MainFeedUiState(postData.post.toPost(), postData.writer), postCommentDialogState = postCommentDialogState, - missionText = missionTextState?.content + missionText = postData.post.missionContent ) } } else { @@ -204,7 +198,7 @@ fun PostViewPage( addPostReactionViewModel = addPostReactionViewModel, postData = postState.data, postCommentDialogState = postCommentDialogState, - missionText = missionTextState?.content + missionText = null ) } diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/FamilySwipePostsViewModel.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/FamilySwipePostsViewModel.kt index 47a55f4..d4a92c8 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/FamilySwipePostsViewModel.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/FamilySwipePostsViewModel.kt @@ -6,35 +6,34 @@ import com.no5ing.bbibbi.data.model.APIResponse import com.no5ing.bbibbi.data.model.member.Member import com.no5ing.bbibbi.data.repository.Arguments import com.no5ing.bbibbi.presentation.feature.uistate.family.MainFeedUiState +import com.no5ing.bbibbi.presentation.feature.uistate.post.CalendarFeedUiState import com.no5ing.bbibbi.presentation.feature.view_model.BaseViewModel import com.skydoves.sandwich.suspendMapSuccess import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import java.time.LocalDate import javax.inject.Inject @HiltViewModel class FamilySwipePostsViewModel @Inject constructor( private val restAPI: RestAPI, private val memberCacheProvider: MemberCacheProvider, -) : BaseViewModel>>() { - override fun initState(): APIResponse> { +) : BaseViewModel>>() { + override fun initState(): APIResponse> { return APIResponse.idle() } override fun invoke(arguments: Arguments) { - val date = arguments.get("date") ?: throw RuntimeException() + val date = arguments.get("date")?.let(LocalDate::parse) ?: throw RuntimeException() withMutexScope(Dispatchers.IO) { - restAPI.getPostApi().getPosts( - page = 1, - size = 50, + restAPI.getPostApi().getDailyCalendar( date = date, - memberId = null, ).suspendMapSuccess { val posts = this.results.map { val member = kotlin.runCatching { memberCacheProvider.getMember(it.authorId) } - MainFeedUiState( + CalendarFeedUiState( writer = member.getOrElse { Member.unknown() }, post = it ) From 5cbc26e12f288984b8d6ed6872f4247f553e1075 Mon Sep 17 00:00:00 2001 From: ChuYong Date: Thu, 2 May 2024 15:25:40 +0900 Subject: [PATCH 18/26] feat: push push --- .../feature/view/main/calendar/detail/CalendarDetailPage.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/calendar/detail/CalendarDetailPage.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/calendar/detail/CalendarDetailPage.kt index ae9b47c..e7101d6 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/calendar/detail/CalendarDetailPage.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/calendar/detail/CalendarDetailPage.kt @@ -95,7 +95,6 @@ fun CalendarDetailPage( calendarWeekViewModel: CalendarWeekViewModel = hiltViewModel(), familyPostsViewModel: FamilySwipePostsViewModel = hiltViewModel(), ) { - // val postState = familyPostViewModel.uiState.collectAsState() val resources = localResources() val snackBarState = LocalSnackbarHostState.current val uiState = calendarWeekViewModel.uiState.collectAsState() From a7b5fc44fdb2b5d26932657bbb08c6c7da92ae09 Mon Sep 17 00:00:00 2001 From: ChuYong Date: Thu, 2 May 2024 15:32:02 +0900 Subject: [PATCH 19/26] feat: add platforms --- Gemfile.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/Gemfile.lock b/Gemfile.lock index a9a83b7..5723e77 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -217,6 +217,7 @@ GEM xcpretty (~> 0.2, >= 0.0.7) PLATFORMS + arm64-darwin-22 arm64-darwin-23 x86_64-darwin-19 From 3d1570fabeddf0c2daec7de2a38d1314da150b95 Mon Sep 17 00:00:00 2001 From: ChuYong Date: Thu, 2 May 2024 15:34:03 +0900 Subject: [PATCH 20/26] =?UTF-8?q?style:=20=EC=BD=94=EB=93=9C=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bbibbi/presentation/component/Grid.kt | 4 +-- .../component/button/FlatButton.kt | 2 +- .../uistate/post/CalendarFeedUiState.kt | 1 - .../feature/view/common/CustomAlertDialog.kt | 1 - .../feature/view/common/GenericPopup.kt | 11 ------ .../view/common/PostTypeSwitchButton.kt | 17 +++++----- .../calendar/detail/CalendarDetailPage.kt | 6 ++-- .../feature/view/main/home/HomePage.kt | 4 +-- .../feature/view/main/home/HomePageContent.kt | 24 ++++++++----- .../view/main/home/HomePageFeedElement.kt | 1 - .../view/main/home/HomePageStoryBar.kt | 12 ++----- .../view/main/home/HomePageUploadButton.kt | 10 ++---- .../view/main/home/NightHomePageContent.kt | 34 +++++++++++-------- .../feature/view/main/home/TryPickPopup.kt | 6 ---- .../feature/view/main/home/TryWidgetPopup.kt | 1 - .../view/main/home/UploadCountDownBar.kt | 7 ---- .../main/mission_upload/MissionUploadPage.kt | 2 +- .../UploadMissionCamera.kt | 2 +- .../UploadMissionDisplayBar.kt | 2 +- .../UploadMissionPreviewBox.kt | 7 ---- .../view/main/post_view/PostViewPage.kt | 13 ++++--- .../view/main/profile/ProfilePageContent.kt | 6 ++-- .../view/main/setting_home/SettingHomePage.kt | 3 +- .../main/HomePageController.kt | 17 +++++----- .../feature/view_model/MainPageViewModel.kt | 2 +- .../post/FamilySwipePostsViewModel.kt | 1 - .../view_model/post/PostCommentViewModel.kt | 22 ++++++------ .../java/com/no5ing/bbibbi/util/DateParser.kt | 4 +-- 28 files changed, 98 insertions(+), 124 deletions(-) diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/component/Grid.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/component/Grid.kt index eeffe52..454f647 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/component/Grid.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/component/Grid.kt @@ -44,8 +44,8 @@ fun VerticalGrid( val column = index % columns val row = index / columns placeable.placeRelative( - x = column * itemWidth + if(column > 0) gap else 0, - y = columnY[column] + if(row > 0) verticalGap else 0, + x = column * itemWidth + if (column > 0) gap else 0, + y = columnY[column] + if (row > 0) verticalGap else 0, ) columnY[column] += placeable.height } diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/component/button/FlatButton.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/component/button/FlatButton.kt index 7af3b16..8525833 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/component/button/FlatButton.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/component/button/FlatButton.kt @@ -23,7 +23,7 @@ fun FlatButton( modifier: Modifier = Modifier, buttonColor: Color = MaterialTheme.bbibbiScheme.mainYellow, textColor: Color = MaterialTheme.bbibbiScheme.backgroundPrimary, - contentPadding: PaddingValues = PaddingValues( + contentPadding: PaddingValues = PaddingValues( start = 24.dp, top = 12.dp, end = 24.dp, diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/uistate/post/CalendarFeedUiState.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/uistate/post/CalendarFeedUiState.kt index 5ef2594..12b86ab 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/uistate/post/CalendarFeedUiState.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/uistate/post/CalendarFeedUiState.kt @@ -3,7 +3,6 @@ package com.no5ing.bbibbi.presentation.feature.uistate.post import com.no5ing.bbibbi.data.model.BaseModel import com.no5ing.bbibbi.data.model.member.Member import com.no5ing.bbibbi.data.model.post.DailyCalendarElement -import com.no5ing.bbibbi.data.model.post.Post data class CalendarFeedUiState( val post: DailyCalendarElement, diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/common/CustomAlertDialog.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/common/CustomAlertDialog.kt index bf9c200..83fd55a 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/common/CustomAlertDialog.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/common/CustomAlertDialog.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialogDefaults import androidx.compose.material3.BasicAlertDialog import androidx.compose.material3.Button diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/common/GenericPopup.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/common/GenericPopup.kt index e8d1c1b..00e914e 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/common/GenericPopup.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/common/GenericPopup.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialogDefaults import androidx.compose.material3.BasicAlertDialog import androidx.compose.material3.Button @@ -17,23 +16,13 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties -import com.no5ing.bbibbi.R -import com.no5ing.bbibbi.presentation.feature.view.common.AlertDialogContent -import com.no5ing.bbibbi.presentation.feature.view.common.AlertDialogFlowRow -import com.no5ing.bbibbi.presentation.feature.view.common.ButtonsCrossAxisSpacing -import com.no5ing.bbibbi.presentation.feature.view.common.ButtonsMainAxisSpacing import com.no5ing.bbibbi.presentation.theme.bbibbiScheme import com.no5ing.bbibbi.presentation.theme.bbibbiTypo diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/common/PostTypeSwitchButton.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/common/PostTypeSwitchButton.kt index 37c25bf..3d260e1 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/common/PostTypeSwitchButton.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/common/PostTypeSwitchButton.kt @@ -47,19 +47,20 @@ fun PostTypeSwitchButton( ) { val isSurvival = state.value == PostType.SURVIVAL val widthMax = 138.dp.dpToPx() - val buttonPosition: Dp by animateDpAsState(targetValue = - if(isSurvival) 0.dp else 69.dp, animationSpec = tween( - durationMillis = 130, - easing = LinearEasing, - ), + val buttonPosition: Dp by animateDpAsState( + targetValue = + if (isSurvival) 0.dp else 69.dp, animationSpec = tween( + durationMillis = 130, + easing = LinearEasing, + ), label = "" ) val survivalButtonColor: Color by animateColorAsState( - targetValue =if(isSurvival) MaterialTheme.bbibbiScheme.backgroundPrimary else MaterialTheme.bbibbiScheme.gray500, + targetValue = if (isSurvival) MaterialTheme.bbibbiScheme.backgroundPrimary else MaterialTheme.bbibbiScheme.gray500, label = "", ) val missionButtonColor: Color by animateColorAsState( - targetValue = if(isSurvival) MaterialTheme.bbibbiScheme.gray500 else MaterialTheme.bbibbiScheme.backgroundPrimary, + targetValue = if (isSurvival) MaterialTheme.bbibbiScheme.gray500 else MaterialTheme.bbibbiScheme.backgroundPrimary, label = "", ) Box( @@ -77,7 +78,7 @@ fun PostTypeSwitchButton( Timber.d("offset: $offset") } } - //.padding(vertical = 8.dp, horizontal = 12.dp) + //.padding(vertical = 8.dp, horizontal = 12.dp) ) { Box( modifier = Modifier diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/calendar/detail/CalendarDetailPage.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/calendar/detail/CalendarDetailPage.kt index e7101d6..d604182 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/calendar/detail/CalendarDetailPage.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/calendar/detail/CalendarDetailPage.kt @@ -56,7 +56,6 @@ import com.no5ing.bbibbi.presentation.component.snackBarWarning import com.no5ing.bbibbi.presentation.feature.uistate.family.MainFeedUiState import com.no5ing.bbibbi.presentation.feature.view.main.calendar.MainCalendarDay import com.no5ing.bbibbi.presentation.feature.view.main.post_view.PostViewContent -import com.no5ing.bbibbi.presentation.feature.view_model.mission.GetMissionByIdViewModel import com.no5ing.bbibbi.presentation.feature.view_model.post.AddPostReactionViewModel import com.no5ing.bbibbi.presentation.feature.view_model.post.CalendarWeekViewModel import com.no5ing.bbibbi.presentation.feature.view_model.post.FamilySwipePostsViewModel @@ -316,7 +315,10 @@ fun CalendarDetailPage( CalendarDetailBody( onTapProfile = onTapProfile, onTapRealEmojiCreate = onTapRealEmojiCreate, - item = MainFeedUiState(item.post.toPost(), item.writer), + item = MainFeedUiState( + item.post.toPost(), + item.writer + ), familyPostReactionBarViewModel = familyPostReactionBarViewModel, removePostReactionViewModel = removePostReactionViewModel, addPostReactionViewModel = addPostReactionViewModel, diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePage.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePage.kt index 3f47a7d..16092a5 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePage.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePage.kt @@ -77,7 +77,7 @@ fun HomePage( unsavedDialogUri.value = tempUri unsavedDialogEnabled.value = true } - if(isDayTime) { + if (isDayTime) { mainPageViewModel.invoke(Arguments()) } else { mainPageNightViewModel.invoke(Arguments()) @@ -135,7 +135,7 @@ fun HomePage( isUploadAbleTime = remember { gapUntilNext() > 0 }, isAlreadyUploaded = !mainPageState.value.isReady() || mainPageState.value.data.isMeSurvivalUploadedToday, - pickers = if(mainPageState.value.isReady()) mainPageState.value.data.pickers + pickers = if (mainPageState.value.isReady()) mainPageState.value.data.pickers else emptyList(), ) } else { diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt index 31c3bff..d8178d0 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt @@ -72,7 +72,7 @@ fun HomePageContent( val survivalFeedItems = if (mainPageModel.isReady()) mainPageModel.data.survivalFeeds - else emptyList() + else emptyList() val missionFeedItems = if (mainPageModel.isReady()) mainPageModel.data.missionFeeds else emptyList() @@ -124,14 +124,14 @@ fun HomePageContent( ) Spacer(modifier = Modifier.height(24.dp)) UploadCountDownBar(warningState = warningState) - if(postViewTypeState.value == PostType.SURVIVAL) { + if (postViewTypeState.value == PostType.SURVIVAL) { SurvivalTextDescription(warningState = warningState) } else { MissionTextDescription( warningState = warningState, isMissionUnlocked = mainPageModel.isReady() && mainPageModel.data.isMissionUnlocked, - missionText = if(mainPageModel.isReady()) mainPageModel.data.dailyMissionContent else "", - remainingMemberCnt = if(mainPageModel.isReady()) mainPageModel.data.leftUploadCountUntilMissionUnlock else 0 + missionText = if (mainPageModel.isReady()) mainPageModel.data.dailyMissionContent else "", + remainingMemberCnt = if (mainPageModel.isReady()) mainPageModel.data.leftUploadCountUntilMissionUnlock else 0 ) } @@ -227,8 +227,10 @@ fun MissionFeedTab( verticalArrangement = Arrangement.Center, ) { Image( - painter = painterResource(if(isMissionUnlocked) R.drawable.bbibbi - else R.drawable.chest_bibbi), + painter = painterResource( + if (isMissionUnlocked) R.drawable.bbibbi + else R.drawable.chest_bibbi + ), contentDescription = null, // 필수 param modifier = Modifier .fillMaxWidth(), @@ -257,7 +259,9 @@ fun MissionFeedTab( @Composable fun SurvivalTextDescription(warningState: MutableState) { Row( - modifier = Modifier.fillMaxWidth().height(20.dp), + modifier = Modifier + .fillMaxWidth() + .height(20.dp), horizontalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterHorizontally), verticalAlignment = Alignment.CenterVertically, ) { @@ -294,7 +298,9 @@ fun MissionTextDescription( remainingMemberCnt: Int, ) { Row( - modifier = Modifier.fillMaxWidth().height(20.dp), + modifier = Modifier + .fillMaxWidth() + .height(20.dp), horizontalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterHorizontally), verticalAlignment = Alignment.CenterVertically, ) { @@ -305,7 +311,7 @@ fun MissionTextDescription( } append("만 더 올리면 미션 열쇠를 받아요!") } - if(isMissionUnlocked) { + if (isMissionUnlocked) { Text( text = missionText, color = MaterialTheme.bbibbiScheme.textSecondary, diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageFeedElement.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageFeedElement.kt index 35850c8..0941448 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageFeedElement.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageFeedElement.kt @@ -26,7 +26,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.no5ing.bbibbi.R -import com.no5ing.bbibbi.presentation.component.MicroTextBubbleBox import com.no5ing.bbibbi.presentation.theme.bbibbiScheme import com.no5ing.bbibbi.presentation.theme.bbibbiTypo import com.no5ing.bbibbi.util.asyncImagePainter diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageStoryBar.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageStoryBar.kt index e3db64a..04cd319 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageStoryBar.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageStoryBar.kt @@ -20,7 +20,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -30,32 +29,27 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.no5ing.bbibbi.R -import com.no5ing.bbibbi.data.model.APIResponse -import com.no5ing.bbibbi.data.model.member.Member -import com.no5ing.bbibbi.data.model.view.MainPageModel import com.no5ing.bbibbi.data.model.view.MainPageTopBarModel import com.no5ing.bbibbi.presentation.component.CircleProfileImage -import com.no5ing.bbibbi.presentation.feature.uistate.family.MainFeedStoryElementUiState import com.no5ing.bbibbi.presentation.theme.bbibbiScheme import com.no5ing.bbibbi.presentation.theme.bbibbiTypo import com.no5ing.bbibbi.util.LocalSessionState -import com.no5ing.bbibbi.util.gapBetweenNow import com.no5ing.bbibbi.util.gapUntilNext import kotlinx.coroutines.flow.StateFlow @Composable fun HomePageStoryBar( //mainPageState: StateFlow>, - items: List, + items: List, deferredPickStateSet: StateFlow>, onTapProfile: (String) -> Unit = {}, onTapPick: (MainPageTopBarModel) -> Unit = {}, onTapInvite: () -> Unit = {}, ) { val meId = LocalSessionState.current.memberId - // val mainPageModel by mainPageState.collectAsState() + // val mainPageModel by mainPageState.collectAsState() val deferredPickSet = deferredPickStateSet.collectAsState() - // val items = if (mainPageModel.isReady()) mainPageModel.data.topBarElements else emptyList() + // val items = if (mainPageModel.isReady()) mainPageModel.data.topBarElements else emptyList() if (items.size == 1) { HomePageNoFamilyBar( diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageUploadButton.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageUploadButton.kt index b5ed452..0102152 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageUploadButton.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageUploadButton.kt @@ -5,7 +5,6 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.Canvas import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -30,7 +29,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Outline import androidx.compose.ui.graphics.Paint import androidx.compose.ui.graphics.Path @@ -40,13 +38,11 @@ import androidx.compose.ui.graphics.drawscope.drawIntoCanvas import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil.compose.AsyncImage import com.no5ing.bbibbi.R import com.no5ing.bbibbi.data.model.view.MainPagePickerModel -import com.no5ing.bbibbi.presentation.component.CircleProfileImage import com.no5ing.bbibbi.presentation.component.button.CameraCaptureButton import com.no5ing.bbibbi.presentation.theme.bbibbiScheme import com.no5ing.bbibbi.presentation.theme.bbibbiTypo @@ -72,7 +68,7 @@ fun BoxScope.HomePageSurvivalUploadButton( exit = fadeOut(), ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { - if(pickers.isNotEmpty() && isUploadAbleTime && !isAlreadyUploaded && !isLoading) { + if (pickers.isNotEmpty() && isUploadAbleTime && !isAlreadyUploaded && !isLoading) { WaitingMembersPop( pickers = pickers, ) @@ -119,7 +115,7 @@ fun BoxScope.HomePageMissionUploadButton( Column(horizontalAlignment = Alignment.CenterHorizontally) { UploadHelperPop( text = - if(!isMeUploadedToday) + if (!isMeUploadedToday) "생존신고 후 미션 사진을 올릴 수 있어요" else if (!isMissionUnlocked) "아직 미션 사진을 찍을 수 없어요" @@ -228,7 +224,7 @@ fun WaitingMembersPop( } Spacer(modifier = Modifier.width(16.dp.times(pickersShattered.size - 1))) } - if(pickers.size == 1) { + if (pickers.size == 1) { Text( text = "${pickers.first().displayName}님이 기다리고 있어요", style = MaterialTheme.bbibbiTypo.bodyTwoRegular, diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/NightHomePageContent.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/NightHomePageContent.kt index 25a387c..6429f2a 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/NightHomePageContent.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/NightHomePageContent.kt @@ -151,7 +151,7 @@ fun NightHomePageContent( UploadCountDownBar(warningState = warningState) SurvivalTextDescription(warningState = warningState) Spacer(modifier = Modifier.height(24.dp)) - if(mainPageModel.isReady()) { + if (mainPageModel.isReady()) { val ranking = mainPageModel.data.familyMemberMonthlyRanking Box( modifier = Modifier @@ -161,7 +161,7 @@ fun NightHomePageContent( MaterialTheme.bbibbiScheme.backgroundSecondary, RoundedCornerShape(24.dp) ) - ){ + ) { Column( modifier = Modifier.padding(vertical = 20.dp, horizontal = 20.dp), horizontalAlignment = Alignment.CenterHorizontally, @@ -218,7 +218,7 @@ fun NightHomePageContent( horizontalArrangement = Arrangement.SpaceBetween, ) { Box( - Modifier.padding(top = 36.dp) + Modifier.padding(top = 36.dp) ) { RankIcon( model = ranking.secondRanker, @@ -255,7 +255,7 @@ fun NightHomePageContent( } } Spacer(modifier = Modifier.height(36.dp)) - if(ranking.isAllRankersNull()) { + if (ranking.isAllRankersNull()) { Text( text = "아직 활동한 가족이 없어요", style = MaterialTheme.bbibbiTypo.bodyOneBold, @@ -295,7 +295,7 @@ fun RankIcon( rankImage: Painter, badgeHeight: Dp, ) { - if(model == null) { + if (model == null) { Column( verticalArrangement = Arrangement.spacedBy(9.dp), horizontalAlignment = Alignment.CenterHorizontally, @@ -312,10 +312,12 @@ fun RankIcon( .background(MaterialTheme.bbibbiScheme.backgroundHover, CircleShape), contentAlignment = Alignment.Center, ) { - Text(text = "?", style = TextStyle( - fontSize = 33.75.sp, - fontWeight = FontWeight.SemiBold - ), color = MaterialTheme.colorScheme.onSurface) + Text( + text = "?", style = TextStyle( + fontSize = 33.75.sp, + fontWeight = FontWeight.SemiBold + ), color = MaterialTheme.colorScheme.onSurface + ) } Image( painter = noRankImage, @@ -369,13 +371,15 @@ fun CommonEmptyBlock() { verticalArrangement = Arrangement.spacedBy(4.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { - Box(modifier = Modifier - .size(width = 53.dp, height = 16.dp) - .background(MaterialTheme.bbibbiScheme.gray600, RoundedCornerShape(4.dp)) + Box( + modifier = Modifier + .size(width = 53.dp, height = 16.dp) + .background(MaterialTheme.bbibbiScheme.gray600, RoundedCornerShape(4.dp)) ) - Box(modifier = Modifier - .size(width = 29.dp, height = 12.dp) - .background(MaterialTheme.bbibbiScheme.button, RoundedCornerShape(4.dp)) + Box( + modifier = Modifier + .size(width = 29.dp, height = 12.dp) + .background(MaterialTheme.bbibbiScheme.button, RoundedCornerShape(4.dp)) ) } } diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/TryPickPopup.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/TryPickPopup.kt index 2e06260..a2c4c0b 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/TryPickPopup.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/TryPickPopup.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialogDefaults import androidx.compose.material3.BasicAlertDialog import androidx.compose.material3.Button @@ -17,21 +16,16 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties import com.no5ing.bbibbi.R import com.no5ing.bbibbi.presentation.feature.view.common.AlertDialogContent import com.no5ing.bbibbi.presentation.feature.view.common.AlertDialogFlowRow -import com.no5ing.bbibbi.presentation.feature.view.common.ButtonsCrossAxisSpacing import com.no5ing.bbibbi.presentation.feature.view.common.ButtonsMainAxisSpacing import com.no5ing.bbibbi.presentation.theme.bbibbiScheme import com.no5ing.bbibbi.presentation.theme.bbibbiTypo diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/TryWidgetPopup.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/TryWidgetPopup.kt index e9b8bab..36dbf5b 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/TryWidgetPopup.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/TryWidgetPopup.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialogDefaults import androidx.compose.material3.BasicAlertDialog import androidx.compose.material3.Button diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/UploadCountDownBar.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/UploadCountDownBar.kt index 04c9263..830d70d 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/UploadCountDownBar.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/UploadCountDownBar.kt @@ -1,13 +1,9 @@ package com.no5ing.bbibbi.presentation.feature.view.main.home -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement 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.size import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -18,10 +14,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import com.no5ing.bbibbi.R import com.no5ing.bbibbi.presentation.theme.bbibbiScheme import com.no5ing.bbibbi.presentation.theme.bbibbiTypo import com.no5ing.bbibbi.util.gapUntilNext diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload/MissionUploadPage.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload/MissionUploadPage.kt index 8989ee1..a1635b9 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload/MissionUploadPage.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload/MissionUploadPage.kt @@ -114,7 +114,7 @@ fun MissionUploadPage( ) Spacer(modifier = Modifier.height(48.dp)) UploadMissionDisplayBar( - missionText = if(missionModel.isReady()) missionModel.data.content else "" + missionText = if (missionModel.isReady()) missionModel.data.content else "" ) Spacer(modifier = Modifier.height(16.dp)) PostUploadPageImagePreview( diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload_camera/UploadMissionCamera.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload_camera/UploadMissionCamera.kt index a690a6e..1812071 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload_camera/UploadMissionCamera.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload_camera/UploadMissionCamera.kt @@ -196,7 +196,7 @@ fun UploadMissionCamera( ) Spacer(modifier = Modifier.height(48.dp)) UploadMissionDisplayBar( - missionText = if(missionModel.isReady()) missionModel.data.content else "" + missionText = if (missionModel.isReady()) missionModel.data.content else "" ) Spacer(modifier = Modifier.height(16.dp)) UploadMissionPreviewBox( diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload_camera/UploadMissionDisplayBar.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload_camera/UploadMissionDisplayBar.kt index 2bc37a2..04fee99 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload_camera/UploadMissionDisplayBar.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload_camera/UploadMissionDisplayBar.kt @@ -34,6 +34,6 @@ fun UploadMissionDisplayBar( text = missionText, style = MaterialTheme.bbibbiTypo.bodyTwoBold, color = MaterialTheme.bbibbiScheme.mainYellow, - ) + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload_camera/UploadMissionPreviewBox.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload_camera/UploadMissionPreviewBox.kt index 7249961..1e13a8c 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload_camera/UploadMissionPreviewBox.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload_camera/UploadMissionPreviewBox.kt @@ -1,7 +1,6 @@ package com.no5ing.bbibbi.presentation.feature.view.main.mission_upload_camera import androidx.camera.view.PreviewView -import androidx.compose.foundation.Canvas import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box @@ -14,12 +13,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.graphics.ClipOp -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Path -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.graphics.drawscope.clipPath import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/post_view/PostViewPage.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/post_view/PostViewPage.kt index 32a1251..55abde2 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/post_view/PostViewPage.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/post_view/PostViewPage.kt @@ -46,7 +46,6 @@ import com.no5ing.bbibbi.presentation.component.CircleProfileImage import com.no5ing.bbibbi.presentation.feature.state.post.view.PostViewPageState import com.no5ing.bbibbi.presentation.feature.state.post.view.rememberPostViewPageState import com.no5ing.bbibbi.presentation.feature.uistate.family.MainFeedUiState -import com.no5ing.bbibbi.presentation.feature.view_model.mission.GetMissionByIdViewModel import com.no5ing.bbibbi.presentation.feature.view_model.post.AddPostReactionViewModel import com.no5ing.bbibbi.presentation.feature.view_model.post.FamilyPostViewModel import com.no5ing.bbibbi.presentation.feature.view_model.post.FamilySwipePostsViewModel @@ -151,7 +150,9 @@ fun PostViewPage( model = asyncImagePainter( source = if (postState.isReady()) - if (siblingPostState.isReady()) siblingPostState.data.getOrNull(pagerState.currentPage)?.post?.postImgUrl + if (siblingPostState.isReady()) siblingPostState.data.getOrNull( + pagerState.currentPage + )?.post?.postImgUrl else postState.data.post.imageUrl else null ), @@ -175,7 +176,8 @@ fun PostViewPage( if (postState.isReady()) { if (isPagerReady) { HorizontalPager(state = pagerState) { page -> - val postData = siblingPostState.data.getOrNull(page) ?: return@HorizontalPager + val postData = + siblingPostState.data.getOrNull(page) ?: return@HorizontalPager PostViewBody( onDispose = onDispose, onTapProfile = onTapProfile, @@ -183,7 +185,10 @@ fun PostViewPage( familyPostReactionBarViewModel = familyPostReactionBarViewModel, removePostReactionViewModel = removePostReactionViewModel, addPostReactionViewModel = addPostReactionViewModel, - postData = MainFeedUiState(postData.post.toPost(), postData.writer), + postData = MainFeedUiState( + postData.post.toPost(), + postData.writer + ), postCommentDialogState = postCommentDialogState, missionText = postData.post.missionContent ) diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/profile/ProfilePageContent.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/profile/ProfilePageContent.kt index a5bcd9b..eebca0b 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/profile/ProfilePageContent.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/profile/ProfilePageContent.kt @@ -89,13 +89,14 @@ fun ProfilePageContent( state = pagerState, verticalAlignment = Alignment.Top, ) { - when(it) { + when (it) { 0 -> { SurvivalProfilePageFeed( postItemsState = postItemsState, onTapContent = onTapContent ) } + 1 -> { MissionProfilePageFeed( postItemsState = missionItemState, @@ -103,7 +104,7 @@ fun ProfilePageContent( ) } } - + } } @@ -245,6 +246,7 @@ fun MissionProfilePageFeed( ) } } + @Composable fun ProfilePageContentItem( imageUrl: String, diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/setting_home/SettingHomePage.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/setting_home/SettingHomePage.kt index eb55820..25c36d4 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/setting_home/SettingHomePage.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/setting_home/SettingHomePage.kt @@ -20,7 +20,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.core.content.ContextCompat.startActivity import androidx.hilt.navigation.compose.hiltViewModel import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted @@ -151,7 +150,7 @@ fun SettingHomePage( ) }, onTapFormBanner = { - context.openBrowser("https://docs.google.com/forms/d/e/1FAIpQLSeeIRAn45EBU4otZ5y2X4QPA9pCzU1Vw6IaDFF7czSrpgAeRg/viewform") + context.openBrowser("https://docs.google.com/forms/d/e/1FAIpQLSeeIRAn45EBU4otZ5y2X4QPA9pCzU1Vw6IaDFF7czSrpgAeRg/viewform") }, onTapMarketOpen = { context.openMarket() diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/HomePageController.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/HomePageController.kt index e972870..44de6b5 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/HomePageController.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/HomePageController.kt @@ -56,14 +56,15 @@ object HomePageController : NavigationDestination( } } LaunchedEffect(postViewTypeState.value) { - if(postViewTypeState.value == PostType.MISSION) { + if (postViewTypeState.value == PostType.MISSION) { //미션 피드 진입 val mainPageState = mainPageViewModel.uiState.value - if(mainPageState.isReady() + if (mainPageState.isReady() && mainPageState.data.isMissionUnlocked && mainPageState.data.isMeSurvivalUploadedToday - && !mainPageState.data.isMeMissionUploadedToday) { - if(mainPageViewModel.isMissionPopupShowable()) + && !mainPageState.data.isMeMissionUploadedToday + ) { + if (mainPageViewModel.isMissionPopupShowable()) isTryMissionPictureDialogVisible = true } } @@ -75,7 +76,7 @@ object HomePageController : NavigationDestination( isPickDialogVisible = false mainPageViewModel.addPickMembersSet(tryPickDialogMember?.memberId ?: "") snackBarHost.showSnackBarWithDismiss( - message = "${tryPickDialogMember?.displayName?:""}님에게 생존신고 알림을 보냈어요", + message = "${tryPickDialogMember?.displayName ?: ""}님에게 생존신고 알림을 보냈어요", actionLabel = snackBarPick, ) pickMemberViewModel.invoke( @@ -155,14 +156,14 @@ object HomePageController : NavigationDestination( && uiValue.data.isMissionUnlocked && !uiValue.data.isMeSurvivalUploadedToday && !uiValue.data.isMeMissionUploadedToday - ) { + ) { isRequireSurvivalDialogVisible = true - } else if(uiValue.isReady() + } else if (uiValue.isReady() && !uiValue.data.isMeMissionUploadedToday && uiValue.data.isMeSurvivalUploadedToday && uiValue.data.isMissionUnlocked && gapUntilNext() > 0 - ) { + ) { //MISSION UPLOAD PAGE navController.goMissionUploadPage() navController.goMissionCameraPage() diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/MainPageViewModel.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/MainPageViewModel.kt index 0d93db2..4f85201 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/MainPageViewModel.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/MainPageViewModel.kt @@ -24,7 +24,7 @@ class MainPageViewModel @Inject constructor( fun isMissionPopupShowable(): Boolean { val today = LocalDate.now() val lastSeen = localDataStorage.getLastWidgetPopupSeenDate() - if(lastSeen == null || lastSeen.isBefore(today)) { + if (lastSeen == null || lastSeen.isBefore(today)) { localDataStorage.setLastWidgetPopupSeenDate(today) return true } diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/FamilySwipePostsViewModel.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/FamilySwipePostsViewModel.kt index d4a92c8..1c9dd7f 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/FamilySwipePostsViewModel.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/FamilySwipePostsViewModel.kt @@ -5,7 +5,6 @@ import com.no5ing.bbibbi.data.datasource.network.RestAPI import com.no5ing.bbibbi.data.model.APIResponse import com.no5ing.bbibbi.data.model.member.Member import com.no5ing.bbibbi.data.repository.Arguments -import com.no5ing.bbibbi.presentation.feature.uistate.family.MainFeedUiState import com.no5ing.bbibbi.presentation.feature.uistate.post.CalendarFeedUiState import com.no5ing.bbibbi.presentation.feature.view_model.BaseViewModel import com.skydoves.sandwich.suspendMapSuccess diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/PostCommentViewModel.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/PostCommentViewModel.kt index 55ad231..ec8796b 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/PostCommentViewModel.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/PostCommentViewModel.kt @@ -16,7 +16,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.stateIn import javax.inject.Inject @@ -27,16 +26,17 @@ class PostCommentViewModel @Inject constructor( private val _currentQuery = MutableLiveData() val currentQuery: LiveData = _currentQuery - val commentLiveData: Flow> = currentQuery.switchMap { arguments -> - getCommentsRepository - .fetch(arguments) - .cachedIn(viewModelScope) - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = PagingData.empty() - ).asLiveData(Dispatchers.IO) - }.asFlow() + val commentLiveData: Flow> = + currentQuery.switchMap { arguments -> + getCommentsRepository + .fetch(arguments) + .cachedIn(viewModelScope) + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = PagingData.empty() + ).asLiveData(Dispatchers.IO) + }.asFlow() override fun initState(): PagingData { return PagingData.empty() diff --git a/app/src/main/java/com/no5ing/bbibbi/util/DateParser.kt b/app/src/main/java/com/no5ing/bbibbi/util/DateParser.kt index 7da1787..120b573 100644 --- a/app/src/main/java/com/no5ing/bbibbi/util/DateParser.kt +++ b/app/src/main/java/com/no5ing/bbibbi/util/DateParser.kt @@ -111,7 +111,7 @@ fun LocalDate.isBirthdayNow(): Boolean { fun formatYearMonth(year: Int, month: Int): String { val isSameYear = year == ZonedDateTime.now().year val currentYearMonth = YearMonth.of(year, month) - return if(isSameYear) + return if (isSameYear) currentYearMonth.format(sameYearMonthFormatter) - else currentYearMonth.format(yearMonthFormatter) + else currentYearMonth.format(yearMonthFormatter) } From 991c7c77253a6354495318287c2b85981bff8c74 Mon Sep 17 00:00:00 2001 From: ChuYong Date: Thu, 2 May 2024 19:40:13 +0900 Subject: [PATCH 21/26] =?UTF-8?q?feat:=20=EB=AC=B8=EC=9E=90=EC=97=B4=20?= =?UTF-8?q?=EB=B2=88=EC=97=AD=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../view/main/home/HomePageUploadButton.kt | 14 +- .../view/main/home/NightHomePageContent.kt | 13 +- .../feature/view/main/home/TryPickPopup.kt | 126 ------------------ .../main/HomePageController.kt | 35 +++-- app/src/main/res/values-en/strings.xml | 30 +++++ app/src/main/res/values-ja/strings.xml | 30 +++++ app/src/main/res/values/strings.xml | 31 +++++ 7 files changed, 126 insertions(+), 153 deletions(-) delete mode 100644 app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/TryPickPopup.kt diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageUploadButton.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageUploadButton.kt index 0102152..824cb44 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageUploadButton.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageUploadButton.kt @@ -116,13 +116,13 @@ fun BoxScope.HomePageMissionUploadButton( UploadHelperPop( text = if (!isMeUploadedToday) - "생존신고 후 미션 사진을 올릴 수 있어요" + stringResource(id = R.string.home_mission_survival_not_uploadable) else if (!isMissionUnlocked) - "아직 미션 사진을 찍을 수 없어요" + stringResource(id = R.string.home_mission_not_uploadable) else if (isMeMissionUploaded) - "오늘의 미션은 완료되었어요" + stringResource(id = R.string.home_mission_completed) else - "미션 사진을 찍으러 가볼까요?" + stringResource(id = R.string.home_mission_upload_ready) ) CameraCaptureButton( onClick = onTap, @@ -226,13 +226,15 @@ fun WaitingMembersPop( } if (pickers.size == 1) { Text( - text = "${pickers.first().displayName}님이 기다리고 있어요", + text = stringResource(id = R.string.home_someone_waiting_you, + pickers.first().displayName), style = MaterialTheme.bbibbiTypo.bodyTwoRegular, color = MaterialTheme.bbibbiScheme.backgroundHover, ) } else { Text( - text = "${pickers.first().displayName}님이 외 ${pickers.size - 1}명이 기다리고 있어요", + text = stringResource(id = R.string.home_some_people_waiting_you, + pickers.first().displayName, pickers.size - 1), style = MaterialTheme.bbibbiTypo.bodyTwoRegular, color = MaterialTheme.bbibbiScheme.backgroundHover, ) diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/NightHomePageContent.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/NightHomePageContent.kt index 6429f2a..913aa15 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/NightHomePageContent.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/NightHomePageContent.kt @@ -42,6 +42,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -83,7 +84,6 @@ fun NightHomePageContent( mutableIntStateOf(0) } val balloonColor = MaterialTheme.bbibbiScheme.button - val balloonText = "생존신고 횟수가 동일한 경우\n이모지, 댓글 수를 합산해서 등수를 정해요" val builder = rememberBalloonBuilder { setArrowSize(10) setArrowPosition(0.5f) @@ -96,7 +96,6 @@ fun NightHomePageContent( setMarginHorizontal(12) setCornerRadius(12f) setBackgroundColor(balloonColor) - // setBackgroundColorResource(balloonColor) setBalloonAnimation(BalloonAnimation.ELASTIC) } @@ -178,7 +177,7 @@ fun NightHomePageContent( horizontalArrangement = Arrangement.spacedBy(4.dp) ) { Text( - text = "이번달 최고 기여자", + text = stringResource(id = R.string.home_max_monthly_contributor), style = MaterialTheme.bbibbiTypo.headTwoBold, color = MaterialTheme.bbibbiScheme.textPrimary, ) @@ -186,7 +185,7 @@ fun NightHomePageContent( builder = builder, balloonContent = { Text( - text = balloonText, + text = stringResource(id = R.string.home_contributor_baloon), textAlign = TextAlign.Center, color = MaterialTheme.bbibbiScheme.white, style = MaterialTheme.bbibbiTypo.bodyTwoRegular, @@ -205,7 +204,7 @@ fun NightHomePageContent( } Text( - text = "${ranking.month}월 생존신고 횟수", + text = stringResource(id = R.string.home_max_monthly_contributor_month, ranking.month), style = MaterialTheme.bbibbiTypo.bodyTwoRegular, color = MaterialTheme.bbibbiScheme.textSecondary, ) @@ -257,14 +256,14 @@ fun NightHomePageContent( Spacer(modifier = Modifier.height(36.dp)) if (ranking.isAllRankersNull()) { Text( - text = "아직 활동한 가족이 없어요", + text = stringResource(id = R.string.home_no_before_survival), style = MaterialTheme.bbibbiTypo.bodyOneBold, color = MaterialTheme.bbibbiScheme.textSecondary, ) Spacer(modifier = Modifier.height(8.dp)) } else { FlatButton( - text = "지난 날 생존신고 보기", + text = stringResource(id = R.string.home_see_before_survival), modifier = Modifier.fillMaxWidth(), onClick = { ranking.mostRecentSurvivalPostDate?.apply(onTapViewPost) diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/TryPickPopup.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/TryPickPopup.kt deleted file mode 100644 index a2c4c0b..0000000 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/TryPickPopup.kt +++ /dev/null @@ -1,126 +0,0 @@ -package com.no5ing.bbibbi.presentation.feature.view.main.home - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -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.shape.RoundedCornerShape -import androidx.compose.material3.AlertDialogDefaults -import androidx.compose.material3.BasicAlertDialog -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.ExperimentalMaterial3Api -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.res.painterResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.DialogProperties -import com.no5ing.bbibbi.R -import com.no5ing.bbibbi.presentation.feature.view.common.AlertDialogContent -import com.no5ing.bbibbi.presentation.feature.view.common.AlertDialogFlowRow -import com.no5ing.bbibbi.presentation.feature.view.common.ButtonsMainAxisSpacing -import com.no5ing.bbibbi.presentation.theme.bbibbiScheme -import com.no5ing.bbibbi.presentation.theme.bbibbiTypo - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun TryPickPopup( - enabledState: Boolean = false, - targetNickname: String = "", - onTapNow: () -> Unit = {}, - onTapLater: () -> Unit = {}, -) { - if (enabledState) { - BasicAlertDialog( - onDismissRequest = onTapLater, - modifier = Modifier, - properties = DialogProperties() - ) { - AlertDialogContent( - textPadding = PaddingValues(0.dp), - buttons = { - AlertDialogFlowRow( - mainAxisSpacing = ButtonsMainAxisSpacing, - crossAxisSpacing = 8.dp - ) { - Button( - onClick = onTapNow, - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.bbibbiScheme.mainYellow - ), - shape = RoundedCornerShape(10.dp), - modifier = Modifier - .height(44.dp) - .fillMaxWidth() - ) { - Text( - "지금 하기", - style = MaterialTheme.bbibbiTypo.bodyOneBold, - color = Color(0xff242427) - ) - } - Button( - onClick = onTapLater, - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.bbibbiScheme.button - ), - shape = RoundedCornerShape(10.dp), - modifier = Modifier - .height(44.dp) - .fillMaxWidth() - ) { - Text( - "다음에 하기", - style = MaterialTheme.bbibbiTypo.bodyOneBold, - color = MaterialTheme.bbibbiScheme.icon - ) - } - } - }, - icon = null, - title = { - Text( - "생존 확인하기", - color = MaterialTheme.bbibbiScheme.iconSelected, - style = MaterialTheme.bbibbiTypo.headTwoBold, - ) - }, - text = { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxWidth() - ) { - Text( - "${targetNickname}님의 생존 여부를 물어볼까요?\n지금 알림이 전송됩니다.", - color = MaterialTheme.bbibbiScheme.textSecondary, - style = MaterialTheme.bbibbiTypo.bodyTwoRegular, - textAlign = TextAlign.Center, - ) - Spacer(modifier = Modifier.height(24.dp)) - Image( - painter = painterResource(id = R.drawable.lying_bibbi), - contentDescription = null, - ) - } - - }, - shape = RoundedCornerShape(14.dp), - containerColor = MaterialTheme.bbibbiScheme.backgroundPrimary, - tonalElevation = AlertDialogDefaults.TonalElevation, - iconContentColor = AlertDialogDefaults.iconContentColor, - titleContentColor = AlertDialogDefaults.titleContentColor, - textContentColor = AlertDialogDefaults.textContentColor, - modifier = Modifier.padding(0.dp) - ) - - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/HomePageController.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/HomePageController.kt index 44de6b5..9d3b55c 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/HomePageController.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/HomePageController.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavBackStackEntry import androidx.navigation.NavHostController @@ -19,7 +20,6 @@ import com.no5ing.bbibbi.presentation.component.showSnackBarWithDismiss import com.no5ing.bbibbi.presentation.component.snackBarPick import com.no5ing.bbibbi.presentation.feature.view.common.GenericPopup import com.no5ing.bbibbi.presentation.feature.view.main.home.HomePage -import com.no5ing.bbibbi.presentation.feature.view.main.home.TryPickPopup import com.no5ing.bbibbi.presentation.feature.view_controller.CameraViewPageController.goCameraViewPage import com.no5ing.bbibbi.presentation.feature.view_controller.NavigationDestination import com.no5ing.bbibbi.presentation.feature.view_controller.main.CalendarDetailPageController.goCalendarDetailPage @@ -35,6 +35,7 @@ import com.no5ing.bbibbi.presentation.feature.view_model.MainPageViewModel import com.no5ing.bbibbi.presentation.feature.view_model.members.PickMemberViewModel import com.no5ing.bbibbi.util.LocalSnackbarHostState import com.no5ing.bbibbi.util.gapUntilNext +import com.no5ing.bbibbi.util.localResources object HomePageController : NavigationDestination( route = mainHomePageRoute, @@ -50,6 +51,7 @@ object HomePageController : NavigationDestination( var tryPickDialogMember by remember { mutableStateOf(null) } val pickState = pickMemberViewModel.uiState.collectAsState() val postViewTypeState = remember { mutableStateOf(PostType.SURVIVAL) } + val resources = localResources() LaunchedEffect(pickState.value.status) { if (!pickState.value.isIdle()) { mainPageViewModel.invoke(Arguments()) @@ -69,14 +71,19 @@ object HomePageController : NavigationDestination( } } } - TryPickPopup( + + GenericPopup( enabledState = isPickDialogVisible, - targetNickname = tryPickDialogMember?.displayName ?: "", - onTapNow = { + title = stringResource(id = R.string.home_check_survival), + description = stringResource(id = R.string.home_check_survival_description, tryPickDialogMember?.displayName ?: ""), + image = painterResource(id = R.drawable.mission_require_survival), + confirmText = stringResource(id = R.string.home_check_survival_confirm), + cancelText = stringResource(id = R.string.home_check_survival_cancel), + onTapConfirm = { isPickDialogVisible = false mainPageViewModel.addPickMembersSet(tryPickDialogMember?.memberId ?: "") snackBarHost.showSnackBarWithDismiss( - message = "${tryPickDialogMember?.displayName ?: ""}님에게 생존신고 알림을 보냈어요", + message = resources.getString(R.string.home_check_survival_snack, tryPickDialogMember?.displayName ?: ""), actionLabel = snackBarPick, ) pickMemberViewModel.invoke( @@ -87,17 +94,17 @@ object HomePageController : NavigationDestination( ) ) }, - onTapLater = { + onTapCancel = { isPickDialogVisible = false } ) GenericPopup( enabledState = isRequireSurvivalDialogVisible, - title = "생존신고 사진을 먼저 찍으세요!", - description = "미션 사진을 올리려면\n생존신고 사진을 먼저 업로드해야해요.", + title = stringResource(id = R.string.home_survival_first), + description = stringResource(id = R.string.home_survival_first_description), image = painterResource(id = R.drawable.mission_require_survival), - confirmText = "생존신고 먼저 하기", - cancelText = "다음에 하기", + confirmText = stringResource(id = R.string.home_survival_first_confirm), + cancelText = stringResource(id = R.string.home_survival_first_cancel), onTapConfirm = { isRequireSurvivalDialogVisible = false navController.goPostUploadPage() @@ -109,11 +116,11 @@ object HomePageController : NavigationDestination( ) GenericPopup( enabledState = isTryMissionPictureDialogVisible, - title = "미션 열쇠 획득!", - description = "열쇠를 획득해 잠금이 해제되었어요.\n미션 사진을 찍을 수 있어요!", + title = stringResource(id = R.string.home_mission_key), + description = stringResource(id = R.string.home_mission_key_description), image = painterResource(id = R.drawable.mission_key), - confirmText = "미션 사진 찍기", - cancelText = "닫기", + confirmText = stringResource(id = R.string.home_mission_key_confirm), + cancelText = stringResource(id = R.string.home_mission_key_cancel), onTapConfirm = { isTryMissionPictureDialogVisible = false navController.goMissionUploadPage() diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 6aea245..bc7214c 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -13,6 +13,36 @@ There\'s not much time left! You can upload photos from 12pm tomorrow! + You can upload mission photos after survival reporting + You cannot take mission photos yet + Today\'s mission is completed + Shall we go take mission photos? + + %1$s is waiting for you + %1$s and %2$d others are waiting for you + + Check Survival + Shall we ask about %1$s\'s survival?\nAn alert will be sent now. + Do it now + Do it later + A survival alert has been sent to %1$s + + Take the survival report photo first! + To upload mission photos,\nyou need to upload the survival report photo first. + Do survival report first + Do it later + + Mission key obtained! + The lock has been unlocked by obtaining the key.\nYou can take mission photos now + Take mission photo + Close + + Top Contributor of the Month + %1$d times survival reporting in this month + View past survival reports + No family activity yet + In case of equal survival reporting counts,\nrank is determined by summing up emojis and comments + Continue Complete diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 0b20c64..cee757d 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -13,6 +13,36 @@ 残り時間があまりありません! 明日の12時から写真をアップロードできます! + 生存報告後にミッション写真をアップロードできます + まだミッション写真を撮ることができません + 今日のミッションは完了しました + ミッションの写真を撮りに行きましょうか? + + %1$sがあなたを待っています + %1$sと他%2$d人があなたを待っています + + 生存を確認する + %1$sの生存状況を尋ねましょうか?\nアラートが今送信されます。 + 今すぐ行う + 後で行う + %1$sに生存報告アラートが送信されました + + まず生存報告写真を撮ってください! + ミッション写真をアップロードするには、\nまず生存報告写真をアップロードする必要があります。 + 先に生存報告をする + 後で行う + + ミッションキーを入手しました! + 鍵を入手してロックが解除されました。\n今、ミッションの写真を撮ることができます + ミッションの写真を撮る + 閉じる + + 今月のトップ貢献者 + %1$d月の生存報告回数 + 過去の生存報告を見る + まだ家族の活動がありません + 生存報告の回数が同じ場合、\n絵文字とコメントを合算して順位を決定します + 続ける 完了 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 97744ec..3954542 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -13,6 +13,37 @@ 시간이 얼마 남지 않았어요! 내일 낮 10시부터 사진을 올릴 수 있어요! + 생존신고 후 미션 사진을 올릴 수 있어요 + 아직 미션 사진을 찍을 수 없어요 + 오늘의 미션은 완료되었어요 + 미션 사진을 찍으러 가볼까요? + + %1$s님이 기다리고 있어요 + %1$s님 외 %2$d명이 기다리고 있어요 + + 생존 확인하기 + %1$s님의 생존 여부를 물어볼까요?\n지금 알림이 전송됩니다. + 지금 하기 + 다음에 하기 + %1$s님에게 생존신고 알림을 보냈어요 + + 생존신고 사진을 먼저 찍으세요! + 미션 사진을 올리려면\n생존신고 사진을 먼저 업로드해야해요. + 생존신고 먼저 하기 + 다음에 하기 + + 미션 열쇠 획득! + 열쇠를 획득해 잠금이 해제되었어요.\n미션 사진을 찍을 수 있어요 + 미션 사진 찍기 + 닫기 + + 이번달 최고 기여자 + %1$d월 생존신고 횟수 + 지난 날 생존신고 보기 + 아직 활동한 가족이 없어요 + 생존신고 횟수가 동일한 경우\n이모지, 댓글 수를 합산해서 등수를 정해요 + + 계속 완료 From cd0be6fe0f5ce8e23ecf02d434abda14b65e6a46 Mon Sep 17 00:00:00 2001 From: ChuYong Date: Thu, 2 May 2024 19:49:09 +0900 Subject: [PATCH 22/26] =?UTF-8?q?style:=20=EC=95=88=EC=93=B0=EB=8A=94=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../view/main/home/HomePageUploadButton.kt | 12 ++-- .../view/main/home/NightHomePageContent.kt | 5 +- .../main/HomePageController.kt | 10 ++- .../view_model/MainPageNightViewModel.kt | 2 - .../view_model/auth/RetrieveMeViewModel.kt | 11 --- .../mission/GetMissionByIdViewModel.kt | 25 ------- .../post/CalendarDetailContentViewModel.kt | 68 ------------------- .../post/DailyFamilyTopViewModel.kt | 68 ------------------- .../post/IsMeUploadedTodayViewModel.kt | 46 ------------- .../view_model/post/MainPostFeedViewModel.kt | 46 ------------- 10 files changed, 20 insertions(+), 273 deletions(-) delete mode 100644 app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/mission/GetMissionByIdViewModel.kt delete mode 100644 app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/CalendarDetailContentViewModel.kt delete mode 100644 app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/DailyFamilyTopViewModel.kt delete mode 100644 app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/IsMeUploadedTodayViewModel.kt delete mode 100644 app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/MainPostFeedViewModel.kt diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageUploadButton.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageUploadButton.kt index 824cb44..3d2667c 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageUploadButton.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageUploadButton.kt @@ -226,15 +226,19 @@ fun WaitingMembersPop( } if (pickers.size == 1) { Text( - text = stringResource(id = R.string.home_someone_waiting_you, - pickers.first().displayName), + text = stringResource( + id = R.string.home_someone_waiting_you, + pickers.first().displayName + ), style = MaterialTheme.bbibbiTypo.bodyTwoRegular, color = MaterialTheme.bbibbiScheme.backgroundHover, ) } else { Text( - text = stringResource(id = R.string.home_some_people_waiting_you, - pickers.first().displayName, pickers.size - 1), + text = stringResource( + id = R.string.home_some_people_waiting_you, + pickers.first().displayName, pickers.size - 1 + ), style = MaterialTheme.bbibbiTypo.bodyTwoRegular, color = MaterialTheme.bbibbiScheme.backgroundHover, ) diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/NightHomePageContent.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/NightHomePageContent.kt index 913aa15..46cc65a 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/NightHomePageContent.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/NightHomePageContent.kt @@ -204,7 +204,10 @@ fun NightHomePageContent( } Text( - text = stringResource(id = R.string.home_max_monthly_contributor_month, ranking.month), + text = stringResource( + id = R.string.home_max_monthly_contributor_month, + ranking.month + ), style = MaterialTheme.bbibbiTypo.bodyTwoRegular, color = MaterialTheme.bbibbiScheme.textSecondary, ) diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/HomePageController.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/HomePageController.kt index 9d3b55c..010dbd9 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/HomePageController.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/HomePageController.kt @@ -75,7 +75,10 @@ object HomePageController : NavigationDestination( GenericPopup( enabledState = isPickDialogVisible, title = stringResource(id = R.string.home_check_survival), - description = stringResource(id = R.string.home_check_survival_description, tryPickDialogMember?.displayName ?: ""), + description = stringResource( + id = R.string.home_check_survival_description, + tryPickDialogMember?.displayName ?: "" + ), image = painterResource(id = R.drawable.mission_require_survival), confirmText = stringResource(id = R.string.home_check_survival_confirm), cancelText = stringResource(id = R.string.home_check_survival_cancel), @@ -83,7 +86,10 @@ object HomePageController : NavigationDestination( isPickDialogVisible = false mainPageViewModel.addPickMembersSet(tryPickDialogMember?.memberId ?: "") snackBarHost.showSnackBarWithDismiss( - message = resources.getString(R.string.home_check_survival_snack, tryPickDialogMember?.displayName ?: ""), + message = resources.getString( + R.string.home_check_survival_snack, + tryPickDialogMember?.displayName ?: "" + ), actionLabel = snackBarPick, ) pickMemberViewModel.invoke( diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/MainPageNightViewModel.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/MainPageNightViewModel.kt index a9cd409..4911f4f 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/MainPageNightViewModel.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/MainPageNightViewModel.kt @@ -1,6 +1,5 @@ package com.no5ing.bbibbi.presentation.feature.view_model -import com.no5ing.bbibbi.data.datasource.local.LocalDataStorage import com.no5ing.bbibbi.data.datasource.network.RestAPI import com.no5ing.bbibbi.data.model.APIResponse import com.no5ing.bbibbi.data.model.APIResponse.Companion.wrapToAPIResponse @@ -13,7 +12,6 @@ import javax.inject.Inject @HiltViewModel class MainPageNightViewModel @Inject constructor( private val restAPI: RestAPI, - private val localDataStorage: LocalDataStorage, ) : BaseViewModel>() { override fun initState(): APIResponse { diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/auth/RetrieveMeViewModel.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/auth/RetrieveMeViewModel.kt index f173369..67f3149 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/auth/RetrieveMeViewModel.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/auth/RetrieveMeViewModel.kt @@ -1,7 +1,5 @@ package com.no5ing.bbibbi.presentation.feature.view_model.auth -import android.net.Uri -import com.no5ing.bbibbi.data.datasource.local.LocalDataStorage import com.no5ing.bbibbi.data.datasource.network.RestAPI import com.no5ing.bbibbi.data.model.APIResponse import com.no5ing.bbibbi.data.model.APIResponse.Companion.wrapToAPIResponse @@ -15,20 +13,11 @@ import javax.inject.Inject @HiltViewModel class RetrieveMeViewModel @Inject constructor( private val restAPI: RestAPI, - private val localDataStorage: LocalDataStorage, ) : BaseViewModel>() { override fun initState(): APIResponse { return APIResponse.idle() } - fun getAndDeleteTemporaryUri(): Uri? { - val uri = localDataStorage.getTemporaryUri() - if (uri != null) { - localDataStorage.clearTemporaryUri() - } - return uri?.let { Uri.parse(it) } - } - override fun invoke(arguments: Arguments) { withMutexScope(Dispatchers.IO) { val meResult = restAPI.getMemberApi().getMeInfo() diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/mission/GetMissionByIdViewModel.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/mission/GetMissionByIdViewModel.kt deleted file mode 100644 index 7c78b0e..0000000 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/mission/GetMissionByIdViewModel.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.no5ing.bbibbi.presentation.feature.view_model.mission - -import com.no5ing.bbibbi.data.datasource.local.MissionCacheProvider -import com.no5ing.bbibbi.data.model.mission.Mission -import com.no5ing.bbibbi.data.repository.Arguments -import com.no5ing.bbibbi.presentation.feature.view_model.BaseViewModel -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import javax.inject.Inject - -@HiltViewModel -class GetMissionByIdViewModel @Inject constructor( - private val missionCacheProvider: MissionCacheProvider, -) : BaseViewModel() { - override fun initState(): Mission? { - return null - } - - override fun invoke(arguments: Arguments) { - val missionId = arguments.resourceId ?: throw RuntimeException() - withMutexScope(Dispatchers.IO) { - setState(missionCacheProvider.getMission(missionId)) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/CalendarDetailContentViewModel.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/CalendarDetailContentViewModel.kt deleted file mode 100644 index 54dc2f7..0000000 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/CalendarDetailContentViewModel.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.no5ing.bbibbi.presentation.feature.view_model.post - -import com.no5ing.bbibbi.data.datasource.local.MemberCacheProvider -import com.no5ing.bbibbi.data.datasource.network.RestAPI -import com.no5ing.bbibbi.data.model.APIResponse -import com.no5ing.bbibbi.data.model.APIResponse.Companion.wrapToAPIResponse -import com.no5ing.bbibbi.data.model.member.Member -import com.no5ing.bbibbi.data.repository.Arguments -import com.no5ing.bbibbi.presentation.feature.uistate.family.MainFeedUiState -import com.no5ing.bbibbi.presentation.feature.view_model.BaseViewModel -import com.skydoves.sandwich.suspendMapSuccess -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import javax.inject.Inject - -typealias CalenderDetailContentUiState = Triple - -@HiltViewModel -class CalendarDetailContentViewModel @Inject constructor( - private val restAPI: RestAPI, - private val memberCacheProvider: MemberCacheProvider, -) : BaseViewModel>() { - private val detailCache = mutableMapOf() - override fun initState(): APIResponse { - return APIResponse.idle() - } - - override fun invoke(arguments: Arguments) { - val left = arguments.get("left") - val right = arguments.get("right") - withMutexScope(Dispatchers.IO) { - val resId = arguments.resourceId ?: throw RuntimeException() - val mainPost = getOrFetch(resId) - val leftPost = getOrFetch(left) - val rightPost = getOrFetch(right) - val mainPostResult = CalenderDetailContentUiState( - first = leftPost.await(), - second = mainPost.await(), - third = rightPost.await() - ) - setState(APIResponse.success(mainPostResult)) - } - } - - private fun CoroutineScope.getOrFetch(postId: String?): Deferred = async { - postId?.let { - val previous = detailCache[it] - if (previous != null) return@let previous - val currentPost = restAPI.getPostApi().getPost(it) - val results = currentPost.suspendMapSuccess { - val member = kotlin.runCatching { - memberCacheProvider.getMember(this.authorId) - } - val uiState = MainFeedUiState( - writer = member.getOrElse { Member.unknown() }, - post = this - ) - detailCache[it] = uiState - uiState - }.wrapToAPIResponse() - if (results.isReady()) results.data else null - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/DailyFamilyTopViewModel.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/DailyFamilyTopViewModel.kt deleted file mode 100644 index 40e5573..0000000 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/DailyFamilyTopViewModel.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.no5ing.bbibbi.presentation.feature.view_model.post - -import com.no5ing.bbibbi.data.datasource.network.RestAPI -import com.no5ing.bbibbi.data.model.APIResponse -import com.no5ing.bbibbi.data.repository.Arguments -import com.no5ing.bbibbi.presentation.feature.uistate.family.MainFeedStoryElementUiState -import com.no5ing.bbibbi.presentation.feature.view_model.BaseViewModel -import com.no5ing.bbibbi.util.todayAsString -import com.skydoves.sandwich.getOrNull -import com.skydoves.sandwich.isSuccess -import com.skydoves.sandwich.suspendOnFailure -import com.skydoves.sandwich.suspendOnSuccess -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import javax.inject.Inject - -@HiltViewModel -class DailyFamilyTopViewModel @Inject constructor( - private val restAPI: RestAPI, -) : BaseViewModel>>() { - override fun initState(): APIResponse> { - return APIResponse.idle() - } - - override fun invoke(arguments: Arguments) { - withMutexScope(Dispatchers.IO) { - val newList = ArrayList() - val members = async { - restAPI.getMemberApi() - .getMembers( - page = 1, - size = 100 - ) - } - restAPI - .getPostApi() - .getPosts( - page = 1, - size = 100, - memberId = null, - date = todayAsString(), - sort = "ASC" - ).suspendOnSuccess { - val response = members.await() - if (response.isSuccess) { - val memberMap = (response.getOrNull()?.results?.associateBy { it.memberId } - ?: emptyMap()) - .toMutableMap() - data.results.forEachIndexed { index, post -> - val currentMember = - memberMap.remove(post.authorId) ?: return@forEachIndexed - newList.add(MainFeedStoryElementUiState(currentMember, true)) - } - memberMap.forEach { - newList.add(MainFeedStoryElementUiState(it.value, false)) - } - setState(APIResponse.success(newList)) - } else { - setState(APIResponse.unknownError()) - } - }.suspendOnFailure { - setState(APIResponse.unknownError()) - } - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/IsMeUploadedTodayViewModel.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/IsMeUploadedTodayViewModel.kt deleted file mode 100644 index f13d2f5..0000000 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/IsMeUploadedTodayViewModel.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.no5ing.bbibbi.presentation.feature.view_model.post - -import com.no5ing.bbibbi.data.datasource.local.LocalDataStorage -import com.no5ing.bbibbi.data.datasource.network.RestAPI -import com.no5ing.bbibbi.data.model.APIResponse -import com.no5ing.bbibbi.data.repository.Arguments -import com.no5ing.bbibbi.presentation.feature.view_model.BaseViewModel -import com.no5ing.bbibbi.util.todayAsString -import com.skydoves.sandwich.suspendOnSuccess -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import javax.inject.Inject - -@HiltViewModel -class IsMeUploadedTodayViewModel @Inject constructor( - private val restAPI: RestAPI, - private val localDataStorage: LocalDataStorage, -) : BaseViewModel>() { - var shouldDisplayWidgetPopup = localDataStorage.getAndRemoveWidgetPopupPeriod() - override fun initState(): APIResponse { - return APIResponse.loading() - } - - override fun invoke(arguments: Arguments) { - val memberId = arguments.get("memberId") ?: throw RuntimeException() - withMutexScope(Dispatchers.IO) { - restAPI - .getPostApi() - .getPosts( - page = 1, - size = 1, - memberId = memberId, - date = todayAsString() - ).suspendOnSuccess { - val isMeUploadedToday = data.results.isNotEmpty() - setState( - APIResponse( - status = APIResponse.Status.SUCCESS, - data = isMeUploadedToday - ) - ) - } - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/MainPostFeedViewModel.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/MainPostFeedViewModel.kt deleted file mode 100644 index 3378556..0000000 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/MainPostFeedViewModel.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.no5ing.bbibbi.presentation.feature.view_model.post - -import androidx.lifecycle.viewModelScope -import androidx.paging.PagingData -import androidx.paging.cachedIn -import com.no5ing.bbibbi.data.repository.Arguments -import com.no5ing.bbibbi.data.repository.post.GetFeedsRepository -import com.no5ing.bbibbi.presentation.feature.uistate.family.MainFeedUiState -import com.no5ing.bbibbi.presentation.feature.view_model.BaseViewModel -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.stateIn -import javax.inject.Inject - -@HiltViewModel -class MainPostFeedViewModel @Inject constructor( - private val getPostsRepository: GetFeedsRepository, -) : BaseViewModel>() { - override fun initState(): PagingData { - return PagingData.empty() - } - - override fun invoke(arguments: Arguments) { - withMutexScope(Dispatchers.IO) { - getPostsRepository - .fetch(arguments) - .cachedIn(viewModelScope) - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = PagingData.empty() - ).collectLatest { - setState(it) - } - } - } - - fun refresh() = getPostsRepository.invalidateSource() - - override fun release() { - super.release() - getPostsRepository.closeResources() - } -} \ No newline at end of file From 68cbe2adfc51854a9f6f0eacf729b4023b63f0ce Mon Sep 17 00:00:00 2001 From: ChuYong Date: Wed, 8 May 2024 12:15:42 +0900 Subject: [PATCH 23/26] =?UTF-8?q?feat:=20ranking=20null=EA=B0=92=20?= =?UTF-8?q?=EB=8C=80=EC=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feature/view/main/home/NightHomePageContent.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/NightHomePageContent.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/NightHomePageContent.kt index 46cc65a..ebaa8a6 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/NightHomePageContent.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/NightHomePageContent.kt @@ -257,7 +257,7 @@ fun NightHomePageContent( } } Spacer(modifier = Modifier.height(36.dp)) - if (ranking.isAllRankersNull()) { + if (ranking.isAllRankersNull() || ranking.mostRecentSurvivalPostDate == null) { Text( text = stringResource(id = R.string.home_no_before_survival), style = MaterialTheme.bbibbiTypo.bodyOneBold, @@ -269,7 +269,7 @@ fun NightHomePageContent( text = stringResource(id = R.string.home_see_before_survival), modifier = Modifier.fillMaxWidth(), onClick = { - ranking.mostRecentSurvivalPostDate?.apply(onTapViewPost) + ranking.mostRecentSurvivalPostDate.apply(onTapViewPost) }, ) } From 6c66c294fbc2df655cc473e2b747a573584df887 Mon Sep 17 00:00:00 2001 From: ChuYong Date: Sat, 11 May 2024 19:28:43 +0900 Subject: [PATCH 24/26] =?UTF-8?q?feat:=20main=20view=20api=20=EB=8B=89?= =?UTF-8?q?=EB=84=A4=EC=9E=84=20nullable=20=EB=8C=80=EC=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/no5ing/bbibbi/data/model/view/MainPageModel.kt | 2 +- .../feature/view/main/home/HomePageUploadButton.kt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/no5ing/bbibbi/data/model/view/MainPageModel.kt b/app/src/main/java/com/no5ing/bbibbi/data/model/view/MainPageModel.kt index 2be9924..d8883ce 100644 --- a/app/src/main/java/com/no5ing/bbibbi/data/model/view/MainPageModel.kt +++ b/app/src/main/java/com/no5ing/bbibbi/data/model/view/MainPageModel.kt @@ -37,7 +37,7 @@ data class MainPageFeedModel( data class MainPagePickerModel( val memberId: String, val imageUrl: String?, - val displayName: String, + val displayName: String?, ) : Parcelable @Parcelize diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageUploadButton.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageUploadButton.kt index 3d2667c..e644510 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageUploadButton.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageUploadButton.kt @@ -214,7 +214,7 @@ fun WaitingMembersPop( Box { pickersShattered.reversed().forEachIndexed { rawIdx, it -> MiniCircledIcon( - noImageLetter = it.displayName.first().toString(), + noImageLetter = it.displayName?.first()?.toString() ?: "?", imageUrl = it.imageUrl, modifier = Modifier.offset( x = 16.dp * (pickersShattered.size - 1 - rawIdx) @@ -228,7 +228,7 @@ fun WaitingMembersPop( Text( text = stringResource( id = R.string.home_someone_waiting_you, - pickers.first().displayName + pickers.first().displayName ?:"" ), style = MaterialTheme.bbibbiTypo.bodyTwoRegular, color = MaterialTheme.bbibbiScheme.backgroundHover, @@ -237,7 +237,7 @@ fun WaitingMembersPop( Text( text = stringResource( id = R.string.home_some_people_waiting_you, - pickers.first().displayName, pickers.size - 1 + pickers.first().displayName ?: "", pickers.size - 1 ), style = MaterialTheme.bbibbiTypo.bodyTwoRegular, color = MaterialTheme.bbibbiScheme.backgroundHover, From 5cc22b0dde2e524ca952273cdceb146ffa8c54ca Mon Sep 17 00:00:00 2001 From: ChuYong Date: Sat, 11 May 2024 23:43:38 +0900 Subject: [PATCH 25/26] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EB=B0=B0=EB=84=88=20=EC=8A=A4=EC=BC=88=EB=A0=88=ED=86=A4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feature/view/main/calendar/MainCalendarPage.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/calendar/MainCalendarPage.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/calendar/MainCalendarPage.kt index 5c0aacd..080f058 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/calendar/MainCalendarPage.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/calendar/MainCalendarPage.kt @@ -1,6 +1,7 @@ package com.no5ing.bbibbi.presentation.feature.view.main.calendar import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -15,6 +16,7 @@ import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -182,6 +184,14 @@ fun MainCalendarPage( } } + } else { + Box( + modifier = Modifier + .padding(horizontal = 20.dp) + .fillMaxWidth() + .background(MaterialTheme.bbibbiScheme.backgroundSecondary, RoundedCornerShape(24.dp)) + .aspectRatio(335.0f/220) + ) } Spacer(modifier = Modifier.height(24.dp)) StaticCalendar( From ea3c74197aa27900d05221e77e2e0fe434f2a5b4 Mon Sep 17 00:00:00 2001 From: ChuYong Date: Thu, 30 May 2024 12:36:43 +0900 Subject: [PATCH 26/26] =?UTF-8?q?feat:=20=EC=B4=AC=EC=98=81=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EB=B0=8F=20=EC=97=85=EB=A1=9C=EB=93=9C=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EB=8B=A8=EC=B6=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UploadMissionCamera.kt | 2 +- .../com/no5ing/bbibbi/util/KotlinExtension.kt | 73 +------------------ 2 files changed, 2 insertions(+), 73 deletions(-) diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload_camera/UploadMissionCamera.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload_camera/UploadMissionCamera.kt index 1812071..bbeeca8 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload_camera/UploadMissionCamera.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload_camera/UploadMissionCamera.kt @@ -74,7 +74,7 @@ fun UploadMissionCamera( val coroutineScope = rememberCoroutineScope() val torchState = remember { mutableStateOf(false) } val isPermissionGranted = remember { mutableStateOf(false) } - val cameraDirection = remember { mutableStateOf(CameraSelector.DEFAULT_FRONT_CAMERA) } + val cameraDirection = remember { mutableStateOf(CameraSelector.DEFAULT_BACK_CAMERA) } val cameraState = remember { mutableStateOf(null) } val captureState = remember { mutableStateOf(ImageCapture.Builder().build()) } var isCapturing by remember { mutableStateOf(false) } diff --git a/app/src/main/java/com/no5ing/bbibbi/util/KotlinExtension.kt b/app/src/main/java/com/no5ing/bbibbi/util/KotlinExtension.kt index 1892fdd..3f489bc 100644 --- a/app/src/main/java/com/no5ing/bbibbi/util/KotlinExtension.kt +++ b/app/src/main/java/com/no5ing/bbibbi/util/KotlinExtension.kt @@ -93,43 +93,6 @@ suspend fun Context.getCameraProvider(): ProcessCameraProvider = } } -suspend fun ImageCapture.takePhoto(context: Context): Uri? = - suspendCoroutine { continuation -> - val name = "${System.currentTimeMillis()}.jpg" - val contentValues = ContentValues().apply { - put(MediaStore.MediaColumns.DISPLAY_NAME, name) - put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg") - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { - put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/BBiBBi-Image") - } - } - - val outputOptions = ImageCapture.OutputFileOptions - .Builder( - context.contentResolver, - MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues - ) - .build() - - this.takePicture( - outputOptions, - ContextCompat.getMainExecutor(context), - object : ImageCapture.OnImageSavedCallback { - override fun onError(e: ImageCaptureException) { - Timber.e("[CameraView] photo capture failed", e) - continuation.resume(null) - } - - override fun onImageSaved( - output: ImageCapture.OutputFileResults - ) { - Timber.d("onImageSaved: ${output.savedUri}") - continuation.resume(output.savedUri) - } - } - ) - } - suspend fun ImageCapture.takePhotoWithImage(context: Context, requiredFlip: Boolean): Uri? = suspendCoroutine { continuation -> this.takePicture( @@ -149,7 +112,7 @@ suspend fun ImageCapture.takePhotoWithImage(context: Context, requiredFlip: Bool else image.toBitmap() .rotateWithCropCenterRecycle(image.imageInfo.rotationDegrees).flip() - bitmap.compress(Bitmap.CompressFormat.PNG, 100, faos) + bitmap.compress(Bitmap.CompressFormat.JPEG, 80, faos) faos.close() continuation.resume(Uri.fromFile(context.getFileStreamPath(fileName))) } @@ -174,26 +137,6 @@ suspend fun ImageCapture.takePhotoTemporary(context: Context): ImageProxy? = ) } -fun ImageProxy.toRequestBody( - contentType: MediaType? = null, - offset: Int = 0, -): RequestBody { - val baos = ByteArrayOutputStream() - val actualBitmap = toBitmap().rotateWithCropCenter(imageInfo.rotationDegrees) - actualBitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos) - return object : RequestBody() { - override fun contentType() = contentType - - override fun contentLength() = baos.size().toLong() - - override fun writeTo(sink: BufferedSink) { - baos.use { - sink.write(it.toByteArray(), offset, it.size()) - } - } - } -} - fun Bitmap.rotateWithCropCenter(degrees: Int): Bitmap { val matrix = Matrix().apply { postRotate(degrees.toFloat()) } val newBitMap = if (width >= height) { @@ -255,12 +198,6 @@ fun Modifier.customDialogModifier(pos: CustomDialogPosition) = layout { measurab } } -@Composable -fun getDialogWindow(): Window? = (LocalView.current.parent as? DialogWindowProvider)?.window - -@Composable -fun getActivityWindow(): Window? = LocalView.current.context.getActivityWindow() - private tailrec fun Context.getActivityWindow(): Window? = when (this) { is Activity -> window @@ -282,14 +219,6 @@ fun getScreenSize(): Pair { } } -fun Context.findAndroidActivity(): Activity? { - var context = this - while (context is ContextWrapper) { - if (context is Activity) return context - context = context.baseContext - } - return null -} fun getLinkIdFromUrl(url: String): String { return url.split("/").last()