Skip to content

Commit

Permalink
Merge pull request #147 from boostcampwm2023/android/feature/60
Browse files Browse the repository at this point in the history
Upload UI 상태
  • Loading branch information
youlalala authored Nov 22, 2023
2 parents ac2cc69 + 1dfbf3b commit bf4b743
Show file tree
Hide file tree
Showing 16 changed files with 231 additions and 38 deletions.
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) }
}

@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
}
}
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

0 comments on commit bf4b743

Please sign in to comment.