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
10 changes: 10 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,15 @@
<meta-data
android:name="com.kakao.sdk.AppKey"
android:value="${KAKAO_NATIVE_APP_KEY}" />

<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ class MainActivity : AppCompatActivity(), BottomNavigationController {
com.project200.undabang.feature.matching.R.id.exercisePlaceFragment,
com.project200.undabang.feature.matching.R.id.exercisePlaceSearchFragment,
com.project200.undabang.feature.matching.R.id.exercisePlaceRegisterFragment,
com.project200.undabang.feature.exercise.R.id.exerciseShareEditFragment,
com.project200.undabang.feature.matching.R.id.matchingGuideFragment,
com.project200.undabang.feature.chatting.R.id.chattingRoomFragment,
// ... 필요한 다른 프래그먼트 ID들 추가 ... //
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/res/xml/file_paths.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path
name="share"
path="share/" />
</paths>
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import com.project200.domain.usecase.ExitChatRoomUseCase
import com.project200.domain.usecase.GetChatMessagesUseCase
import com.project200.domain.usecase.GetNewChatMessagesUseCase
import com.project200.domain.usecase.ObserveOpponentStatusUseCase
import com.project200.domain.usecase.ObserveSocketErrorsUseCase
import com.project200.domain.usecase.ObserveSocketMessagesUseCase
import com.project200.domain.usecase.SendSocketMessageUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import com.project200.presentation.base.BaseAlertDialog
import com.project200.presentation.base.BindingFragment
import com.project200.presentation.utils.UiState
import com.project200.presentation.utils.mapFailureToString
import com.project200.presentation.view.MenuBottomSheetDialog
import com.project200.undabang.feature.exercise.R
import com.project200.undabang.feature.exercise.databinding.FragmentExerciseDetailBinding
import dagger.hilt.android.AndroidEntryPoint
Expand All @@ -26,6 +25,7 @@ import kotlinx.coroutines.launch
class ExerciseDetailFragment : BindingFragment<FragmentExerciseDetailBinding>(R.layout.fragment_exercise_detail) {
private val viewModel: ExerciseDetailViewModel by viewModels()
private val args: ExerciseDetailFragmentArgs by navArgs()
private var currentRecord: ExerciseRecord? = null

override fun getViewBinding(view: View): FragmentExerciseDetailBinding {
return FragmentExerciseDetailBinding.bind(view)
Expand All @@ -36,6 +36,11 @@ class ExerciseDetailFragment : BindingFragment<FragmentExerciseDetailBinding>(R.
binding.baseToolbar.apply {
setTitle(getString(R.string.exercise_detail))
showBackButton(true) { findNavController().navigateUp() }
setSecondarySubButton(R.drawable.ic_share) {
findNavController().navigate(
ExerciseDetailFragmentDirections.actionExerciseDetailFragmentToExerciseShareEditFragment(args.recordId),
)
}
setSubButton(R.drawable.ic_menu) { showExerciseDetailMenu() }
}
}
Expand All @@ -53,6 +58,7 @@ class ExerciseDetailFragment : BindingFragment<FragmentExerciseDetailBinding>(R.
}
is UiState.Success -> {
binding.shimmerLayout.stopShimmer()
currentRecord = state.data
bindExerciseRecordData(state.data)
}
is UiState.Error -> {
Expand Down Expand Up @@ -138,15 +144,15 @@ class ExerciseDetailFragment : BindingFragment<FragmentExerciseDetailBinding>(R.
}

private fun showExerciseDetailMenu() {
MenuBottomSheetDialog(
ExerciseMenuBottomSheetDialog(
onEditClicked = {
findNavController().navigate(
ExerciseDetailFragmentDirections
.actionExerciseDetailFragmentToExerciseFormFragment(args.recordId),
)
},
onDeleteClicked = { showDeleteConfirmationDialog() },
).show(parentFragmentManager, MenuBottomSheetDialog::class.java.simpleName)
).show(parentFragmentManager, ExerciseMenuBottomSheetDialog::class.java.simpleName)
}

private fun showDeleteConfirmationDialog() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.project200.feature.exercise.detail

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.project200.undabang.feature.exercise.databinding.BottomSheetExerciseMenuBinding

class ExerciseMenuBottomSheetDialog(
private val onEditClicked: () -> Unit,
private val onDeleteClicked: () -> Unit,
) : BottomSheetDialogFragment() {
private var _binding: BottomSheetExerciseMenuBinding? = null
val binding get() = _binding!!

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NORMAL, com.project200.undabang.presentation.R.style.CustomBottomSheetDialogTheme)
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
_binding = BottomSheetExerciseMenuBinding.inflate(inflater, container, false)
return binding.root
}

override fun onViewCreated(
view: View,
savedInstanceState: Bundle?,
) {
super.onViewCreated(view, savedInstanceState)

binding.editBtn.setOnClickListener {
onEditClicked()
dismiss()
}

binding.deleteBtn.setOnClickListener {
onDeleteClicked()
dismiss()
}

binding.closeBtn.setOnClickListener { dismiss() }
}

override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package com.project200.feature.exercise.share

import android.graphics.Bitmap
import android.view.View
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.bumptech.glide.Glide
import com.project200.domain.model.ExerciseRecord
import com.project200.feature.exercise.utils.ExerciseRecordStickerGenerator
import com.project200.feature.exercise.utils.ExerciseShareHelper
import com.project200.feature.exercise.utils.ShareEventData
import com.project200.presentation.base.BindingFragment
import com.project200.undabang.feature.exercise.R
import com.project200.undabang.feature.exercise.databinding.FragmentExerciseShareEditBinding
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch

@AndroidEntryPoint
class ExerciseShareEditFragment : BindingFragment<FragmentExerciseShareEditBinding>(R.layout.fragment_exercise_share_edit) {
private val viewModel: ExerciseShareEditViewModel by viewModels()
private val args: ExerciseShareEditFragmentArgs by navArgs()

private var currentStickerBitmap: Bitmap? = null

override fun getViewBinding(view: View): FragmentExerciseShareEditBinding {
return FragmentExerciseShareEditBinding.bind(view)
}

override fun setupViews() {
viewModel.loadExerciseRecord(args.recordId)

binding.themeDarkBtn.setOnClickListener {
viewModel.selectTheme(StickerTheme.DARK)
}
binding.themeLightBtn.setOnClickListener {
viewModel.selectTheme(StickerTheme.LIGHT)
}
binding.themeMinimalBtn.setOnClickListener {
viewModel.selectTheme(StickerTheme.MINIMAL)
}

binding.cancelBtn.setOnClickListener {
findNavController().navigateUp()
}
binding.shareBtn.setOnClickListener {
viewModel.requestShare(binding.stickerPreview.getTransformInfo())
}
}

override fun setupObservers() {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.backgroundImageUrl.collect { url ->
url?.let { loadBackgroundImage(it) }
}
}

launch {
viewModel.selectedTheme.collect { theme ->
updateThemeButtonSelection(theme)
}
}

launch {
viewModel.stickerState.collect { state ->
updateStickerPreview(state.record, state.theme)
}
}

launch {
viewModel.isLoading.collect { isLoading ->
binding.loadingOverlay.visibility = if (isLoading) View.VISIBLE else View.GONE
}
}

launch {
viewModel.shareEvent.collect { data ->
shareImage(data)
}
}
}
}
}

private fun loadBackgroundImage(url: String) {
Glide.with(this)
.load(url)
.centerCrop()
.into(binding.backgroundImage)
}

private fun updateThemeButtonSelection(theme: StickerTheme) {
val selectedAlpha = 1.0f
val unselectedAlpha = 0.4f

binding.themeDarkBtn.alpha = if (theme == StickerTheme.DARK) selectedAlpha else unselectedAlpha
binding.themeLightBtn.alpha = if (theme == StickerTheme.LIGHT) selectedAlpha else unselectedAlpha
binding.themeMinimalBtn.alpha = if (theme == StickerTheme.MINIMAL) selectedAlpha else unselectedAlpha
}

private fun updateStickerPreview(
record: ExerciseRecord,
theme: StickerTheme,
) {
val currentTransform = binding.stickerPreview.getTransformInfo()
val hasUserTransform = binding.stickerPreview.hasUserInteracted()

viewLifecycleOwner.lifecycleScope.launch {
currentStickerBitmap?.recycle()
currentStickerBitmap =
ExerciseRecordStickerGenerator.generateStickerBitmap(
requireContext(),
record,
theme,
)

if (hasUserTransform) {
binding.stickerPreview.setPendingTransform(currentTransform)
}
binding.stickerPreview.setImageBitmap(currentStickerBitmap)
}
}

private fun shareImage(data: ShareEventData) {
viewLifecycleOwner.lifecycleScope.launch {
try {
ExerciseShareHelper.shareExerciseRecord(
requireContext(),
data.record,
data.theme,
data.transformInfo,
)
} finally {
viewModel.onShareCompleted()
}
}
}

override fun onDestroyView() {
currentStickerBitmap?.recycle()
currentStickerBitmap = null
super.onDestroyView()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.project200.feature.exercise.share

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.project200.domain.model.BaseResult
import com.project200.domain.model.ExerciseRecord
import com.project200.domain.usecase.GetExerciseRecordDetailUseCase
import com.project200.feature.exercise.utils.ShareEventData
import com.project200.feature.exercise.utils.StickerState
import com.project200.feature.exercise.utils.StickerTransformInfo
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class ExerciseShareEditViewModel
@Inject
constructor(
private val getExerciseRecordDetailUseCase: GetExerciseRecordDetailUseCase,
) : ViewModel() {
private val _selectedTheme = MutableStateFlow(StickerTheme.DARK)
val selectedTheme: StateFlow<StickerTheme> = _selectedTheme.asStateFlow()

private val _exerciseRecord = MutableStateFlow<ExerciseRecord?>(null)
val exerciseRecord: StateFlow<ExerciseRecord?> = _exerciseRecord.asStateFlow()

private val _backgroundImageUrl = MutableStateFlow<String?>(null)
val backgroundImageUrl: StateFlow<String?> = _backgroundImageUrl.asStateFlow()

private val _isLoading = MutableStateFlow(true)
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()

private val _shareEvent = MutableSharedFlow<ShareEventData>()
val shareEvent: SharedFlow<ShareEventData> = _shareEvent.asSharedFlow()

val stickerState: Flow<StickerState> =
_exerciseRecord
.filterNotNull()
.combine(_selectedTheme) { record, theme -> StickerState(record, theme) }

fun loadExerciseRecord(recordId: Long) {
viewModelScope.launch {
_isLoading.value = true
when (val result = getExerciseRecordDetailUseCase(recordId)) {
is BaseResult.Success -> {
_exerciseRecord.value = result.data
_backgroundImageUrl.value = result.data.pictures?.firstOrNull()?.url
}
is BaseResult.Error -> {
}
}
_isLoading.value = false
}
}

fun selectTheme(theme: StickerTheme) {
_selectedTheme.value = theme
}

fun requestShare(transformInfo: StickerTransformInfo) {
val record = _exerciseRecord.value ?: return
viewModelScope.launch {
_isLoading.value = true
_shareEvent.emit(ShareEventData(record, _selectedTheme.value, transformInfo))
}
}

fun onShareCompleted() {
_isLoading.value = false
}
}
Loading