diff --git a/app/src/main/java/com/eatssu/android/data/local/AppFeatureDataStore.kt b/app/src/main/java/com/eatssu/android/data/local/AppFeatureDataStore.kt new file mode 100644 index 000000000..eb31d2637 --- /dev/null +++ b/app/src/main/java/com/eatssu/android/data/local/AppFeatureDataStore.kt @@ -0,0 +1,35 @@ +package com.eatssu.android.data.local + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.preferencesDataStore +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +private val Context.appFeatureDataStore: DataStore by preferencesDataStore(name = "app_feature") + +class AppFeatureDataStore @Inject constructor( + @ApplicationContext private val context: Context +) { + + companion object { + private val ANYONE_BUT_ME_EVENT_POPUP_DISMISSED = + booleanPreferencesKey("anyone_but_me_event_popup_dismissed") + } + + val isAnyoneButMeEventPopupDismissed: Flow = context.appFeatureDataStore.data + .map { preferences -> + preferences[ANYONE_BUT_ME_EVENT_POPUP_DISMISSED] ?: false + } + + suspend fun setAnyoneButMeEventPopupDismissed(dismissed: Boolean) { + context.appFeatureDataStore.edit { preferences -> + preferences[ANYONE_BUT_ME_EVENT_POPUP_DISMISSED] = dismissed + } + } +} diff --git a/app/src/main/java/com/eatssu/android/presentation/MainActivity.kt b/app/src/main/java/com/eatssu/android/presentation/MainActivity.kt index 8a7c9dc9b..46069bb95 100644 --- a/app/src/main/java/com/eatssu/android/presentation/MainActivity.kt +++ b/app/src/main/java/com/eatssu/android/presentation/MainActivity.kt @@ -17,9 +17,10 @@ import androidx.work.WorkManager import com.eatssu.android.R import com.eatssu.android.databinding.ActivityMainBinding import com.eatssu.android.presentation.base.BaseActivity +import com.eatssu.android.presentation.event.AnyoneButMeEventPopupController +import com.eatssu.android.presentation.event.AnyoneButMeEventTooltipController import com.eatssu.android.presentation.login.LoginActivity import com.eatssu.android.presentation.mypage.MyPageViewModel -import com.eatssu.android.presentation.mypage.terms.WebViewActivity import com.eatssu.android.presentation.mypage.userinfo.UserInfoActivity import com.eatssu.android.presentation.util.showInfoToast import com.eatssu.android.presentation.util.showToast @@ -45,6 +46,12 @@ class MainActivity : BaseActivity( @Inject lateinit var workManager: WorkManager + @Inject + lateinit var anyoneButMeEventPopupController: AnyoneButMeEventPopupController + + @Inject + lateinit var anyoneButMeEventTooltipController: AnyoneButMeEventTooltipController + private val mainViewModel: MainViewModel by viewModels() private val myPageViewModel: MyPageViewModel by viewModels() @@ -54,6 +61,8 @@ class MainActivity : BaseActivity( setupNoToolbar() setNavigation() + bindEventPopup(showOnLaunch = savedInstanceState == null) + bindEventTooltip() checkAlarmPermission() collectState() @@ -79,15 +88,7 @@ class MainActivity : BaseActivity( } R.id.anyone_but_me_menu -> { - startActivity { - putExtra(WebViewActivity.EXTRA_URL, getString(R.string.anyone_but_me_url)) - putExtra(WebViewActivity.EXTRA_TITLE, getString(R.string.nav_anyone_but_me)) - putExtra("SCREEN_ID", ScreenId.ANYONE_BUT_ME_MAIN.name) - putExtra( - WebViewActivity.EXTRA_BACK_ICON_RES_ID, - com.eatssu.design_system.R.drawable.ic_close - ) - } + anyoneButMeEventPopupController.openAnyoneButMePage() false } @@ -103,6 +104,21 @@ class MainActivity : BaseActivity( } } + private fun bindEventPopup(showOnLaunch: Boolean) { + anyoneButMeEventPopupController.bind( + composeView = binding.composeEventPopup, + lifecycleScope = lifecycleScope, + showOnLaunch = showOnLaunch + ) + } + + private fun bindEventTooltip() { + anyoneButMeEventTooltipController.bind( + tooltipComposeView = binding.composeEventTooltip, + bottomNavigationView = binding.bottomNaviBar + ) + } + // set UI -- private fun setupNoToolbar() { // 툴바 사용하지 않도록 설정 @@ -211,6 +227,5 @@ class MainActivity : BaseActivity( } } - override fun shouldLogScreenId() = false } diff --git a/app/src/main/java/com/eatssu/android/presentation/event/AnyoneButMeEventDialog.kt b/app/src/main/java/com/eatssu/android/presentation/event/AnyoneButMeEventDialog.kt new file mode 100644 index 000000000..271e129eb --- /dev/null +++ b/app/src/main/java/com/eatssu/android/presentation/event/AnyoneButMeEventDialog.kt @@ -0,0 +1,176 @@ +package com.eatssu.android.presentation.event + +import androidx.compose.foundation.Image +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.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.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.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.eatssu.android.R +import com.eatssu.design_system.theme.EatssuTheme +import com.eatssu.design_system.theme.White + +private val EventButton = Color(0xFF1F1F1F) + +@Composable +fun AnyoneButMeEventDialog( + onDismiss: () -> Unit, + onDismissForever: () -> Unit, + onInstagramClick: () -> Unit, + onAnyoneButMeClick: () -> Unit, +) { + Dialog( + onDismissRequest = onDismiss, + properties = DialogProperties( + dismissOnBackPress = true, + dismissOnClickOutside = false, + usePlatformDefaultWidth = false + ) + ) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 20.dp), + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp), + verticalArrangement = Arrangement.spacedBy(11.dp) + ) { + Card( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onAnyoneButMeClick), + shape = RoundedCornerShape(20.dp), + colors = CardDefaults.cardColors(containerColor = White) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(640f / 720f), + ) { + Image( + painter = painterResource(R.drawable.ic_event_popup), + contentDescription = null, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Fit + ) + + Box( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = 40.dp) + ) { + InstagramButton(onClick = onInstagramClick) + } + } + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + FooterActionText( + text = "다시 보지 않기", + onClick = onDismissForever + ) + FooterActionText( + text = "닫기", + onClick = onDismiss + ) + } + } + } + } +} + +@Composable +private fun InstagramButton(onClick: () -> Unit) { + Row( + modifier = Modifier + .clip(RoundedCornerShape(999.dp)) + .background(EventButton) + .border(1.dp, White, RoundedCornerShape(999.dp)) + .clickable(onClick = onClick) + .padding(start = 16.dp, end = 8.dp, top = 8.dp, bottom = 8.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "EAT-SSU 인스타그램 바로가기", + color = White, + style = EatssuTheme.typography.body2 + ) + Box( + modifier = Modifier + .size(24.dp) + .background(White, CircleShape), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource(R.drawable.ic_arrow_right), + contentDescription = null, + modifier = Modifier.size(12.dp) + ) + } + } +} + +@Composable +private fun FooterActionText( + text: String, + onClick: () -> Unit, +) { + Text( + text = text, + modifier = Modifier.clickable(onClick = onClick), + color = White, + style = EatssuTheme.typography.body2 + ) +} + +@Preview(showBackground = true) +@Composable +private fun AnyoneButMeEventDialogPreview() { + EatssuTheme { + Box( + modifier = Modifier + .fillMaxWidth() + .height(520.dp) + ) { + AnyoneButMeEventDialog( + onDismiss = {}, + onDismissForever = {}, + onInstagramClick = {}, + onAnyoneButMeClick = {} + ) + } + } +} diff --git a/app/src/main/java/com/eatssu/android/presentation/event/AnyoneButMeEventPopupController.kt b/app/src/main/java/com/eatssu/android/presentation/event/AnyoneButMeEventPopupController.kt new file mode 100644 index 000000000..497be8f69 --- /dev/null +++ b/app/src/main/java/com/eatssu/android/presentation/event/AnyoneButMeEventPopupController.kt @@ -0,0 +1,104 @@ +package com.eatssu.android.presentation.event + +import android.content.Context +import android.content.Intent +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.lifecycle.LifecycleCoroutineScope +import com.eatssu.android.R +import com.eatssu.android.data.local.AppFeatureDataStore +import com.eatssu.android.presentation.mypage.terms.WebViewActivity +import com.eatssu.android.presentation.util.openInBrowser +import com.eatssu.common.enums.ScreenId +import com.eatssu.design_system.theme.EatssuTheme +import dagger.hilt.android.qualifiers.ActivityContext +import dagger.hilt.android.scopes.ActivityScoped +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import javax.inject.Inject + +@ActivityScoped +class AnyoneButMeEventPopupController @Inject constructor( + @ActivityContext private val context: Context, + private val appFeatureDataStore: AppFeatureDataStore, +) { + private lateinit var composeView: ComposeView + private lateinit var lifecycleScope: LifecycleCoroutineScope + private var canAutoShowOnLaunch = false + private var hasHandledLaunchPopup = false + private val isPopupVisible = mutableStateOf(false) + + fun bind( + composeView: ComposeView, + lifecycleScope: LifecycleCoroutineScope, + showOnLaunch: Boolean, + ) { + this.composeView = composeView + this.lifecycleScope = lifecycleScope + canAutoShowOnLaunch = showOnLaunch + setupContent() + observePopupState() + } + + private fun setupContent() { + composeView.setViewCompositionStrategy( + ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed + ) + composeView.setContent { + EatssuTheme { + if (isPopupVisible.value) { + AnyoneButMeEventDialog( + onDismiss = ::hide, + onDismissForever = ::dismissForever, + onInstagramClick = ::openInstagram, + onAnyoneButMeClick = ::openAnyoneButMePage + ) + } + } + } + } + + private fun observePopupState() { + lifecycleScope.launch { + appFeatureDataStore.isAnyoneButMeEventPopupDismissed.collectLatest { dismissed -> + if (canAutoShowOnLaunch && !hasHandledLaunchPopup && !dismissed) { + hasHandledLaunchPopup = true + isPopupVisible.value = true + } + } + } + } + + private fun dismissForever() { + hide() + lifecycleScope.launch { + appFeatureDataStore.setAnyoneButMeEventPopupDismissed(true) + } + } + + fun openAnyoneButMePage() { + hide() + context.startActivity( + Intent(context, WebViewActivity::class.java).apply { + putExtra(WebViewActivity.EXTRA_URL, context.getString(R.string.anyone_but_me_url)) + putExtra(WebViewActivity.EXTRA_TITLE, context.getString(R.string.nav_anyone_but_me)) + putExtra("SCREEN_ID", ScreenId.ANYONE_BUT_ME_MAIN.name) + putExtra( + WebViewActivity.EXTRA_BACK_ICON_RES_ID, + com.eatssu.design_system.R.drawable.ic_close + ) + } + ) + } + + private fun openInstagram() { + hide() + context.openInBrowser(context.getString(R.string.eatssu_event_instagram_url)) + } + + private fun hide() { + canAutoShowOnLaunch = false + isPopupVisible.value = false + } +} diff --git a/app/src/main/java/com/eatssu/android/presentation/event/AnyoneButMeEventTooltip.kt b/app/src/main/java/com/eatssu/android/presentation/event/AnyoneButMeEventTooltip.kt new file mode 100644 index 000000000..9c014e4c0 --- /dev/null +++ b/app/src/main/java/com/eatssu/android/presentation/event/AnyoneButMeEventTooltip.kt @@ -0,0 +1,62 @@ +package com.eatssu.android.presentation.event + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +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.Path +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.eatssu.design_system.theme.EatssuTheme +import com.eatssu.design_system.theme.Primary +import com.eatssu.design_system.theme.White + +@Composable +fun AnyoneButMeEventTooltip( + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "EVENT!", + modifier = Modifier + .background( + color = Primary, + shape = RoundedCornerShape(999.dp) + ) + .padding(horizontal = 12.dp, vertical = 4.dp), + color = White, + style = EatssuTheme.typography.caption2 + ) + + Canvas( + modifier = Modifier.size(width = 12.dp, height = 6.dp) + ) { + drawPath( + path = Path().apply { + moveTo(0f, 0f) + lineTo(size.width / 2f, size.height) + lineTo(size.width, 0f) + close() + }, + color = Primary + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun AnyoneButMeEventTooltipPreview() { + EatssuTheme { + AnyoneButMeEventTooltip() + } +} diff --git a/app/src/main/java/com/eatssu/android/presentation/event/AnyoneButMeEventTooltipController.kt b/app/src/main/java/com/eatssu/android/presentation/event/AnyoneButMeEventTooltipController.kt new file mode 100644 index 000000000..55b9d107d --- /dev/null +++ b/app/src/main/java/com/eatssu/android/presentation/event/AnyoneButMeEventTooltipController.kt @@ -0,0 +1,92 @@ +package com.eatssu.android.presentation.event + +import android.view.View +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.core.view.doOnLayout +import com.eatssu.design_system.theme.EatssuTheme +import com.google.android.material.bottomnavigation.BottomNavigationView +import dagger.hilt.android.scopes.ActivityScoped +import javax.inject.Inject + +@ActivityScoped +class AnyoneButMeEventTooltipController @Inject constructor() { + private companion object { + const val BOTTOM_NAVIGATION_ITEM_COUNT = 4 + const val ANYONE_BUT_ME_MENU_INDEX = 2 + } + + private lateinit var tooltipComposeView: ComposeView + private lateinit var bottomNavigationView: BottomNavigationView + + private val bottomNavigationLayoutChangeListener = View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> + updateTooltipPosition() + } + private val tooltipLayoutChangeListener = View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> + updateTooltipPosition() + } + + fun bind( + tooltipComposeView: ComposeView, + bottomNavigationView: BottomNavigationView, + ) { + clearPreviousBindings() + + this.tooltipComposeView = tooltipComposeView + this.bottomNavigationView = bottomNavigationView + + setupContent() + observeLayout() + } + + private fun clearPreviousBindings() { + if (::bottomNavigationView.isInitialized) { + bottomNavigationView.removeOnLayoutChangeListener(bottomNavigationLayoutChangeListener) + } + + if (::tooltipComposeView.isInitialized) { + tooltipComposeView.removeOnLayoutChangeListener(tooltipLayoutChangeListener) + } + } + + private fun setupContent() { + tooltipComposeView.apply { + isClickable = false + isFocusable = false + importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO + setViewCompositionStrategy( + ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed + ) + setContent { + EatssuTheme { + AnyoneButMeEventTooltip() + } + } + } + } + + private fun observeLayout() { + bottomNavigationView.addOnLayoutChangeListener(bottomNavigationLayoutChangeListener) + tooltipComposeView.addOnLayoutChangeListener(tooltipLayoutChangeListener) + + bottomNavigationView.doOnLayout { updateTooltipPosition() } + tooltipComposeView.doOnLayout { updateTooltipPosition() } + tooltipComposeView.post { updateTooltipPosition() } + } + + private fun updateTooltipPosition() { + if (!::tooltipComposeView.isInitialized || !::bottomNavigationView.isInitialized) return + if (tooltipComposeView.width == 0 || tooltipComposeView.height == 0) return + + val itemWidth = bottomNavigationView.width / BOTTOM_NAVIGATION_ITEM_COUNT.toFloat() + val itemCenterX = itemWidth * ANYONE_BUT_ME_MENU_INDEX + (itemWidth / 2f) + + tooltipComposeView.x = + bottomNavigationView.x + + itemCenterX - + (tooltipComposeView.width / 2f) + tooltipComposeView.y = + bottomNavigationView.y - tooltipComposeView.height.toFloat() + tooltipComposeView.visibility = View.VISIBLE + } +} diff --git a/app/src/main/java/com/eatssu/android/presentation/util/BrowserUtil.kt b/app/src/main/java/com/eatssu/android/presentation/util/BrowserUtil.kt new file mode 100644 index 000000000..c04084e3c --- /dev/null +++ b/app/src/main/java/com/eatssu/android/presentation/util/BrowserUtil.kt @@ -0,0 +1,37 @@ +package com.eatssu.android.presentation.util + +import android.app.Activity +import android.content.Context +import android.content.Intent +import androidx.core.net.toUri + +fun Context.openInBrowser(url: String) { + val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri()).apply { + addCategory(Intent.CATEGORY_BROWSABLE) + if (this@openInBrowser !is Activity) { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + } + + findBrowserPackage(browserIntent)?.let(browserIntent::setPackage) + startActivity(browserIntent) +} + +private fun Context.findBrowserPackage(intent: Intent): String? { + val browserPackages = packageManager.queryIntentActivities( + Intent(Intent.ACTION_VIEW, "https://www.google.com".toUri()).apply { + addCategory(Intent.CATEGORY_BROWSABLE) + }, + 0 + ).map { resolveInfo -> + resolveInfo.activityInfo.packageName + }.toSet() + + return packageManager.queryIntentActivities(intent, 0) + .firstOrNull { resolveInfo -> + resolveInfo.activityInfo.packageName in browserPackages && + resolveInfo.activityInfo.packageName != packageName + } + ?.activityInfo + ?.packageName +} diff --git a/app/src/main/res/drawable/ic_event_popup.png b/app/src/main/res/drawable/ic_event_popup.png new file mode 100644 index 000000000..fe5ef135b Binary files /dev/null and b/app/src/main/res/drawable/ic_event_popup.png differ diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 6072d1ba6..056dc73a7 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -37,4 +37,23 @@ app:itemHorizontalTranslationEnabled="false" app:itemPaddingTop="10dp" app:menu="@menu/menu_bottom_navigation" /> + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5391246c3..5e55c064b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -301,5 +301,7 @@ https://github.com/EAT-SSU/Docs/wiki/EAT%E2%80%90SSU-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%9D%B4%EC%9A%A9%EC%95%BD%EA%B4%80 https://eatssu-coffee.figma.site/ eatssu.official + https://www.instagram.com/eatssu.official/ + https://www.instagram.com/p/DVu1n6SEs5b/ https://eat-ssu.notion.site/1d2eeef75a1681ae800cf6ffa6faa37d?pvs=74