diff --git a/app/src/main/java/com/daedan/festabook/presentation/common/component/URLText.kt b/app/src/main/java/com/daedan/festabook/presentation/common/component/URLText.kt new file mode 100644 index 0000000..f57d577 --- /dev/null +++ b/app/src/main/java/com/daedan/festabook/presentation/common/component/URLText.kt @@ -0,0 +1,112 @@ +package com.daedan.festabook.presentation.common.component + +import android.util.Patterns +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.TextUnit +import com.daedan.festabook.presentation.theme.FestabookColor + +@Composable +fun URLText( + text: String, + modifier: Modifier = Modifier, + color: Color = Color.Unspecified, + fontSize: TextUnit = TextUnit.Unspecified, + fontStyle: FontStyle? = null, + fontWeight: FontWeight? = null, + fontFamily: FontFamily? = null, + letterSpacing: TextUnit = TextUnit.Unspecified, + textDecoration: TextDecoration? = null, + textAlign: TextAlign? = null, + lineHeight: TextUnit = TextUnit.Unspecified, + overflow: TextOverflow = TextOverflow.Clip, + softWrap: Boolean = true, + maxLines: Int = Int.MAX_VALUE, + minLines: Int = 1, + inlineContent: Map = mapOf(), + onTextLayout: (TextLayoutResult) -> Unit = {}, + style: TextStyle = LocalTextStyle.current, +) { + val uriHandler = LocalUriHandler.current + var layoutResult by remember { mutableStateOf(null) } + val linkedText = + buildAnnotatedString { + append(text) + val urlPattern = Patterns.WEB_URL + val matcher = urlPattern.matcher(text) + while (matcher.find()) { + addStyle( + style = + SpanStyle( + color = FestabookColor.gray500, + textDecoration = TextDecoration.Underline, + ), + start = matcher.start(), + end = matcher.end(), + ) + addStringAnnotation( + tag = "URL", + annotation = matcher.group(), + start = matcher.start(), + end = matcher.end(), + ) + } + } + Text( + text = linkedText, + modifier = + modifier.pointerInput(Unit) { + detectTapGestures { + layoutResult?.let { result -> + val position = result.getOffsetForPosition(it) + linkedText + .getStringAnnotations("URL", position, position) + .firstOrNull() + ?.let { annotation -> + uriHandler.openUri(annotation.item) + } + } + } + }, + color = color, + fontSize = fontSize, + fontStyle = fontStyle, + fontWeight = fontWeight, + fontFamily = fontFamily, + letterSpacing = letterSpacing, + textDecoration = textDecoration, + textAlign = textAlign, + lineHeight = lineHeight, + overflow = overflow, + softWrap = softWrap, + maxLines = maxLines, + minLines = minLines, + inlineContent = inlineContent, + onTextLayout = { + layoutResult = it + onTextLayout(it) + }, + style = style, + ) +} diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapViewModel.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapViewModel.kt index 5eebe62..5cefacf 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapViewModel.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapViewModel.kt @@ -62,6 +62,15 @@ class PlaceMapViewModel( private val _selectedPlace: MutableLiveData = MutableLiveData() val selectedPlace: LiveData = _selectedPlace + val selectedPlaceFlow: StateFlow = + _selectedPlace + .asFlow() + .stateIn( + scope = viewModelScope, + started = SharingStarted.Lazily, + initialValue = SelectedPlaceUiState.Loading, + ) + private val _navigateToDetail = SingleLiveData() val navigateToDetail: LiveData = _navigateToDetail diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/PlaceDetailPreviewFragment.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/PlaceDetailPreviewFragment.kt index 6e7e29d..cf57845 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/PlaceDetailPreviewFragment.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/PlaceDetailPreviewFragment.kt @@ -1,26 +1,38 @@ package com.daedan.festabook.presentation.placeMap.placeDetailPreview import android.os.Bundle +import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup import androidx.activity.OnBackPressedCallback +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.daedan.festabook.R import com.daedan.festabook.databinding.FragmentPlaceDetailPreviewBinding import com.daedan.festabook.di.fragment.FragmentKey import com.daedan.festabook.logging.logger import com.daedan.festabook.presentation.common.BaseFragment import com.daedan.festabook.presentation.common.OnMenuItemReClickListener -import com.daedan.festabook.presentation.common.loadImage -import com.daedan.festabook.presentation.common.setFormatDate -import com.daedan.festabook.presentation.common.showBottomAnimation import com.daedan.festabook.presentation.common.showErrorSnackBar import com.daedan.festabook.presentation.placeDetail.PlaceDetailActivity import com.daedan.festabook.presentation.placeDetail.model.PlaceDetailUiModel import com.daedan.festabook.presentation.placeMap.PlaceMapViewModel import com.daedan.festabook.presentation.placeMap.logging.PlacePreviewClick import com.daedan.festabook.presentation.placeMap.model.SelectedPlaceUiState +import com.daedan.festabook.presentation.placeMap.placeDetailPreview.component.PlaceDetailPreviewScreen +import com.daedan.festabook.presentation.theme.FestabookTheme +import com.daedan.festabook.presentation.theme.festabookSpacing import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesIntoMap import dev.zacsweers.metro.Inject @@ -42,13 +54,70 @@ class PlaceDetailPreviewFragment( } } + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + return ComposeView(requireContext()).apply { + super.onCreateView(inflater, container, savedInstanceState) + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + FestabookTheme { + val placeDetailUiState by viewModel.selectedPlaceFlow.collectAsStateWithLifecycle() + val visible = placeDetailUiState is SelectedPlaceUiState.Success + + LaunchedEffect(placeDetailUiState) { + backPressedCallback.isEnabled = true + } + + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.BottomCenter, + ) { + PlaceDetailPreviewScreen( + placeUiState = placeDetailUiState, + visible = visible, + modifier = + Modifier + .padding( + vertical = festabookSpacing.paddingBody4, + horizontal = festabookSpacing.paddingScreenGutter, + ), + onClick = { selectedPlace -> + if (selectedPlace !is SelectedPlaceUiState.Success) return@PlaceDetailPreviewScreen + startPlaceDetailActivity(selectedPlace.value) + binding.logger.log( + PlacePreviewClick( + baseLogData = binding.logger.getBaseLogData(), + placeName = + selectedPlace.value.place.title + ?: "undefined", + timeTag = + viewModel.selectedTimeTag.value?.name + ?: "undefined", + category = selectedPlace.value.place.category.name, + ), + ) + }, + onError = { selectedPlace -> + showErrorSnackBar(selectedPlace.throwable) + }, + onEmpty = { + backPressedCallback.isEnabled = false + }, + ) + } + } + } + } + } + override fun onViewCreated( view: View, savedInstanceState: Bundle?, ) { super.onViewCreated(view, savedInstanceState) - setUpObserver() - setupBinding() setUpBackPressedCallback() } @@ -63,63 +132,6 @@ class PlaceDetailPreviewFragment( ) } - private fun setupBinding() { - binding.layoutSelectedPlace.setOnClickListener { - val selectedPlaceState = viewModel.selectedPlace.value - if (selectedPlaceState is SelectedPlaceUiState.Success) { - startPlaceDetailActivity(selectedPlaceState.value) - binding.logger.log( - PlacePreviewClick( - baseLogData = binding.logger.getBaseLogData(), - placeName = selectedPlaceState.value.place.title ?: "undefined", - timeTag = viewModel.selectedTimeTag.value?.name ?: "undefined", - category = selectedPlaceState.value.place.category.name, - ), - ) - } - } - } - - private fun setUpObserver() { - viewModel.selectedPlace.observe(viewLifecycleOwner) { selectedPlace -> - backPressedCallback.isEnabled = true - binding.layoutSelectedPlace.visibility = - if (selectedPlace == SelectedPlaceUiState.Empty) View.GONE else View.VISIBLE - - when (selectedPlace) { - is SelectedPlaceUiState.Loading -> Unit - is SelectedPlaceUiState.Success -> { - binding.layoutSelectedPlace.showBottomAnimation() - updateSelectedPlaceUi(selectedPlace.value) - } - - is SelectedPlaceUiState.Error -> showErrorSnackBar(selectedPlace.throwable) - is SelectedPlaceUiState.Empty -> backPressedCallback.isEnabled = false - } - } - } - - private fun updateSelectedPlaceUi(selectedPlace: PlaceDetailUiModel) { - with(binding) { - layoutSelectedPlace.visibility = View.VISIBLE - tvSelectedPlaceTitle.text = - selectedPlace.place.title ?: getString(R.string.place_list_default_title) - tvSelectedPlaceLocation.text = - selectedPlace.place.location ?: getString(R.string.place_list_default_location) - setFormatDate( - binding.tvSelectedPlaceTime, - selectedPlace.startTime, - selectedPlace.endTime, - ) - tvSelectedPlaceHost.text = - selectedPlace.host ?: getString(R.string.place_detail_default_host) - tvSelectedPlaceDescription.text = selectedPlace.place.description - ?: getString(R.string.place_list_default_description) - cvPlaceCategory.setCategory(selectedPlace.place.category) - ivSelectedPlaceImage.loadImage(selectedPlace.featuredImage) - } - } - private fun startPlaceDetailActivity(placeDetail: PlaceDetailUiModel) { startActivity(PlaceDetailActivity.newIntent(requireContext(), placeDetail)) } diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/PlaceDetailPreviewSecondaryFragment.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/PlaceDetailPreviewSecondaryFragment.kt index 684f6bd..48edaeb 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/PlaceDetailPreviewSecondaryFragment.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/PlaceDetailPreviewSecondaryFragment.kt @@ -1,26 +1,36 @@ package com.daedan.festabook.presentation.placeMap.placeDetailPreview import android.os.Bundle +import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup import androidx.activity.OnBackPressedCallback +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider -import coil3.load +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.daedan.festabook.R import com.daedan.festabook.databinding.FragmentPlaceDetailPreviewSecondaryBinding +import com.daedan.festabook.di.appGraph import com.daedan.festabook.di.fragment.FragmentKey -import com.daedan.festabook.logging.logger import com.daedan.festabook.presentation.common.BaseFragment import com.daedan.festabook.presentation.common.OnMenuItemReClickListener -import com.daedan.festabook.presentation.common.showBottomAnimation import com.daedan.festabook.presentation.common.showErrorSnackBar -import com.daedan.festabook.presentation.placeDetail.model.PlaceDetailUiModel import com.daedan.festabook.presentation.placeMap.PlaceMapViewModel import com.daedan.festabook.presentation.placeMap.logging.PlacePreviewClick import com.daedan.festabook.presentation.placeMap.model.SelectedPlaceUiState -import com.daedan.festabook.presentation.placeMap.model.getIconId -import com.daedan.festabook.presentation.placeMap.model.getTextId +import com.daedan.festabook.presentation.placeMap.placeDetailPreview.component.PlaceDetailPreviewSecondaryScreen +import com.daedan.festabook.presentation.theme.FestabookTheme +import com.daedan.festabook.presentation.theme.festabookSpacing import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesIntoMap import dev.zacsweers.metro.Inject @@ -43,56 +53,62 @@ class PlaceDetailPreviewSecondaryFragment( } } - override fun onViewCreated( - view: View, + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, savedInstanceState: Bundle?, - ) { - super.onViewCreated(view, savedInstanceState) - setUpObserver() - setUpBackPressedCallback() - } - - override fun onMenuItemReClick() { - viewModel.unselectPlace() - } + ): View { + return ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + val placeDetailUiState by viewModel.selectedPlaceFlow.collectAsStateWithLifecycle() + val visible = placeDetailUiState is SelectedPlaceUiState.Success - private fun setUpBackPressedCallback() { - requireActivity().onBackPressedDispatcher.addCallback( - viewLifecycleOwner, - backPressedCallback, - ) - } - - private fun setUpObserver() { - viewModel.selectedPlace.observe(viewLifecycleOwner) { selectedPlace -> - backPressedCallback.isEnabled = true - when (selectedPlace) { - is SelectedPlaceUiState.Success -> { - binding.layoutSelectedPlace.visibility = View.VISIBLE - binding.layoutSelectedPlace.showBottomAnimation() - updateSelectedPlaceUi(selectedPlace.value) - binding.logger.log( - PlacePreviewClick( - baseLogData = binding.logger.getBaseLogData(), - placeName = selectedPlace.value.place.title ?: "undefined", - timeTag = viewModel.selectedTimeTag.value?.name ?: "undefined", - category = selectedPlace.value.place.category.name, - ), - ) + LaunchedEffect(placeDetailUiState) { + backPressedCallback.isEnabled = true } - is SelectedPlaceUiState.Error -> showErrorSnackBar(selectedPlace.throwable) - is SelectedPlaceUiState.Loading -> Unit - is SelectedPlaceUiState.Empty -> backPressedCallback.isEnabled = false + FestabookTheme { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.BottomCenter, + ) { + PlaceDetailPreviewSecondaryScreen( + visible = visible, + placeUiState = placeDetailUiState, + modifier = + Modifier + .padding( + vertical = festabookSpacing.paddingBody4, + horizontal = festabookSpacing.paddingScreenGutter, + ), + onError = { + showErrorSnackBar(it.throwable) + }, + onEmpty = { + backPressedCallback.isEnabled = false + }, + onClick = { + if (it !is SelectedPlaceUiState.Success) return@PlaceDetailPreviewSecondaryScreen + appGraph.defaultFirebaseLogger.log( + PlacePreviewClick( + baseLogData = appGraph.defaultFirebaseLogger.getBaseLogData(), + placeName = it.value.place.title ?: "undefined", + timeTag = + viewModel.selectedTimeTag.value?.name + ?: "undefined", + category = it.value.place.category.name, + ), + ) + }, + ) + } + } } } } - private fun updateSelectedPlaceUi(selectedPlace: PlaceDetailUiModel) { - with(binding) { - ivSecondaryCategoryItem.load(selectedPlace.place.category.getIconId()) - tvSelectedPlaceTitle.text = - selectedPlace.place.title ?: getString(selectedPlace.place.category.getTextId()) - } + override fun onMenuItemReClick() { + viewModel.unselectPlace() } } diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/component/PlaceDetailPreviewScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/component/PlaceDetailPreviewScreen.kt new file mode 100644 index 0000000..0e4c101 --- /dev/null +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/component/PlaceDetailPreviewScreen.kt @@ -0,0 +1,215 @@ +package com.daedan.festabook.presentation.placeMap.placeDetailPreview.component + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.daedan.festabook.R +import com.daedan.festabook.presentation.common.component.CoilImage +import com.daedan.festabook.presentation.common.component.URLText +import com.daedan.festabook.presentation.common.convertImageUrl +import com.daedan.festabook.presentation.placeDetail.model.PlaceDetailUiModel +import com.daedan.festabook.presentation.placeMap.component.PlaceCategoryLabel +import com.daedan.festabook.presentation.placeMap.model.PlaceCategoryUiModel +import com.daedan.festabook.presentation.placeMap.model.PlaceUiModel +import com.daedan.festabook.presentation.placeMap.model.SelectedPlaceUiState +import com.daedan.festabook.presentation.theme.FestabookColor +import com.daedan.festabook.presentation.theme.FestabookTheme +import com.daedan.festabook.presentation.theme.FestabookTypography +import com.daedan.festabook.presentation.theme.festabookShapes +import com.daedan.festabook.presentation.theme.festabookSpacing + +@Composable +fun PlaceDetailPreviewScreen( + placeUiState: SelectedPlaceUiState, + modifier: Modifier = Modifier, + visible: Boolean = false, + onClick: (SelectedPlaceUiState) -> Unit = {}, + onError: (SelectedPlaceUiState.Error) -> Unit = {}, + onEmpty: () -> Unit = {}, +) { + PreviewAnimatableBox( + visible = visible, + modifier = + modifier + .wrapContentSize() + .clickable { onClick(placeUiState) }, + ) { + when (placeUiState) { + is SelectedPlaceUiState.Loading -> Unit + is SelectedPlaceUiState.Success -> { + PlaceDetailPreviewContent(placeDetail = placeUiState.value) + } + + is SelectedPlaceUiState.Error -> onError(placeUiState) + is SelectedPlaceUiState.Empty -> onEmpty() + } + } +} + +@Composable +private fun PlaceDetailPreviewContent( + placeDetail: PlaceDetailUiModel, + modifier: Modifier = Modifier, +) { + Column( + modifier = + modifier.padding( + horizontal = festabookSpacing.paddingScreenGutter, + vertical = 20.dp, + ), + ) { + PlaceCategoryLabel( + category = placeDetail.place.category, + ) + + Row(modifier = Modifier.wrapContentSize()) { + Column( + modifier = Modifier.weight(1f), + ) { + Text( + modifier = + Modifier + .padding(top = festabookSpacing.paddingBody1), + style = FestabookTypography.displaySmall, + text = + placeDetail.place.title + ?: stringResource(R.string.place_list_default_title), + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + Row( + modifier = Modifier.padding(top = festabookSpacing.paddingBody3), + ) { + Icon( + painter = painterResource(R.drawable.ic_place_detail_clock), + contentDescription = stringResource(R.string.content_description_iv_clock), + ) + + Text( + modifier = Modifier.padding(start = festabookSpacing.paddingBody1), + text = formattedDate(placeDetail.startTime, placeDetail.endTime), + style = FestabookTypography.bodySmall, + color = FestabookColor.gray500, + ) + } + + Row( + modifier = Modifier.padding(top = festabookSpacing.paddingBody1), + ) { + Icon( + painter = painterResource(R.drawable.ic_location), + contentDescription = stringResource(R.string.content_description_iv_location), + ) + + Text( + modifier = Modifier.padding(start = festabookSpacing.paddingBody1), + text = + placeDetail.place.location + ?: stringResource(R.string.place_list_default_location), + style = FestabookTypography.bodySmall, + color = FestabookColor.gray500, + ) + } + + Row( + modifier = Modifier.padding(top = festabookSpacing.paddingBody1), + ) { + Icon( + painter = painterResource(R.drawable.ic_place_detail_host), + contentDescription = stringResource(R.string.content_description_iv_host), + ) + + Text( + modifier = Modifier.padding(start = festabookSpacing.paddingBody1), + text = + placeDetail.host + ?: stringResource(R.string.place_detail_default_host), + style = FestabookTypography.bodySmall, + color = FestabookColor.gray500, + ) + } + } + + CoilImage( + modifier = + Modifier + .size(88.dp) + .clip(festabookShapes.radius2), + url = placeDetail.place.imageUrl.convertImageUrl() ?: "", + contentDescription = stringResource(R.string.content_description_booth_image), + ) + } + + URLText( + modifier = Modifier.padding(top = festabookSpacing.paddingBody3), + text = + placeDetail.place.description + ?: stringResource(R.string.place_list_default_description), + style = FestabookTypography.bodySmall, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } +} + +@Composable +private fun formattedDate( + startTime: String?, + endTime: String?, +): String = + if (startTime == null && endTime == null) { + stringResource(R.string.place_detail_default_time) + } else { + listOf(startTime, endTime).joinToString(" ~ ") + } + +@Preview +@Composable +private fun PlaceDetailPreviewScreenPreview() { + FestabookTheme { + PlaceDetailPreviewScreen( + modifier = + Modifier + .padding(festabookSpacing.paddingScreenGutter), + placeUiState = + SelectedPlaceUiState.Success( + value = FAKE_PLACE_DETAIL, + ), + ) + } +} + +private val FAKE_PLACE = + PlaceUiModel( + id = 1, + imageUrl = null, + category = PlaceCategoryUiModel.FOOD_TRUCK, + title = "테스트테스트테스트테스트테스트테스트테스트테스트테스트테스트테스트테스트테스트", + description = "https://onlyfor-me-blog.tistory.com/1190", + location = null, + isBookmarked = false, + timeTagId = listOf(1), + ) + +private val FAKE_PLACE_DETAIL = + PlaceDetailUiModel( + place = FAKE_PLACE, + notices = listOf(), + host = null, + startTime = null, + endTime = null, + images = listOf(), + ) diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/component/PlaceDetailPreviewSecondaryScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/component/PlaceDetailPreviewSecondaryScreen.kt new file mode 100644 index 0000000..f6a3ba2 --- /dev/null +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/component/PlaceDetailPreviewSecondaryScreen.kt @@ -0,0 +1,124 @@ +package com.daedan.festabook.presentation.placeMap.placeDetailPreview.component + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +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.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.daedan.festabook.R +import com.daedan.festabook.presentation.placeDetail.model.PlaceDetailUiModel +import com.daedan.festabook.presentation.placeMap.model.PlaceCategoryUiModel +import com.daedan.festabook.presentation.placeMap.model.PlaceUiModel +import com.daedan.festabook.presentation.placeMap.model.SelectedPlaceUiState +import com.daedan.festabook.presentation.placeMap.model.getIconId +import com.daedan.festabook.presentation.placeMap.model.getTextId +import com.daedan.festabook.presentation.theme.FestabookTheme +import com.daedan.festabook.presentation.theme.FestabookTypography +import com.daedan.festabook.presentation.theme.festabookShapes +import com.daedan.festabook.presentation.theme.festabookSpacing + +@Composable +fun PlaceDetailPreviewSecondaryScreen( + placeUiState: SelectedPlaceUiState, + modifier: Modifier = Modifier, + onError: (SelectedPlaceUiState.Error) -> Unit = {}, + onEmpty: () -> Unit = {}, + onClick: (SelectedPlaceUiState) -> Unit = {}, + visible: Boolean = false, +) { + PreviewAnimatableBox( + visible = visible, + modifier = + modifier + .fillMaxWidth() + .clickable { + onClick(placeUiState) + }, + shape = festabookShapes.radius2, + ) { + when (placeUiState) { + is SelectedPlaceUiState.Loading -> Unit + is SelectedPlaceUiState.Error -> onError(placeUiState) + is SelectedPlaceUiState.Empty -> onEmpty() + is SelectedPlaceUiState.Success -> { + Row( + modifier = + Modifier.padding( + horizontal = festabookSpacing.paddingBody4, + vertical = festabookSpacing.paddingBody3, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier.size(24.dp), + painter = + painterResource( + placeUiState.value.place.category + .getIconId(), + ), + tint = Color.Unspecified, + contentDescription = stringResource(R.string.content_description_iv_category_marker), + ) + + Text( + modifier = Modifier.padding(start = festabookSpacing.paddingBody2), + text = + placeUiState.value.place.title + ?: stringResource( + placeUiState.value.place.category + .getTextId(), + ), + style = FestabookTypography.displaySmall, + ) + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PlaceDetailPreviewSecondaryScreenPreview() { + FestabookTheme { + PlaceDetailPreviewSecondaryScreen( + visible = true, + modifier = Modifier.padding(horizontal = festabookSpacing.paddingScreenGutter), + placeUiState = + SelectedPlaceUiState.Success( + FAKE_PLACE_DETAIL, + ), + ) + } +} + +private val FAKE_PLACE = + PlaceUiModel( + id = 1, + imageUrl = null, + category = PlaceCategoryUiModel.TOILET, + title = "테스트테스", + description = "https://onlyfor-me-blog.tistory.com/1190", + location = null, + isBookmarked = false, + timeTagId = listOf(1), + ) + +private val FAKE_PLACE_DETAIL = + PlaceDetailUiModel( + place = FAKE_PLACE, + notices = listOf(), + host = null, + startTime = null, + endTime = null, + images = listOf(), + ) diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/component/PreviewAnimatableBox.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/component/PreviewAnimatableBox.kt new file mode 100644 index 0000000..0d478d9 --- /dev/null +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/component/PreviewAnimatableBox.kt @@ -0,0 +1,67 @@ +package com.daedan.festabook.presentation.placeMap.placeDetailPreview.component + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.daedan.festabook.presentation.common.component.cardBackground +import com.daedan.festabook.presentation.theme.FestabookColor +import com.daedan.festabook.presentation.theme.festabookShapes +import kotlinx.coroutines.launch + +@Composable +fun PreviewAnimatableBox( + visible: Boolean, + modifier: Modifier = Modifier, + backgroundColor: Color = FestabookColor.white, + borderColor: Color = FestabookColor.gray200, + shape: Shape = festabookShapes.radius5, + borderStroke: Dp = 1.dp, + content: @Composable BoxScope.() -> Unit = {}, +) { + val offsetY = remember { Animatable(120f) } + val alpha = remember { Animatable(0.3f) } + + LaunchedEffect(visible) { + if (visible) { + launch { + offsetY.animateTo( + targetValue = 0f, + animationSpec = tween(300), + ) + } + launch { + alpha.animateTo(1f, animationSpec = tween(300)) + } + } else { + // 나갈 때 애니메이션 (위에서 아래로 + 페이드아웃) + launch { offsetY.snapTo(120f) } + launch { alpha.snapTo(0.3f) } + } + } + + Box( + modifier = + modifier + .graphicsLayer { + translationY = offsetY.value + this.alpha = alpha.value + }.cardBackground( + backgroundColor = backgroundColor, + borderColor = borderColor, + shape = shape, + borderStroke = borderStroke, + ), + ) { + content() + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cca637b..398b9f6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -91,7 +91,10 @@ 공지 아이콘 고정핀 아이콘 위치 아이콘 + 카테고리 아이콘 부스 이미지 + 운영 시간 아이콘 + 호스트 아이콘 공지사항이 없습니다 새로고침 플로팅 지도 버튼