fix(이미지 선택): 이미지 선택 및 크롭 로직 수정

This commit is contained in:
2025-09-18 00:17:20 +09:00
parent 02155065f7
commit d22907c7d5
16 changed files with 397 additions and 571 deletions

View File

@@ -2,16 +2,18 @@ package kr.co.vividnext.sodalive.audio_content.modify
import android.Manifest
import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.core.view.setPadding
import coil.load
import coil.transform.RoundedCornersTransformation
import com.github.dhaval2404.imagepicker.ImagePicker
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestOptions
import com.gun0912.tedpermission.PermissionListener
import com.gun0912.tedpermission.normal.TedPermission
import com.jakewharton.rxbinding4.widget.textChanges
@@ -20,6 +22,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.ImagePickerCropper
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.RealPathUtil
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentModifyBinding
@@ -33,36 +36,7 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
private val viewModel: AudioContentModifyViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private val imageResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
val resultCode = result.resultCode
val data = result.data
if (resultCode == RESULT_OK) {
val fileUri = data?.data
if (fileUri != null) {
binding.ivCover.setPadding(0)
binding.ivCover.background = null
binding.ivCover.load(fileUri) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(13.3f.dpToPx()))
}
viewModel.coverImageUri = fileUri
} else {
Toast.makeText(
this,
"잘못된 파일입니다.\n다시 선택해 주세요.",
Toast.LENGTH_SHORT
).show()
}
} else if (resultCode == ImagePicker.RESULT_ERROR) {
Toast.makeText(this, ImagePicker.getError(data), Toast.LENGTH_SHORT).show()
}
}
private lateinit var cropper: ImagePickerCropper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -82,24 +56,50 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
viewModel.getAudioContentDetail(audioContentId = audioContentId) { finish() }
}
override fun onDestroy() {
cropper.cleanup()
super.onDestroy()
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
cropper = ImagePickerCropper(
caller = this,
context = this,
excludeGif = true,
isEnabledFreeStyleCrop = true,
config = ImagePickerCropper.Config(
aspectX = 1f, aspectY = 1f,
compressFormat = Bitmap.CompressFormat.JPEG,
compressQuality = 90
),
onSuccess = { file, uri ->
binding.ivCover.setPadding(0)
binding.ivCover.background = null
Glide.with(this)
.load(uri)
.placeholder(R.drawable.ic_place_holder)
.apply(
RequestOptions().transform(
RoundedCorners(
13.3f.dpToPx().toInt()
)
)
)
.into(binding.ivCover)
viewModel.coverImageFile = file
},
onError = { e ->
Toast.makeText(this, "${e.message}", Toast.LENGTH_SHORT).show()
}
)
binding.toolbar.tvBack.text = "콘텐츠 수정"
binding.toolbar.tvBack.setOnClickListener { finish() }
binding.ivPhotoPicker.setOnClickListener {
ImagePicker.with(this)
.crop()
.galleryOnly()
.galleryMimeTypes( // Exclude gif images
mimeTypes = arrayOf(
"image/png",
"image/jpg",
"image/jpeg"
)
)
.createIntent { imageResult.launch(it) }
}
binding.ivPhotoPicker.setOnClickListener { cropper.launch() }
binding.llAvailablePoint.setOnClickListener { viewModel.setAvailablePoint(true) }
binding.llNotAvailablePoint.setOnClickListener { viewModel.setAvailablePoint(false) }
@@ -239,8 +239,8 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
viewModel.setAdult(true)
}
viewModel.isAdultLiveData.observe(this) {
if (it) {
viewModel.isAdultLiveData.observe(this) { isAdult ->
if (isAdult) {
binding.ivAgeAll.visibility = View.GONE
binding.llAgeAll.setBackgroundResource(
R.drawable.bg_round_corner_6_7_13181b

View File

@@ -68,7 +68,7 @@ class AudioContentModifyViewModel(
var title: String? = null
var detail: String? = null
var tags: String? = null
var coverImageUri: Uri? = null
var coverImageFile: File? = null
var isPointAvailable: Boolean? = null
fun setAdult(isAdult: Boolean) {
@@ -154,8 +154,8 @@ class AudioContentModifyViewModel(
val requestJson = Gson().toJson(request)
val coverImage = if (coverImageUri != null) {
val file = File(getRealPathFromURI(coverImageUri!!))
val coverImage = if (coverImageFile != null) {
val file = coverImageFile!!
MultipartBody.Part.createFormData(
"coverImage",
file.name,

View File

@@ -5,6 +5,7 @@ import android.annotation.SuppressLint
import android.app.DatePickerDialog
import android.app.TimePickerDialog
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import android.os.Bundle
@@ -15,8 +16,9 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import coil.load
import coil.transform.CircleCropTransformation
import coil.transform.RoundedCornersTransformation
import com.github.dhaval2404.imagepicker.ImagePicker
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestOptions
import com.gun0912.tedpermission.PermissionListener
import com.gun0912.tedpermission.normal.TedPermission
import com.jakewharton.rxbinding4.widget.textChanges
@@ -26,6 +28,7 @@ import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.PurchaseOption
import kr.co.vividnext.sodalive.audio_content.upload.theme.AudioContentThemeFragment
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.ImagePickerCropper
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.RealPathUtil
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
@@ -48,6 +51,7 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
private val viewModel: AudioContentUploadViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var cropper: ImagePickerCropper
private val themeFragment: AudioContentThemeFragment by lazy {
AudioContentThemeFragment(
@@ -66,35 +70,6 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
)
}
private val imageResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
val resultCode = result.resultCode
val data = result.data
if (resultCode == RESULT_OK) {
val fileUri = data?.data
if (fileUri != null) {
binding.ivCover.background = null
binding.ivCover.load(fileUri) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(13.3f.dpToPx()))
}
viewModel.coverImageUri = fileUri
} else {
Toast.makeText(
this,
"잘못된 파일입니다.\n다시 선택해 주세요.",
Toast.LENGTH_SHORT
).show()
}
} else if (resultCode == ImagePicker.RESULT_ERROR) {
Toast.makeText(this, ImagePicker.getError(data), Toast.LENGTH_SHORT).show()
}
}
private val selectAudioActivityResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
@@ -113,18 +88,29 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
Toast.LENGTH_SHORT
).show()
}
} else if (resultCode == ImagePicker.RESULT_ERROR) {
} else {
binding.tvSelectContent.text = "파일 선택"
viewModel.contentUri = null
Toast.makeText(this, ImagePicker.getError(data), Toast.LENGTH_SHORT).show()
Toast.makeText(
this,
"파일 선택을 실패했습니다.\n다시 시도해 주세요.",
Toast.LENGTH_SHORT
).show()
}
}
private val datePickerDialogListener =
DatePickerDialog.OnDateSetListener { _, year, monthOfYear, dayOfMonth ->
viewModel.releaseDate = String.format("%d-%02d-%02d", year, monthOfYear + 1, dayOfMonth)
viewModel.releaseDate = String.format(
Locale.getDefault(),
"%d-%02d-%02d",
year,
monthOfYear + 1,
dayOfMonth
)
viewModel.setReservationDate(
String.format(
Locale.getDefault(),
"%d.%02d.%02d",
year,
monthOfYear + 1,
@@ -135,7 +121,7 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
private val timePickerDialogListener =
TimePickerDialog.OnTimeSetListener { _, hourOfDay, minute ->
val timeString = String.format("%02d:%02d", hourOfDay, minute)
val timeString = String.format(Locale.getDefault(), "%02d:%02d", hourOfDay, minute)
viewModel.releaseTime = timeString
viewModel.setReservationTime(timeString.convertDateFormat("HH:mm", "a hh:mm"))
}
@@ -151,9 +137,45 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
bindData()
}
override fun onDestroy() {
cropper.cleanup()
super.onDestroy()
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
cropper = ImagePickerCropper(
caller = this,
context = this,
excludeGif = true,
isEnabledFreeStyleCrop = true,
config = ImagePickerCropper.Config(
aspectX = 1f, aspectY = 1f,
compressFormat = Bitmap.CompressFormat.JPEG,
compressQuality = 90
),
onSuccess = { file, uri ->
binding.ivCover.background = null
Glide.with(this)
.load(uri)
.placeholder(R.drawable.ic_place_holder)
.apply(
RequestOptions().transform(
RoundedCorners(
13.3f.dpToPx().toInt()
)
)
)
.into(binding.ivCover)
viewModel.coverImageFile = file
},
onError = { e ->
Toast.makeText(this, "${e.message}", Toast.LENGTH_SHORT).show()
}
)
binding.tvServiceDate.text = if (SharedPreferenceManager.userId == 17958L) {
"※ 이용기간 : 대여(5일) | 소장(이용 기간 1년)"
} else {
@@ -167,19 +189,7 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
themeFragment.show(supportFragmentManager, themeFragment.tag)
}
binding.ivPhotoPicker.setOnClickListener {
ImagePicker.with(this)
.crop()
.galleryOnly()
.galleryMimeTypes( // Exclude gif images
mimeTypes = arrayOf(
"image/png",
"image/jpg",
"image/jpeg"
)
)
.createIntent { imageResult.launch(it) }
}
binding.ivPhotoPicker.setOnClickListener { cropper.launch() }
binding.tvSelectContent.setOnClickListener {
val intent = Intent().apply {
@@ -359,7 +369,7 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
.subscribe {
val price = it.toString().toIntOrNull()
if (price != null) {
viewModel.price = price.toInt()
viewModel.price = price
} else {
viewModel.price = 0
if (it.isNotBlank()) {
@@ -377,7 +387,7 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
.subscribe {
val limited = it.toString().toIntOrNull()
if (limited != null) {
viewModel.limited = limited.toInt()
viewModel.limited = limited
} else {
viewModel.limited = 0
if (it.isNotBlank()) {

View File

@@ -89,7 +89,7 @@ class AudioContentUploadViewModel(
var releaseDate = ""
var releaseTime = ""
var theme: GetAudioContentThemeResponse? = null
var coverImageUri: Uri? = null
var coverImageFile: File? = null
var contentUri: Uri? = null
var previewStartTime: String? = null
var previewEndTime: String? = null
@@ -203,8 +203,8 @@ class AudioContentUploadViewModel(
val requestJson = Gson().toJson(request)
val coverImage = if (coverImageUri != null) {
val file = File(getRealPathFromURI(coverImageUri!!))
val coverImage = if (coverImageFile != null) {
val file = coverImageFile!!
MultipartBody.Part.createFormData(
"coverImage",
file.name,
@@ -323,7 +323,7 @@ class AudioContentUploadViewModel(
return false
}
if (coverImageUri == null) {
if (coverImageFile == null) {
_toastLiveData.postValue("커버이미지를 선택해 주세요.")
return false
}
@@ -413,7 +413,7 @@ class AudioContentUploadViewModel(
// Check if the time difference is greater than 30 seconds (30000 milliseconds)
return date2.time - date1.time
} catch (e: Exception) {
} catch (_: Exception) {
// Handle invalid time formats or parsing errors
return 0
}

View File

@@ -4,6 +4,7 @@ import android.app.Activity
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import android.provider.OpenableColumns
import androidx.activity.result.ActivityResultCaller
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.PickVisualMediaRequest
@@ -13,6 +14,7 @@ import com.yalantis.ucrop.UCrop
import com.yalantis.ucrop.UCropActivity
import kr.co.vividnext.sodalive.BuildConfig
import java.io.File
import java.io.FileOutputStream
/**
* 단일 이미지 선택(13+ Photo Picker / 12- GetContent) → uCrop(기본 9:20) → [File, Uri] 반환
@@ -20,8 +22,10 @@ import java.io.File
* - 결과 파일은 cacheDir에 임시 생성 → 업로드 후 cleanup() 호출로 삭제
*/
class ImagePickerCropper(
private val caller: ActivityResultCaller,
caller: ActivityResultCaller,
private val context: Context,
private val excludeGif: Boolean = false,
private val isEnabledFreeStyleCrop: Boolean = false,
private val config: Config = Config(),
private val onSuccess: (file: File, uri: Uri) -> Unit,
private val onError: (Throwable) -> Unit = { it.printStackTrace() }
@@ -41,15 +45,21 @@ class ImagePickerCropper(
// 13+ : 시스템 Photo Picker
private val pickPhoto: ActivityResultLauncher<PickVisualMediaRequest> =
caller.registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri ->
if (uri == null) onError(CancellationException("User cancelled picking."))
else startCrop(uri)
if (uri == null) onError(CancellationException("이미지 선택을 취소했습니다."))
else handlePickedUri(uri)
}
// 12- : SAF GetContent
private val pickContent: ActivityResultLauncher<String> =
caller.registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
if (uri == null) onError(CancellationException("User cancelled picking."))
else startCrop(uri)
if (uri == null) onError(CancellationException("이미지 선택을 취소했습니다."))
else handlePickedUri(uri)
}
private val openDocument =
caller.registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
if (uri == null) onError(CancellationException("이미지 선택을 취소했습니다."))
else handlePickedUri(uri)
}
// uCrop 결과 수신
@@ -62,38 +72,98 @@ class ImagePickerCropper(
if (out != null && file != null && file.exists()) {
onSuccess(file, out)
} else {
onError(IllegalStateException("Crop finished but no output file/uri"))
onError(IllegalStateException("이미지 크롭을 실패했습니다.\n다시 시도해 주세요"))
}
}
UCrop.RESULT_ERROR -> onError(
UCrop.getError(result.data!!) ?: RuntimeException("Crop error")
UCrop.getError(result.data!!)
?: RuntimeException("이미지 크롭을 실패했습니다.\n다시 시도해 주세요")
)
else -> onError(CancellationException("User cancelled cropping."))
else -> onError(CancellationException("이미지 크롭을 취소했습니다."))
}
}
/** 외부에서 호출: 선택 → 크롭 시작 */
fun launch() {
if (ActivityResultContracts.PickVisualMedia.isPhotoPickerAvailable(context)) {
pickPhoto.launch(
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
)
if (excludeGif) {
openDocument.launch(arrayOf("image/png", "image/jpg", "image/jpeg"))
} else {
pickContent.launch("image/*")
if (ActivityResultContracts.PickVisualMedia.isPhotoPickerAvailable(context)) {
pickPhoto.launch(
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
)
} else {
pickContent.launch("image/*")
}
}
}
/** 마지막 크롭 결과 파일 (있으면) */
fun getCroppedFile(): File? = lastCroppedFile?.takeIf { it.exists() }
/** 임시 파일 삭제 */
fun cleanup() {
lastCroppedFile?.let { if (it.exists()) it.delete() }
lastCroppedFile = null
}
private fun handlePickedUri(source: Uri) {
if (isGifUri(source)) {
// 1) 캐시에 gif 그대로 복사
val gifFile = copyUriToCacheAsGif(source)
lastCroppedFile = gifFile
val fileUri = FileProvider.getUriForFile(
context, "${BuildConfig.APPLICATION_ID}.fileprovider", gifFile
)
// 2) 바로 반환 (크롭 생략)
onSuccess(gifFile, fileUri)
} else {
// 기존 그대로: uCrop 9:20 실행
startCrop(source)
}
}
private fun isGifUri(uri: Uri): Boolean {
// 1순위: MIME type
val mime = context.contentResolver.getType(uri)
if (mime?.equals("image/gif", ignoreCase = true) == true) return true
// 2순위: 파일명 확장자
val name = getDisplayName(uri)?.lowercase()
return name?.endsWith(".gif") == true
}
private fun getDisplayName(uri: Uri): String? {
return context.contentResolver.query(
uri,
arrayOf(OpenableColumns.DISPLAY_NAME),
null,
null,
null
)
?.use { c ->
val idx = c.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (idx >= 0 && c.moveToFirst()) c.getString(idx) else null
}
}
private fun copyUriToCacheAsGif(uri: Uri): File {
val base = if (config.useExternalCache) context.externalCacheDir ?: context.cacheDir
else context.cacheDir
// 원본 이름 유지 시도, 실패하면 타임스탬프
val name = getDisplayName(uri)?.takeIf { it.endsWith(".gif", true) }
?: "picked_${System.currentTimeMillis()}.gif"
val outFile = File(base, name)
context.contentResolver.openInputStream(uri).use { input ->
requireNotNull(input) { "Cannot open input stream for $uri" }
FileOutputStream(outFile).use { output ->
input.copyTo(output)
}
}
return outFile
}
private fun startCrop(source: Uri) {
val destFile = createTempCropFile()
lastCroppedFile = destFile
@@ -110,8 +180,8 @@ class ImagePickerCropper(
setCompressionQuality(config.compressQuality)
// 제스처/컨트롤 (필요시 조절)
setFreeStyleCropEnabled(false)
setHideBottomControls(false)
setFreeStyleCropEnabled(isEnabledFreeStyleCrop)
setHideBottomControls(true)
setAllowedGestures(
UCropActivity.SCALE, // Aspect 줄이진 못하지만 확대/축소
UCropActivity.ROTATE, // 회전

View File

@@ -2,35 +2,29 @@ package kr.co.vividnext.sodalive.explorer.profile.creator_community.modify
import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.graphics.Bitmap
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestOptions
import com.github.dhaval2404.imagepicker.ImagePicker
import com.gun0912.tedpermission.PermissionListener
import com.gun0912.tedpermission.normal.TedPermission
import com.jakewharton.rxbinding4.widget.textChanges
import com.orhanobut.logger.Logger
import com.yalantis.ucrop.UCrop
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.ImagePickerCropper
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.RealPathUtil
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ActivityCreatorCommunityModifyBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
import java.io.File
class CreatorCommunityModifyActivity : BaseActivity<ActivityCreatorCommunityModifyBinding>(
ActivityCreatorCommunityModifyBinding::inflate
@@ -39,142 +33,11 @@ class CreatorCommunityModifyActivity : BaseActivity<ActivityCreatorCommunityModi
private val viewModel: CreatorCommunityModifyViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var croppedTempFile: File
private val imageCropResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
val resultCode = result.resultCode
val data = result.data
if (resultCode == RESULT_OK) {
val fileUri = UCrop.getOutput(data!!)
if (fileUri != null) {
handleCroppedImage(fileUri)
} else {
showWrongImageFile()
}
} else if (resultCode == ImagePicker.RESULT_ERROR) {
val cropError = UCrop.getError(data!!)
cropError?.printStackTrace()
Toast.makeText(
this,
cropError?.message,
Toast.LENGTH_SHORT
).show()
}
}
private val imageSelectResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
val resultCode = result.resultCode
val data = result.data
if (resultCode == RESULT_OK) {
val fileUri = data?.data
if (fileUri != null) {
val mime = contentResolver.getType(fileUri)
if (mime.equals("image/gif", ignoreCase = true)) {
handleGifImage(fileUri)
} else {
launchCrop(fileUri)
}
} else {
showWrongImageFile()
}
} else if (resultCode == ImagePicker.RESULT_ERROR) {
showImagePickerError(data = data)
}
}
private fun launchImagePicker() {
ImagePicker.with(this)
.galleryOnly()
.createIntent { imageSelectResult.launch(it) }
}
private fun launchCrop(fileUri: Uri) {
croppedTempFile = File.createTempFile("cropped_", ".jpg", cacheDir)
val destinationUri = Uri.fromFile(croppedTempFile)
val options = UCrop.Options().apply {
setFreeStyleCropEnabled(true)
setToolbarTitle("이미지 자르기")
}
val uCrop = UCrop.of(fileUri, destinationUri)
.withOptions(options)
.withMaxResultSize(1080, 1080)
imageCropResult.launch(uCrop.getIntent(this))
}
private fun handleGifImage(fileUri: Uri) {
binding.ivContent.background = null
Glide.with(this)
.asGif()
.load(fileUri)
.placeholder(R.drawable.ic_place_holder)
.apply(
RequestOptions().transform(
RoundedCorners(
8f.dpToPx().toInt()
)
)
)
.into(binding.ivContent)
viewModel.imageUri = fileUri
}
private fun handleCroppedImage(fileUri: Uri) {
binding.ivContent.background = null
Glide.with(this)
.load(fileUri)
.placeholder(R.drawable.ic_place_holder)
.apply(
RequestOptions().transform(
RoundedCorners(
8f.dpToPx().toInt()
)
)
)
.into(binding.ivContent)
viewModel.imageUri = fileUri
}
private fun showWrongImageFile() {
Toast.makeText(
this,
"잘못된 파일입니다.\n다시 선택해 주세요.",
Toast.LENGTH_SHORT
).show()
}
private fun showImagePickerError(data: Intent?) {
Toast.makeText(
this,
ImagePicker.getError(data),
Toast.LENGTH_SHORT
).show()
}
private lateinit var cropper: ImagePickerCropper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
checkPermissions()
viewModel.getRealPathFromURI = {
RealPathUtil.getRealPath(applicationContext, it)
}
bindData()
val postId = intent.getLongExtra(Constants.EXTRA_COMMUNITY_POST_ID, 0)
@@ -193,31 +56,47 @@ class CreatorCommunityModifyActivity : BaseActivity<ActivityCreatorCommunityModi
}
override fun onDestroy() {
deleteCroppedTempFile()
cropper.cleanup()
super.onDestroy()
}
private fun deleteCroppedTempFile() {
try {
if (::croppedTempFile.isInitialized && croppedTempFile.exists()) {
val deleted = croppedTempFile.delete()
if (!deleted) {
Logger.w("임시 파일 삭제 실패: ${croppedTempFile.absolutePath}")
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
cropper = ImagePickerCropper(
caller = this,
context = this,
isEnabledFreeStyleCrop = true,
config = ImagePickerCropper.Config(
aspectX = 1f, aspectY = 1f,
compressFormat = Bitmap.CompressFormat.JPEG,
compressQuality = 90
),
onSuccess = { file, uri ->
binding.ivContent.background = null
Glide.with(this)
.load(uri)
.placeholder(R.drawable.ic_place_holder)
.apply(
RequestOptions().transform(
RoundedCorners(
16f.dpToPx().toInt()
)
)
)
.into(binding.ivContent)
viewModel.imageFile = file
},
onError = { e ->
Toast.makeText(this, "${e.message}", Toast.LENGTH_SHORT).show()
}
)
binding.toolbar.tvBack.text = "게시글 등록"
binding.toolbar.tvBack.setOnClickListener { finish() }
binding.ivPhotoPicker.setOnClickListener { launchImagePicker() }
binding.ivPhotoPicker.setOnClickListener { cropper.launch() }
if (SharedPreferenceManager.isAuth) {
binding.llSetAdult.visibility = View.VISIBLE

View File

@@ -1,6 +1,5 @@
package kr.co.vividnext.sodalive.explorer.profile.creator_community.modify
import android.net.Uri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.google.gson.Gson
@@ -46,11 +45,9 @@ class CreatorCommunityModifyViewModel(
val isAvailableCommentLiveData: LiveData<Boolean>
get() = _isAvailableCommentLiveData
lateinit var getRealPathFromURI: (Uri) -> String?
var postId = 0L
var content = ""
var imageUri: Uri? = null
var imageFile: File? = null
private var communityPost: GetCommunityPostListResponse? = null
fun setAdult(isAdult: Boolean) {
@@ -134,8 +131,8 @@ class CreatorCommunityModifyViewModel(
val requestJson = Gson().toJson(request)
val postImage = if (imageUri != null) {
val file = File(getRealPathFromURI(imageUri!!))
val postImage = if (imageFile != null) {
val file = imageFile!!
MultipartBody.Part.createFormData(
"postImage",
file.name,

View File

@@ -2,29 +2,24 @@ package kr.co.vividnext.sodalive.explorer.profile.creator_community.write
import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.graphics.Bitmap
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestOptions
import com.github.dhaval2404.imagepicker.ImagePicker
import com.gun0912.tedpermission.PermissionListener
import com.gun0912.tedpermission.normal.TedPermission
import com.jakewharton.rxbinding4.widget.textChanges
import com.orhanobut.logger.Logger
import com.yalantis.ucrop.UCrop
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.ImagePickerCropper
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.RealPathUtil
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ActivityCreatorCommunityWriteBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
@@ -38,154 +33,18 @@ class CreatorCommunityWriteActivity : BaseActivity<ActivityCreatorCommunityWrite
private val viewModel: CreatorCommunityWriteViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var croppedTempFile: File
private val imageCropResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
val resultCode = result.resultCode
val data = result.data
if (resultCode == RESULT_OK) {
val fileUri = UCrop.getOutput(data!!)
if (fileUri != null) {
handleCroppedImage(fileUri)
} else {
showWrongImageFile()
}
} else if (resultCode == ImagePicker.RESULT_ERROR) {
val cropError = UCrop.getError(data!!)
cropError?.printStackTrace()
Toast.makeText(
this,
cropError?.message,
Toast.LENGTH_SHORT
).show()
}
}
private val imageSelectResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
val resultCode = result.resultCode
val data = result.data
if (resultCode == RESULT_OK) {
val fileUri = data?.data
if (fileUri != null) {
val mime = contentResolver.getType(fileUri)
if (mime.equals("image/gif", ignoreCase = true)) {
handleGifImage(fileUri)
} else {
launchCrop(fileUri)
}
} else {
showWrongImageFile()
}
} else if (resultCode == ImagePicker.RESULT_ERROR) {
showImagePickerError(data = data)
}
}
private fun launchImagePicker() {
ImagePicker.with(this)
.galleryOnly()
.createIntent { imageSelectResult.launch(it) }
}
private fun launchCrop(fileUri: Uri) {
croppedTempFile = File.createTempFile("cropped_", ".jpg", cacheDir)
val destinationUri = Uri.fromFile(croppedTempFile)
val options = UCrop.Options().apply {
setFreeStyleCropEnabled(true)
setToolbarTitle("이미지 자르기")
}
val uCrop = UCrop.of(fileUri, destinationUri)
.withOptions(options)
.withMaxResultSize(1080, 1080)
imageCropResult.launch(uCrop.getIntent(this))
}
private fun handleGifImage(fileUri: Uri) {
binding.llRecordAudio.visibility = View.VISIBLE
binding.ivContent.background = null
Glide.with(this)
.asGif()
.load(fileUri)
.placeholder(R.drawable.ic_place_holder)
.apply(
RequestOptions().transform(
RoundedCorners(
8f.dpToPx().toInt()
)
)
)
.into(binding.ivContent)
viewModel.setImageUri(fileUri)
}
private fun handleCroppedImage(fileUri: Uri) {
binding.llRecordAudio.visibility = View.VISIBLE
binding.ivContent.background = null
Glide.with(this)
.load(fileUri)
.placeholder(R.drawable.ic_place_holder)
.apply(
RequestOptions().transform(
RoundedCorners(
8f.dpToPx().toInt()
)
)
)
.into(binding.ivContent)
viewModel.setImageUri(fileUri)
}
private fun showWrongImageFile() {
binding.llRecordAudio.visibility = View.GONE
Toast.makeText(
this,
"잘못된 파일입니다.\n다시 선택해 주세요.",
Toast.LENGTH_SHORT
).show()
}
private fun showImagePickerError(data: Intent?) {
binding.llRecordAudio.visibility = View.GONE
Toast.makeText(
this,
ImagePicker.getError(data),
Toast.LENGTH_SHORT
).show()
}
private lateinit var cropper: ImagePickerCropper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
checkPermissions()
viewModel.getRealPathFromURI = {
RealPathUtil.getRealPath(applicationContext, it)
}
bindData()
}
override fun onDestroy() {
cropper.cleanup()
deleteAudioFile()
deleteCroppedTempFile()
super.onDestroy()
}
@@ -195,22 +54,39 @@ class CreatorCommunityWriteActivity : BaseActivity<ActivityCreatorCommunityWrite
}
}
private fun deleteCroppedTempFile() {
try {
if (::croppedTempFile.isInitialized && croppedTempFile.exists()) {
val deleted = croppedTempFile.delete()
if (!deleted) {
Logger.w("임시 파일 삭제 실패: ${croppedTempFile.absolutePath}")
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
cropper = ImagePickerCropper(
caller = this,
context = this,
isEnabledFreeStyleCrop = true,
config = ImagePickerCropper.Config(
aspectX = 1f, aspectY = 1f,
compressFormat = Bitmap.CompressFormat.JPEG,
compressQuality = 90
),
onSuccess = { file, uri ->
binding.ivContent.background = null
Glide.with(this)
.load(uri)
.placeholder(R.drawable.ic_place_holder)
.apply(
RequestOptions().transform(
RoundedCorners(
16f.dpToPx().toInt()
)
)
)
.into(binding.ivContent)
viewModel.setImageFile(file)
},
onError = { e ->
Toast.makeText(this, "${e.message}", Toast.LENGTH_SHORT).show()
}
)
binding.toolbar.tvBack.text = "게시글 등록"
binding.toolbar.tvBack.setOnClickListener { finish() }
@@ -220,7 +96,7 @@ class CreatorCommunityWriteActivity : BaseActivity<ActivityCreatorCommunityWrite
fragment.show(supportFragmentManager, fragment.tag)
}
binding.ivPhotoPicker.setOnClickListener { launchImagePicker() }
binding.ivPhotoPicker.setOnClickListener { cropper.launch() }
if (SharedPreferenceManager.isAuth) {
binding.llSetAdult.visibility = View.VISIBLE
@@ -278,7 +154,7 @@ class CreatorCommunityWriteActivity : BaseActivity<ActivityCreatorCommunityWrite
.subscribe {
val price = it.toString().toIntOrNull()
if (price != null) {
viewModel.price = price.toInt()
viewModel.price = price
} else {
viewModel.price = 0
if (it.isNotBlank()) {

View File

@@ -1,6 +1,5 @@
package kr.co.vividnext.sodalive.explorer.profile.creator_community.write
import android.net.Uri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.google.gson.Gson
@@ -45,12 +44,10 @@ class CreatorCommunityWriteViewModel(
val isShowPriceUiLiveData: LiveData<Boolean>
get() = _isShowPriceUiLiveData
lateinit var getRealPathFromURI: (Uri) -> String?
var price = 0
var content = ""
var audioFile: File? = null
private var imageUri: Uri? = null
private var imageFile: File? = null
fun setAdult(isAdult: Boolean) {
_isAdultLiveData.postValue(isAdult)
@@ -64,8 +61,8 @@ class CreatorCommunityWriteViewModel(
_isPriceFreeLiveData.postValue(isPriceFree)
}
fun setImageUri(uri: Uri) {
this.imageUri = uri
fun setImageFile(file: File) {
this.imageFile = file
_isShowPriceUiLiveData.postValue(true)
}
@@ -82,8 +79,8 @@ class CreatorCommunityWriteViewModel(
val requestJson = Gson().toJson(request)
val postImage = if (imageUri != null) {
val file = File(getRealPathFromURI(imageUri!!))
val postImage = if (imageFile != null) {
val file = imageFile!!
MultipartBody.Part.createFormData(
"postImage",
file.name,
@@ -188,12 +185,12 @@ class CreatorCommunityWriteViewModel(
return false
}
if (imageUri == null) {
if (imageFile == null) {
_toastLiveData.postValue("유료 게시글 등록을 위해서는 이미지가 필요합니다.")
return false
}
}
} catch (e: Exception) {
} catch (_: Exception) {
_toastLiveData.postValue("가격은 숫자만 입력 가능 합니다.")
return false
}

View File

@@ -3,15 +3,14 @@ package kr.co.vividnext.sodalive.live.room
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.app.Service
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Rect
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.CountDownTimer
@@ -37,6 +36,7 @@ import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.PopupMenu
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@@ -46,7 +46,6 @@ import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import com.github.dhaval2404.imagepicker.ImagePicker
import com.google.gson.Gson
import com.orhanobut.logger.Logger
import io.agora.rtc2.ClientRoleOptions
@@ -67,8 +66,8 @@ import kr.co.vividnext.sodalive.agora.Agora
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.base.SodaDialog
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.ImagePickerCropper
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.RealPathUtil
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.SodaLiveService
import kr.co.vividnext.sodalive.databinding.ActivityLiveRoomBinding
@@ -117,6 +116,8 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
private lateinit var speakerListAdapter: LiveRoomProfileListAdapter
private lateinit var loadingDialog: LoadingDialog
private lateinit var cropper: ImagePickerCropper
private lateinit var imm: InputMethodManager
private val handler = Handler(Looper.getMainLooper())
@@ -235,20 +236,6 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
}
}
private val imageResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val resultCode = result.resultCode
val data = result.data
if (resultCode == RESULT_OK) {
// Image Uri will not be null for RESULT_OK
val fileUri = data?.data!!
roomInfoEditDialog.setCoverImageUri(fileUri)
} else if (resultCode == ImagePicker.RESULT_ERROR) {
Toast.makeText(this, ImagePicker.getError(data), Toast.LENGTH_SHORT).show()
}
}
private val rouletteConfigResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
@@ -280,9 +267,6 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
super.onCreate(savedInstanceState)
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
viewModel.getRealPathFromURI = {
RealPathUtil.getRealPath(applicationContext, it)
}
this.roomId = intent.getLongExtra(Constants.EXTRA_ROOM_ID, 0)
if (roomId <= 0) {
@@ -321,27 +305,34 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
bindData()
loadingDialog = LoadingDialog(this, layoutInflater)
cropper = ImagePickerCropper(
caller = this,
context = this,
excludeGif = true,
config = ImagePickerCropper.Config(
aspectX = 2f, aspectY = 3.8f,
maxWidth = 1080, maxHeight = 2052,
compressFormat = Bitmap.CompressFormat.JPEG,
compressQuality = 90
),
onSuccess = { file, uri ->
roomInfoEditDialog.setCoverImageUri(file)
},
onError = { e ->
Toast.makeText(this, "${e.message}", Toast.LENGTH_SHORT).show()
}
)
imm = getSystemService(
Service.INPUT_METHOD_SERVICE
INPUT_METHOD_SERVICE
) as InputMethodManager
roomDialog = LiveRoomDialog(this, layoutInflater)
roomInfoEditDialog = LiveRoomInfoEditDialog(
activity = this,
layoutInflater = layoutInflater,
onClickImagePicker = {
ImagePicker.with(this)
.crop()
.galleryOnly()
.galleryMimeTypes( // Exclude gif images
mimeTypes = arrayOf(
"image/png",
"image/jpg",
"image/jpeg"
)
)
.createIntent { imageResult.launch(it) }
}
onClickImagePicker = { cropper.launch() }
)
roomProfileDialog = LiveRoomProfileDialog(
layoutInflater = layoutInflater,
@@ -559,6 +550,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
}
override fun onDestroy() {
cropper.cleanup()
hideKeyboard {
viewModel.quitRoom(roomId) {
SodaLiveService.stopService(this)
@@ -960,7 +952,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
)
roomInfoEditDialog.setCoverImageUrl(response.coverImageUrl)
roomInfoEditDialog.setMenuPreset(it)
roomInfoEditDialog.setConfirmAction { newTitle, newContent, newCoverImageUri, isActivateMenu, menuId, menu, isAdult, isEntryMessageEnabled ->
roomInfoEditDialog.setConfirmAction { newTitle, newContent, newCoverImageFile, isActivateMenu, menuId, menu, isAdult, isEntryMessageEnabled ->
if (isEntryMessageEnabled != null) {
this.isEntryMessageEnabled = isEntryMessageEnabled
}
@@ -969,12 +961,13 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
response.roomId,
newTitle,
newContent,
newCoverImageUri,
newCoverImageFile,
isActivateMenu,
menuId,
menu,
isAdult,
onSuccess = {
cropper.cleanup()
Toast.makeText(
applicationContext,
"라이브 정보가 수정되었습니다.",
@@ -1280,7 +1273,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
val clickableSpan = object : ClickableSpan() {
override fun onClick(widget: View) {
val url = spannable.subSequence(start, end).toString()
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
startActivity(Intent(Intent.ACTION_VIEW, url.toUri()))
}
}
spannable.setSpan(clickableSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)

View File

@@ -1,6 +1,5 @@
package kr.co.vividnext.sodalive.live.room
import android.net.Uri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.google.gson.Gson
@@ -99,8 +98,6 @@ class LiveRoomViewModel(
val isSignatureOn: LiveData<Boolean>
get() = _isSignatureOn
lateinit var getRealPathFromURI: (Uri) -> String?
private val blockedMemberIdList: MutableList<Long> = mutableListOf()
val mutex = Mutex()
@@ -430,7 +427,7 @@ class LiveRoomViewModel(
roomId: Long,
newTitle: String,
newContent: String,
newCoverImageUri: Uri? = null,
newCoverImageFile: File? = null,
isActivateMenu: Boolean?,
menuId: Long,
menu: String,
@@ -462,7 +459,7 @@ class LiveRoomViewModel(
request.notice == null &&
menu == roomInfoResponse.menuPan &&
request.isAdult == null &&
newCoverImageUri == null &&
newCoverImageFile == null &&
request.isActiveMenuPan == null
) {
return
@@ -480,8 +477,8 @@ class LiveRoomViewModel(
null
}
val coverImage = if (newCoverImageUri != null) {
val file = File(getRealPathFromURI(newCoverImageUri))
val coverImage = if (newCoverImageFile != null) {
val file = newCoverImageFile
MultipartBody.Part.createFormData(
"coverImage",
file.name,

View File

@@ -131,6 +131,7 @@ class LiveRoomCreateActivity : BaseActivity<ActivityLiveRoomCreateBinding>(
cropper = ImagePickerCropper(
caller = this,
context = this,
excludeGif = true,
config = ImagePickerCropper.Config(
aspectX = 2f, aspectY = 3.8f,
maxWidth = 1080, maxHeight = 2052,

View File

@@ -2,8 +2,6 @@ package kr.co.vividnext.sodalive.live.room.update
import android.annotation.SuppressLint
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
@@ -15,15 +13,18 @@ import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toDrawable
import androidx.lifecycle.MutableLiveData
import coil.load
import coil.transform.RoundedCornersTransformation
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestOptions
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.DialogLiveRoomInfoUpdateBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.live.room.create.LiveRoomCreateViewModel
import kr.co.vividnext.sodalive.live.room.menu.GetMenuPresetResponse
import java.io.File
@SuppressLint("ClickableViewAccessibility")
class LiveRoomInfoEditDialog(
@@ -35,7 +36,7 @@ class LiveRoomInfoEditDialog(
private val dialogView = DialogLiveRoomInfoUpdateBinding.inflate(layoutInflater)
private var coverImageUrl: String? = null
private var coverImageUri: Uri? = null
private var coverImageFile: File? = null
private var menuId = 0L
private val menuList = mutableListOf<GetMenuPresetResponse>()
@@ -58,7 +59,7 @@ class LiveRoomInfoEditDialog(
alertDialog = dialogBuilder.create()
alertDialog.setCancelable(false)
alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
alertDialog.window?.setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
dialogView.ivPhotoPicker.setOnClickListener { onClickImagePicker() }
dialogView.ivClose.setOnClickListener { alertDialog.dismiss() }
@@ -185,22 +186,34 @@ class LiveRoomInfoEditDialog(
isEntryMessageEnabledLiveData.value = isEntryMessageEnabled
}
fun setCoverImageUri(coverImageUri: Uri) {
this.coverImageUri = coverImageUri
dialogView.ivCover.load(coverImageUri) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(13.3f.dpToPx()))
}
fun setCoverImageUri(file: File) {
this.coverImageFile = file
Glide.with(activity)
.load(file)
.placeholder(R.drawable.ic_place_holder)
.apply(
RequestOptions().transform(
RoundedCorners(
13.3f.dpToPx().toInt()
)
)
)
.into(dialogView.ivCover)
}
fun setCoverImageUrl(coverImageUrl: String) {
this.coverImageUrl = coverImageUrl
dialogView.ivCover.load(coverImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(13.3f.dpToPx()))
}
Glide.with(activity)
.load(coverImageUrl)
.placeholder(R.drawable.ic_place_holder)
.apply(
RequestOptions().transform(
RoundedCorners(
13.3f.dpToPx().toInt()
)
)
)
.into(dialogView.ivCover)
}
fun setMenuPreset(menuList: List<GetMenuPresetResponse>) {
@@ -223,7 +236,7 @@ class LiveRoomInfoEditDialog(
}
fun setConfirmAction(
confirmAction: (String, String, Uri?, Boolean?, Long, String, Boolean?, Boolean?) -> Unit
confirmAction: (String, String, File?, Boolean?, Long, String, Boolean?, Boolean?) -> Unit
) {
dialogView.tvConfirm.setOnClickListener {
alertDialog.dismiss()
@@ -235,7 +248,7 @@ class LiveRoomInfoEditDialog(
confirmAction(
newTitle,
newContent,
coverImageUri,
coverImageFile,
if (isActivateMenu != null) {
isActivateMenu
} else if (
@@ -262,7 +275,7 @@ class LiveRoomInfoEditDialog(
null
}
)
coverImageUri = null
coverImageFile = null
coverImageUrl = null
}
}
@@ -291,7 +304,7 @@ class LiveRoomInfoEditDialog(
)
)
if (menuList.size > 0) {
if (menuList.isNotEmpty()) {
dialogView.llSelectMenu2.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
dialogView.tvSelectMenu2.setTextColor(
ContextCompat.getColor(

View File

@@ -1,21 +1,23 @@
package kr.co.vividnext.sodalive.mypage.profile
import android.content.Intent
import android.graphics.Bitmap
import android.os.Bundle
import android.view.View
import android.widget.LinearLayout
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import coil.load
import coil.transform.CircleCropTransformation
import com.github.dhaval2404.imagepicker.ImagePicker
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.CircleCrop
import com.bumptech.glide.request.RequestOptions
import com.jakewharton.rxbinding4.widget.textChanges
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.ImagePickerCropper
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.RealPathUtil
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ActivityProfileUpdateBinding
import kr.co.vividnext.sodalive.databinding.ItemLiveTagSelectedBinding
@@ -33,27 +35,6 @@ class ProfileUpdateActivity : BaseActivity<ActivityProfileUpdateBinding>(
private val viewModel: ProfileUpdateViewModel by inject()
private val imageResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val resultCode = result.resultCode
val data = result.data
if (resultCode == RESULT_OK) {
// Image Uri will not be null for RESULT_OK
val fileUri = data?.data!!
binding.ivProfile.background = null
viewModel.updateProfileImage(fileUri) {
SharedPreferenceManager.profileImage = it
binding.ivProfile.load(it) {
crossfade(true)
transformations(CircleCropTransformation())
}
}
} else if (resultCode == ImagePicker.RESULT_ERROR) {
Toast.makeText(this, ImagePicker.getError(data), Toast.LENGTH_SHORT).show()
}
}
private val tagFragment: MemberTagFragment by lazy {
MemberTagFragment(viewModel.tags) { tag, isChecked ->
when {
@@ -76,22 +57,23 @@ class ProfileUpdateActivity : BaseActivity<ActivityProfileUpdateBinding>(
private lateinit var loadingDialog: LoadingDialog
private lateinit var cropper: ImagePickerCropper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.getRealPathFromURI = {
RealPathUtil.getRealPath(applicationContext, it)
}
bindData()
}
override fun onStart() {
super.onStart()
viewModel.getUserInfo()
}
override fun onDestroy() {
cropper.cleanup()
super.onDestroy()
}
private fun bindData() {
compositeDisposable.add(
binding.etBlog.textChanges().skip(1)
@@ -220,6 +202,34 @@ class ProfileUpdateActivity : BaseActivity<ActivityProfileUpdateBinding>(
loadingDialog = LoadingDialog(this, layoutInflater)
cropper = ImagePickerCropper(
caller = this,
context = this,
isEnabledFreeStyleCrop = false,
config = ImagePickerCropper.Config(
aspectX = 1f, aspectY = 1f,
compressFormat = Bitmap.CompressFormat.JPEG,
compressQuality = 90
),
onSuccess = { file, _ ->
binding.ivProfile.background = null
viewModel.updateProfileImage(file) {
SharedPreferenceManager.profileImage = it
Glide.with(this)
.load(it)
.placeholder(R.drawable.ic_place_holder)
.apply(
RequestOptions()
.transform(CircleCrop())
)
.into(binding.ivProfile)
}
},
onError = { e ->
Toast.makeText(this, "${e.message}", Toast.LENGTH_SHORT).show()
}
)
compositeDisposable.add(
binding.etIntroduce.textChanges()
.debounce(500, TimeUnit.MILLISECONDS)
@@ -241,19 +251,7 @@ class ProfileUpdateActivity : BaseActivity<ActivityProfileUpdateBinding>(
viewModel.changeGender(Gender.NONE)
}
binding.ivPhotoPicker.setOnClickListener {
ImagePicker.with(this)
.crop()
.galleryOnly()
.galleryMimeTypes( // Exclude gif images
mimeTypes = arrayOf(
"image/png",
"image/jpg",
"image/jpeg"
)
)
.createIntent { imageResult.launch(it) }
}
binding.ivPhotoPicker.setOnClickListener { cropper.launch() }
binding.tvSelectTag.setOnClickListener {
if (tagFragment.isAdded) return@setOnClickListener

View File

@@ -1,6 +1,5 @@
package kr.co.vividnext.sodalive.mypage.profile
import android.net.Uri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.orhanobut.logger.Logger
@@ -53,8 +52,6 @@ class ProfileUpdateViewModel(private val repository: UserRepository) : BaseViewM
val isLoading: LiveData<Boolean>
get() = _isLoading
lateinit var getRealPathFromURI: (Uri) -> String?
fun getUserInfo() {
compositeDisposable.add(
repository.getProfile("Bearer ${SharedPreferenceManager.token}")
@@ -90,8 +87,7 @@ class ProfileUpdateViewModel(private val repository: UserRepository) : BaseViewM
_genderLiveData.postValue(gender)
}
fun updateProfileImage(uri: Uri, onSuccess: (String) -> Unit) {
val file = File(getRealPathFromURI(uri))
fun updateProfileImage(file: File, onSuccess: (String) -> Unit) {
val image = MultipartBody.Part.createFormData(
"image",
file.name,