Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<String, InlineTextContent> = mapOf(),
onTextLayout: (TextLayoutResult) -> Unit = {},
style: TextStyle = LocalTextStyle.current,
) {
val uriHandler = LocalUriHandler.current
var layoutResult by remember { mutableStateOf<TextLayoutResult?>(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,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,15 @@ class PlaceMapViewModel(
private val _selectedPlace: MutableLiveData<SelectedPlaceUiState> = MutableLiveData()
val selectedPlace: LiveData<SelectedPlaceUiState> = _selectedPlace

val selectedPlaceFlow: StateFlow<SelectedPlaceUiState> =
_selectedPlace
.asFlow()
.stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,
initialValue = SelectedPlaceUiState.Loading,
)

private val _navigateToDetail = SingleLiveData<PlaceDetailUiModel>()
val navigateToDetail: LiveData<PlaceDetailUiModel> = _navigateToDetail

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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,
),
Comment on lines +70 to +86
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요 작업들을 컴포저블이 아닌 Fragment에서 하신 이유가 궁금합니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LaunchedEffect(placeDetailUiState) {
    backPressedCallback.isEnabled = true
}

이 부분 같은 경우는 backPressedCallback이 액티비티에 종속적이기 떄문에
컴포저블에서 수행하면 람다를 넘겨줘서 처리해줘야 합니다.
저는 이 부분이 불필요한 람다라고 생각이 되어서 프래그먼트에서 처리해 주었는데요.

다시 찾아보니까 BackHandler 라는것이 있어 더 간편하게 구현할 수 있더라구요!
반영했습니다 !

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제 마이그레이션 코드에도 error 상태 처리가 아직 안되어 있는데 나중에 공통 컴포저블 ErrorSnackBar를 만들어서 같이 사용하면 좋을 것 같네요~

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

일단 점진적 마이그레이션이기 때문에 기존의 에러 상태 처리 로직을 구현했습니다 !
ErrorSnackBar가 만들어진다면 즉시 반영할게요 !

},
onEmpty = {
backPressedCallback.isEnabled = false
},
)
}
}
}
}
}

override fun onViewCreated(
view: View,
savedInstanceState: Bundle?,
) {
super.onViewCreated(view, savedInstanceState)
setUpObserver()
setupBinding()
setUpBackPressedCallback()
}

Expand All @@ -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))
}
Expand Down
Loading
Loading