Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upload UI 상태 #147

Merged
merged 16 commits into from
Nov 22, 2023
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
@@ -1,12 +1,20 @@
package com.ohdodok.catchytape.core.data.api

import com.ohdodok.catchytape.core.data.model.MusicGenresResponse
import com.ohdodok.catchytape.core.data.model.MusicRequest
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST

interface MusicApi {

@GET("musics/genres")
suspend fun getGenres(): Response<MusicGenresResponse>

@POST("musics")
suspend fun postMusic(
@Body music: MusicRequest
): Response<Unit>

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.ohdodok.catchytape.core.data.api

import com.ohdodok.catchytape.core.data.model.UrlResponse
import retrofit2.Response
import retrofit2.http.Headers
import retrofit2.http.POST

interface UploadApi {

@POST("upload/music")
@Headers("Content-Type: audio/mpeg")
suspend fun uploadMusic(
): Response<UrlResponse>

@POST("upload/image")
@Headers("Content-Type: image/png")
suspend fun uploadImage(
): Response<UrlResponse>

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.ohdodok.catchytape.core.data.di

import com.ohdodok.catchytape.core.data.api.MusicApi
import com.ohdodok.catchytape.core.data.api.UploadApi
import com.ohdodok.catchytape.core.data.api.UserApi
import dagger.Module
import dagger.Provides
Expand All @@ -24,4 +25,10 @@ object ApiModule {
fun provideMusicApi(retrofit: Retrofit): MusicApi {
return retrofit.create(MusicApi::class.java)
}

@Provides
@Singleton
fun provideUploadApi(retrofit: Retrofit): UploadApi {
return retrofit.create(UploadApi::class.java)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.ohdodok.catchytape.core.data.model

data class MusicRequest (
val title: String,
val cover: String,
val file: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.ohdodok.catchytape.core.data.model

data class UrlResponse(
val url: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.ohdodok.catchytape.core.domain.usecase

import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import java.io.File
import javax.inject.Inject

class UploadFileUseCase @Inject constructor(

) {

fun getImgUrl(file: File): Flow<String> = flow {
// todo : 서버 기다리는 중..
delay(1000)
emit("https://kr.object.ncloudstorage.com/catchy-tape-bucket2/image/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7%202023-11-21%20180100.png")
}

fun getAudioUrl(file: File): Flow<String> = flow {
// todo : 서버 기다리는 중..
delay(1000)
emit("https://kr.object.ncloudstorage.com/catchy-tape-bucket2/music/2/%EC%9D%B4%EB%85%B8%EB%9E%98.mp3")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.ohdodok.catchytape.core.domain.usecase

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import javax.inject.Inject

class UploadMusicUseCase @Inject constructor() {

operator fun invoke(imgUrl: String, audioUrl: String, title: String, genre: String): Flow<Unit> = flow {
// todo : 서버에 업로드
emit(Unit)
}
}
5 changes: 4 additions & 1 deletion android/core/ui/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,13 @@ android {

dependencies {

implementation(project(":core:domain"))

api(libs.material)

api(libs.navigation.fragment.ktx)
api(libs.navigation.ui.ktx)

implementation(project(":core:domain"))
api(libs.glide)

}
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
package com.ohdodok.catchytape.core.ui

import android.widget.ImageView
import androidx.databinding.BindingAdapter
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView

import com.bumptech.glide.Glide

@BindingAdapter("submitList")
fun <T, VH : RecyclerView.ViewHolder> RecyclerView.bindItems(items: List<T>) {
val adapter = this.adapter ?: return
val listAdapter: ListAdapter<T, VH> = adapter as ListAdapter<T, VH>
listAdapter.submitList(items)
}

@BindingAdapter("imgUrl")
fun ImageView.bindImg(url: String) {
Glide.with(this.context)
.load(url)
.into(this)
}
2 changes: 1 addition & 1 deletion android/core/ui/src/main/res/drawable/ic_camera.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
android:viewportHeight="24">
<path
android:pathData="M20,4H16.83L15,2H9L7.17,4H4C3.47,4 2.961,4.211 2.586,4.586C2.211,4.961 2,5.47 2,6V18C2,18.53 2.211,19.039 2.586,19.414C2.961,19.789 3.47,20 4,20H20C20.53,20 21.039,19.789 21.414,19.414C21.789,19.039 22,18.53 22,18V6C22,5.47 21.789,4.961 21.414,4.586C21.039,4.211 20.53,4 20,4ZM20,18H4V6H8.05L9.88,4H14.12L15.95,6H20V18ZM12,7C10.674,7 9.402,7.527 8.464,8.464C7.527,9.402 7,10.674 7,12C7,13.326 7.527,14.598 8.464,15.535C9.402,16.473 10.674,17 12,17C13.326,17 14.598,16.473 15.535,15.535C16.473,14.598 17,13.326 17,12C17,10.674 16.473,9.402 15.535,8.464C14.598,7.527 13.326,7 12,7ZM12,15C11.204,15 10.441,14.684 9.879,14.121C9.316,13.559 9,12.796 9,12C9,11.204 9.316,10.441 9.879,9.879C10.441,9.316 11.204,9 12,9C12.796,9 13.559,9.316 14.121,9.879C14.684,10.441 15,11.204 15,12C15,12.796 14.684,13.559 14.121,14.121C13.559,14.684 12.796,15 12,15Z"
android:fillColor="@color/on_surface"/>
android:fillColor="@color/black"/>
</vector>
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,6 @@ import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView
import androidx.databinding.BindingAdapter


@BindingAdapter("changeSelectedPosition")
fun AutoCompleteTextView.bindPosition(onChange: (Int) -> Unit) {
setOnItemClickListener { _, _, position, _ -> onChange(position) }
}
Comment on lines -9 to -12
Copy link
Member Author

Choose a reason for hiding this comment

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

position 이 필요한 것 같지 않아서 선택한 텍스트로 판단하도록 바꼈습니당


@BindingAdapter("list")
fun AutoCompleteTextView.setAdapter(list: List<String>) {
val adapter = ArrayAdapter(this.context, R.layout.simple_list_item_1, list)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,45 +1,62 @@
package com.ohdodok.catchytape.feature.upload

import android.net.Uri
import android.os.Bundle
import android.provider.OpenableColumns
import android.view.View
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia
import androidx.core.net.toFile
import androidx.fragment.app.viewModels
import com.ohdodok.catchytape.catchytape.upload.R
import com.ohdodok.catchytape.catchytape.upload.databinding.FragmentUploadBinding
import com.ohdodok.catchytape.core.ui.BaseFragment
import dagger.hilt.android.AndroidEntryPoint
import java.io.File

@AndroidEntryPoint
class UploadFragment : BaseFragment<FragmentUploadBinding>(R.layout.fragment_upload) {
private val viewModel: UploadViewModel by viewModels()

private val imagePickerLauncher = registerForActivityResult(PickVisualMedia()) { uri ->
if (uri == null) return@registerForActivityResult

viewModel.uploadImage(uri.toFile())
uri.path?.let { viewModel.uploadImage(File(it)) }
}

private val filePickerLauncher =
registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
if (uri == null) return@registerForActivityResult

viewModel.uploadAudio(uri.toFile())
binding.btnFile.text = getFileName(uri)
uri.path?.let { viewModel.uploadAudio(File(it)) }
}

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

setUpFileBtn()
setupBackStack(binding.tbUpload)
setupSelectThumbnailImage()
}

private fun setUpFileBtn() {
binding.btnFile.setOnClickListener {
filePickerLauncher.launch("audio/*")
}
}

private fun setupSelectThumbnailImage() {
binding.cvUploadThumbnail.setOnClickListener {
imagePickerLauncher.launch(PickVisualMediaRequest(PickVisualMedia.ImageOnly))
}
}

private fun getFileName(uri: Uri): String? {
val contentResolver = requireContext().contentResolver
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
cursor.moveToFirst()
return cursor.getString(nameIndex)
}
return null
}
Comment on lines +53 to +61
Copy link
Collaborator

Choose a reason for hiding this comment

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

이 함수 하는 일이 무엇인가요?

Copy link
Member Author

Choose a reason for hiding this comment

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

파일이름을 가져오는 함수입니당 공식문서 참고했어용

}
Original file line number Diff line number Diff line change
@@ -1,40 +1,62 @@
package com.ohdodok.catchytape.feature.upload

import android.net.Uri
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import java.io.File
import javax.inject.Inject
import androidx.lifecycle.viewModelScope
import com.ohdodok.catchytape.core.domain.usecase.UploadFileUseCase
import com.ohdodok.catchytape.core.domain.usecase.GetMusicGenresUseCase
import com.ohdodok.catchytape.core.domain.usecase.UploadMusicUseCase
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn

@HiltViewModel
class UploadViewModel @Inject constructor(
private val getMusicGenresUseCase: GetMusicGenresUseCase
private val getMusicGenresUseCase: GetMusicGenresUseCase,
private val uploadFileUseCase: UploadFileUseCase,
private val uploadMusicUseCase: UploadMusicUseCase
) : ViewModel() {

private var uploadedImage: String? = null
val musicTitle = MutableStateFlow("")
val musicGenre = MutableStateFlow("")

val uploadedMusicTitle = MutableStateFlow("")
private val _imageState: MutableStateFlow<UploadedFileState> =
MutableStateFlow(UploadedFileState())
val imageState = _imageState.asStateFlow()

private val _uploadedMusicGenrePosition = MutableStateFlow(-1)
val uploadedMusicGenrePosition = _uploadedMusicGenrePosition.asStateFlow()
private val _audioState: MutableStateFlow<UploadedFileState> =
MutableStateFlow(UploadedFileState())
val audioState = _audioState.asStateFlow()

val isLoading: StateFlow<Boolean> = combine(imageState, audioState) { imageState, audioState ->
imageState.isLoading || audioState.isLoading
}.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = false
)

val isUploadEnable: StateFlow<Boolean> =
combine(uploadedMusicTitle, uploadedMusicGenrePosition) { title, genrePosition ->
title.isNotBlank() && genrePosition != -1
combine(
musicTitle, musicGenre, imageState, audioState
) { title, genre, imageState, audioState ->
title.isNotBlank()
&& genre.isNotBlank()
&& imageState.url.isNotBlank()
&& audioState.url.isNotBlank()
}.stateIn(viewModelScope, SharingStarted.Eagerly, false)

val onChangePosition: (Int) -> Unit =
{ position: Int -> _uploadedMusicGenrePosition.value = position }

private val _musicGenres: MutableStateFlow<List<String>> = MutableStateFlow(emptyList())
val musicGenres = _musicGenres.asStateFlow()

Expand All @@ -49,11 +71,43 @@ class UploadViewModel @Inject constructor(
}

fun uploadImage(imageFile: File) {
// todo : image 파일을 업로드 한다.
// todo : 반환 값을 uploadedImage에 저장한다.
uploadFileUseCase.getImgUrl(imageFile).onStart {
_imageState.value = imageState.value.copy(isLoading = true)
}.onEach { url ->
_imageState.value = imageState.value.copy(url = url)
}.onCompletion {
_imageState.value = imageState.value.copy(isLoading = false)
}.launchIn(viewModelScope)
}

fun uploadAudio(audioFile: File) {
// todo : audio 파일을 업로드 한다.
uploadFileUseCase.getAudioUrl(audioFile).onStart {
_audioState.value = audioState.value.copy(isLoading = true)
}.onEach { url ->
_audioState.value = audioState.value.copy(url = url)
}.onCompletion {
_audioState.value = audioState.value.copy(isLoading = false)
}.launchIn(viewModelScope)
}
}

fun uploadMusic() {
if (isUploadEnable.value) {
uploadMusicUseCase(
imgUrl = imageState.value.url,
audioUrl = audioState.value.url,
title = musicTitle.value,
genre = musicGenre.value
).onEach {
// TODO : 업로드 성공
}.catch {
// TODO : 업로드 실패
}.launchIn(viewModelScope)
}
}
}

data class UploadedFileState(
val isLoading: Boolean = false,
val url: String = ""
)

Loading