WIP agregar audio a post, sube audio
se agrega crashlytics
This commit is contained in:
parent
5084fe337d
commit
835c7304c2
@ -7,6 +7,8 @@ plugins {
|
||||
id 'org.jetbrains.kotlin.plugin.serialization' version '1.8.0'
|
||||
id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
|
||||
id 'androidx.navigation.safeargs.kotlin'
|
||||
id 'com.google.gms.google-services'
|
||||
id 'com.google.firebase.crashlytics'
|
||||
}
|
||||
|
||||
android {
|
||||
@ -125,4 +127,9 @@ dependencies {
|
||||
// QR
|
||||
implementation 'com.github.androidmads:QRGenerator:1.0.1'
|
||||
|
||||
// Firebase
|
||||
implementation(platform("com.google.firebase:firebase-bom:32.7.1"))
|
||||
implementation("com.google.firebase:firebase-crashlytics")
|
||||
implementation("com.google.firebase:firebase-analytics")
|
||||
|
||||
}
|
||||
@ -23,8 +23,8 @@ class Module {
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideAudiosRepository(audiosApi: AudiosApi): AudiosRepository {
|
||||
return AudiosRepositoryImpl(audiosApi)
|
||||
fun provideAudiosRepository(audiosApi: AudiosApi, audiosDraftsDao: AudiosDraftsDao): AudiosRepository {
|
||||
return AudiosRepositoryImpl(audiosApi, audiosDraftsDao)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
||||
@ -1,11 +1,24 @@
|
||||
package com.isolaatti.audio.common.data
|
||||
|
||||
import okhttp3.MultipartBody
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Multipart
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Part
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface AudiosApi {
|
||||
companion object {
|
||||
const val AudioParam= "audioFile"
|
||||
const val NameParam = "name"
|
||||
const val DurationParam = "duration"
|
||||
}
|
||||
@GET("/api/Audios/OfUser/{userId}")
|
||||
fun getAudiosOfUser(@Path("userId") userId: Int, @Query("lastAudioId") lastAudioId: String?): Call<AudiosDto>
|
||||
|
||||
@POST("/api/Audios/Create")
|
||||
@Multipart
|
||||
fun uploadFile(@Part file: MultipartBody.Part, @Part name: MultipartBody.Part, @Part duration: MultipartBody.Part): Call<AudioDto>
|
||||
}
|
||||
@ -1,15 +1,24 @@
|
||||
package com.isolaatti.audio.common.data
|
||||
|
||||
import android.util.Log
|
||||
import com.isolaatti.MyApplication
|
||||
import com.isolaatti.audio.common.domain.Audio
|
||||
import com.isolaatti.audio.common.domain.AudiosRepository
|
||||
import com.isolaatti.audio.drafts.data.AudiosDraftsDao
|
||||
import com.isolaatti.utils.Resource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.RequestBody.Companion.asRequestBody
|
||||
import retrofit2.awaitResponse
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class AudiosRepositoryImpl @Inject constructor(private val audiosApi: AudiosApi) : AudiosRepository {
|
||||
class AudiosRepositoryImpl @Inject constructor(private val audiosApi: AudiosApi, private val audiosDraftsDao: AudiosDraftsDao) : AudiosRepository {
|
||||
companion object {
|
||||
const val LOG_TAG = "AudiosRepositoryImpl"
|
||||
}
|
||||
override fun getAudiosOfUser(userId: Int, lastId: String?): Flow<Resource<List<Audio>>> = flow {
|
||||
emit(Resource.Loading())
|
||||
try {
|
||||
@ -29,4 +38,46 @@ class AudiosRepositoryImpl @Inject constructor(private val audiosApi: AudiosApi)
|
||||
emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
|
||||
}
|
||||
}
|
||||
|
||||
override fun uploadAudio(draftId: Long): Flow<Resource<Audio>> = flow {
|
||||
val audioDraftEntity = audiosDraftsDao.getAudioDraftById(draftId)
|
||||
if(audioDraftEntity == null) {
|
||||
emit(Resource.Error(Resource.Error.ErrorType.NotFoundError, "draft not found"))
|
||||
return@flow
|
||||
}
|
||||
|
||||
val file = File(MyApplication.myApp.filesDir, audioDraftEntity.audioLocalPath)
|
||||
|
||||
if(!file.exists()) {
|
||||
// remove draft, file was removed from file system for some reason
|
||||
audiosDraftsDao.deleteDrafts(arrayOf(audioDraftEntity))
|
||||
emit(Resource.Error(Resource.Error.ErrorType.OtherError, "File not found"))
|
||||
return@flow
|
||||
}
|
||||
|
||||
try {
|
||||
val response = audiosApi.uploadFile(
|
||||
MultipartBody.Part.createFormData(
|
||||
AudiosApi.AudioParam, // api parameter name
|
||||
audioDraftEntity.audioLocalPath.split("/")[1],
|
||||
file.asRequestBody("audio/3gpp".toMediaType()) // actual file to be sent
|
||||
),
|
||||
MultipartBody.Part.createFormData(AudiosApi.NameParam, audioDraftEntity.name),
|
||||
MultipartBody.Part.createFormData(AudiosApi.DurationParam, "0")
|
||||
).awaitResponse()
|
||||
|
||||
if(response.isSuccessful) {
|
||||
val audioDto = response.body()
|
||||
if(audioDto != null) {
|
||||
Log.d(LOG_TAG, "emit audio dto")
|
||||
emit(Resource.Success(Audio.fromDto(audioDto)))
|
||||
}
|
||||
|
||||
} else {
|
||||
emit(Resource.Error(Resource.Error.mapErrorCode(response.code())))
|
||||
}
|
||||
} catch(e: Exception) {
|
||||
Log.d(LOG_TAG, e.message.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5,4 +5,6 @@ import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface AudiosRepository {
|
||||
fun getAudiosOfUser(userId: Int, lastId: String?): Flow<Resource<List<Audio>>>
|
||||
|
||||
fun uploadAudio(draftId: Long): Flow<Resource<Audio>>
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.isolaatti.audio.common.domain
|
||||
|
||||
import com.isolaatti.utils.Resource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
class UploadAudioUC @Inject constructor(private val audiosRepository: AudiosRepository) {
|
||||
operator fun invoke(draftId: Long): Flow<Resource<Audio>> = audiosRepository.uploadAudio(draftId)
|
||||
}
|
||||
@ -7,5 +7,6 @@ import androidx.room.PrimaryKey
|
||||
data class AudioDraftEntity(
|
||||
@PrimaryKey(autoGenerate = true) val id: Long,
|
||||
val name: String,
|
||||
val audioLocalPath: String
|
||||
val audioLocalPath: String,
|
||||
val sizeInBytes: Long
|
||||
)
|
||||
@ -4,14 +4,15 @@ import android.util.Log
|
||||
import com.isolaatti.MyApplication
|
||||
import com.isolaatti.audio.drafts.domain.AudioDraft
|
||||
import com.isolaatti.audio.drafts.domain.repository.AudioDraftsRepository
|
||||
import com.isolaatti.utils.Resource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import java.io.File
|
||||
|
||||
class AudioDraftsRepositoryImpl(private val audiosDraftsDao: AudiosDraftsDao) : AudioDraftsRepository {
|
||||
override fun saveAudioDraft(name: String, relativePath: String): Flow<AudioDraft> = flow {
|
||||
val entity = AudioDraftEntity(0, name, relativePath)
|
||||
val insertedEntityId = audiosDraftsDao.insertAudioDraft(AudioDraftEntity(0, name, relativePath))
|
||||
override fun saveAudioDraft(name: String, relativePath: String, size: Long): Flow<AudioDraft> = flow {
|
||||
val entity = AudioDraftEntity(0, name, relativePath, size)
|
||||
val insertedEntityId = audiosDraftsDao.insertAudioDraft(entity)
|
||||
|
||||
emit(AudioDraft.fromEntity(entity.copy(id = insertedEntityId)))
|
||||
}
|
||||
@ -39,4 +40,14 @@ class AudioDraftsRepositoryImpl(private val audiosDraftsDao: AudiosDraftsDao) :
|
||||
|
||||
emit(rowsAffected > 0)
|
||||
}
|
||||
|
||||
override fun getAudioDraftById(draftId: Long): Flow<Resource<AudioDraft>> = flow {
|
||||
val audioDraft = audiosDraftsDao.getAudioDraftById(draftId)
|
||||
|
||||
if(audioDraft == null) {
|
||||
emit(Resource.Error(Resource.Error.ErrorType.NotFoundError, "Audio draft not found"))
|
||||
} else {
|
||||
emit(Resource.Success(AudioDraft.fromEntity(audioDraft)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -12,7 +12,7 @@ interface AudiosDraftsDao {
|
||||
suspend fun insertAudioDraft(audioDraftEntity: AudioDraftEntity): Long
|
||||
|
||||
@Query("SELECT * FROM audio_drafts WHERE id = :draftId")
|
||||
suspend fun getAudioDraftById(draftId: Long): AudioDraftEntity
|
||||
suspend fun getAudioDraftById(draftId: Long): AudioDraftEntity?
|
||||
|
||||
@Query("SELECT * FROM audio_drafts WHERE id in (:draftId)")
|
||||
suspend fun getAudioDraftsByIds(draftId: LongArray): Array<AudioDraftEntity>
|
||||
|
||||
@ -7,7 +7,7 @@ import com.isolaatti.audio.common.domain.Playable
|
||||
import com.isolaatti.audio.drafts.data.AudioDraftEntity
|
||||
import java.io.File
|
||||
|
||||
data class AudioDraft(val id: Long, val name: String, val localStorageRelativePath: String) : Playable() {
|
||||
data class AudioDraft(val id: Long, val name: String, val localStorageRelativePath: String, val size: Long) : Playable() {
|
||||
override val thumbnail: String?
|
||||
get() = null
|
||||
|
||||
@ -19,7 +19,7 @@ data class AudioDraft(val id: Long, val name: String, val localStorageRelativePa
|
||||
|
||||
companion object {
|
||||
fun fromEntity(audioDraftEntity: AudioDraftEntity): AudioDraft {
|
||||
return AudioDraft(audioDraftEntity.id, audioDraftEntity.name, audioDraftEntity.audioLocalPath)
|
||||
return AudioDraft(audioDraftEntity.id, audioDraftEntity.name, audioDraftEntity.audioLocalPath, audioDraftEntity.sizeInBytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,14 +1,17 @@
|
||||
package com.isolaatti.audio.drafts.domain.repository
|
||||
|
||||
import com.isolaatti.audio.drafts.domain.AudioDraft
|
||||
import com.isolaatti.utils.Resource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface AudioDraftsRepository {
|
||||
fun saveAudioDraft(name: String, relativePath: String): Flow<AudioDraft>
|
||||
fun saveAudioDraft(name: String, relativePath: String, size: Long): Flow<AudioDraft>
|
||||
|
||||
fun getAudioDrafts(): Flow<List<AudioDraft>>
|
||||
|
||||
fun deleteDrafts(draftIds: LongArray): Flow<Boolean>
|
||||
|
||||
fun renameDraft(draftId: Long, name: String): Flow<Boolean>
|
||||
|
||||
fun getAudioDraftById(draftId: Long): Flow<Resource<AudioDraft>>
|
||||
}
|
||||
@ -6,7 +6,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
class SaveAudioDraft @Inject constructor(private val audioDraftsRepository: AudioDraftsRepository) {
|
||||
operator fun invoke(name: String, relativePath: String): Flow<AudioDraft> {
|
||||
return audioDraftsRepository.saveAudioDraft(name, relativePath)
|
||||
operator fun invoke(name: String, relativePath: String, size: Long): Flow<AudioDraft> {
|
||||
return audioDraftsRepository.saveAudioDraft(name, relativePath, size)
|
||||
}
|
||||
}
|
||||
@ -8,8 +8,14 @@ import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.PlaybackException
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.datasource.DefaultDataSource
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.drm.DrmSessionManagerProvider
|
||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||
import androidx.media3.exoplayer.source.MediaSource
|
||||
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy
|
||||
import com.isolaatti.audio.common.domain.Audio
|
||||
import com.isolaatti.audio.common.domain.Playable
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@ -63,6 +69,9 @@ class AudioPlayerConnector(
|
||||
}
|
||||
|
||||
private val playerListener: Player.Listener = object: Player.Listener {
|
||||
override fun onPlayerError(error: PlaybackException) {
|
||||
Log.e(TAG, error.message.toString())
|
||||
}
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
if(audio != null) {
|
||||
listeners.forEach { listener -> listener.onPlaying(isPlaying, audio!!)}
|
||||
@ -119,7 +128,6 @@ class AudioPlayerConnector(
|
||||
|
||||
private fun initializePlayer() {
|
||||
player = ExoPlayer.Builder(context).build()
|
||||
player?.playWhenReady = true
|
||||
player?.addListener(playerListener)
|
||||
player?.prepare()
|
||||
}
|
||||
@ -137,16 +145,14 @@ class AudioPlayerConnector(
|
||||
}
|
||||
|
||||
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
|
||||
Log.d(TAG, event.toString())
|
||||
when(event) {
|
||||
Lifecycle.Event.ON_START -> {
|
||||
initializePlayer()
|
||||
}
|
||||
Lifecycle.Event.ON_RESUME -> {
|
||||
if(player == null) {
|
||||
initializePlayer()
|
||||
}
|
||||
}
|
||||
Lifecycle.Event.ON_STOP, Lifecycle.Event.ON_DESTROY -> {
|
||||
Lifecycle.Event.ON_DESTROY -> {
|
||||
releasePlayer()
|
||||
listeners.clear()
|
||||
}
|
||||
@ -173,6 +179,7 @@ class AudioPlayerConnector(
|
||||
mediaItem = MediaItem.fromUri(audio.uri)
|
||||
|
||||
player?.setMediaItem(mediaItem!!)
|
||||
player?.playWhenReady = true
|
||||
}
|
||||
|
||||
fun stopPlayback() {
|
||||
@ -191,20 +198,9 @@ class AudioPlayerConnector(
|
||||
|
||||
open class DefaultListener() : Listener {
|
||||
override fun onPlaying(isPlaying: Boolean, audio: Playable) {}
|
||||
|
||||
override fun isLoading(isLoading: Boolean, audio: Playable) {}
|
||||
|
||||
override fun progressChanged(second: Int, audio: Playable) {
|
||||
|
||||
}
|
||||
|
||||
override fun durationChanged(duration: Int, audio: Playable) {
|
||||
|
||||
}
|
||||
|
||||
override fun onEnded(audio: Playable) {
|
||||
|
||||
}
|
||||
|
||||
override fun progressChanged(second: Int, audio: Playable) {}
|
||||
override fun durationChanged(duration: Int, audio: Playable) {}
|
||||
override fun onEnded(audio: Playable) {}
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
@ -23,6 +24,7 @@ class AudioRecorderViewModel @Inject constructor(
|
||||
validate()
|
||||
}
|
||||
var relativePath = ""
|
||||
var size: Long = 0
|
||||
private var audioRecorded = false
|
||||
|
||||
val audioDraft: MutableLiveData<AudioDraft> = MutableLiveData()
|
||||
@ -44,7 +46,7 @@ class AudioRecorderViewModel @Inject constructor(
|
||||
}
|
||||
fun saveAudioDraft() {
|
||||
viewModelScope.launch {
|
||||
saveAudioDraftUC(name, relativePath).onEach {
|
||||
saveAudioDraftUC(name, relativePath, size).onEach {
|
||||
audioDraft.postValue(it)
|
||||
}.flowOn(Dispatchers.IO).launchIn(this)
|
||||
}
|
||||
|
||||
@ -191,6 +191,13 @@ class AudioRecorderActivity : AppCompatActivity() {
|
||||
binding.toolbar.setOnMenuItemClickListener {
|
||||
when(it.itemId) {
|
||||
R.id.save_draft -> {
|
||||
|
||||
viewModel.relativePath = "/audios/${UUID.randomUUID()}.3gp"
|
||||
File(outputFile).run {
|
||||
viewModel.size = length()
|
||||
copyTo(File(filesDir.absolutePath, viewModel.relativePath), overwrite = true)
|
||||
}
|
||||
|
||||
viewModel.saveAudioDraft()
|
||||
true
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ import com.isolaatti.auth.data.local.UserInfoEntity
|
||||
import com.isolaatti.settings.data.KeyValueDao
|
||||
import com.isolaatti.settings.data.KeyValueEntity
|
||||
|
||||
@Database(entities = [KeyValueEntity::class, UserInfoEntity::class, AudioDraftEntity::class], version = 4)
|
||||
@Database(entities = [KeyValueEntity::class, UserInfoEntity::class, AudioDraftEntity::class], version = 5)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun keyValueDao(): KeyValueDao
|
||||
abstract fun userInfoDao(): UserInfoDao
|
||||
|
||||
@ -2,6 +2,7 @@ package com.isolaatti.posting.posts.domain.entity
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.isolaatti.audio.common.domain.Audio
|
||||
import com.isolaatti.common.Ownable
|
||||
import com.isolaatti.posting.posts.data.remote.FeedDto
|
||||
import java.io.Serializable
|
||||
@ -18,7 +19,8 @@ data class Post(
|
||||
var numberOfComments: Int,
|
||||
val userName: String,
|
||||
val squadName: String?,
|
||||
var liked: Boolean
|
||||
var liked: Boolean,
|
||||
val audio: Audio? = null
|
||||
) : Ownable, Parcelable {
|
||||
constructor(parcel: Parcel) : this(
|
||||
parcel.readLong(),
|
||||
@ -32,7 +34,8 @@ data class Post(
|
||||
parcel.readInt(),
|
||||
parcel.readString()!!,
|
||||
parcel.readString(),
|
||||
parcel.readByte() != 0.toByte()
|
||||
parcel.readByte() != 0.toByte(),
|
||||
parcel.readSerializable() as? Audio
|
||||
) {
|
||||
}
|
||||
|
||||
@ -98,6 +101,7 @@ data class Post(
|
||||
parcel.writeString(userName)
|
||||
parcel.writeString(squadName)
|
||||
parcel.writeByte(if (liked) 1 else 0)
|
||||
parcel.writeSerializable(audio)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
|
||||
@ -1,8 +1,13 @@
|
||||
package com.isolaatti.posting.posts.presentation
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.isolaatti.audio.common.domain.Audio
|
||||
import com.isolaatti.audio.common.domain.Playable
|
||||
import com.isolaatti.audio.common.domain.UploadAudioUC
|
||||
import com.isolaatti.audio.drafts.domain.repository.AudioDraftsRepository
|
||||
import com.isolaatti.posting.posts.data.remote.CreatePostDto
|
||||
import com.isolaatti.posting.posts.data.remote.EditPostDto
|
||||
import com.isolaatti.posting.posts.data.remote.EditPostDto.Companion.PRIVACY_ISOLAATTI
|
||||
@ -13,6 +18,7 @@ import com.isolaatti.posting.posts.domain.use_case.LoadSinglePost
|
||||
import com.isolaatti.posting.posts.domain.use_case.MakePost
|
||||
import com.isolaatti.utils.Resource
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@ -21,30 +27,97 @@ import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class CreatePostViewModel @Inject constructor(private val makePost: MakePost, private val editPost: EditPost, private val loadPost: LoadSinglePost) : ViewModel() {
|
||||
class CreatePostViewModel @Inject constructor(
|
||||
private val makePost: MakePost,
|
||||
private val editPost: EditPost,
|
||||
private val loadPost: LoadSinglePost,
|
||||
private val audioDraftsRepository: AudioDraftsRepository,
|
||||
private val uploadAudioUC: UploadAudioUC
|
||||
) : ViewModel() {
|
||||
|
||||
companion object {
|
||||
const val LOG_TAG = "CreatePostViewModel"
|
||||
}
|
||||
|
||||
val validation: MutableLiveData<Boolean> = MutableLiveData(false)
|
||||
val posted: MutableLiveData<Post?> = MutableLiveData()
|
||||
val error: MutableLiveData<Resource.Error.ErrorType?> = MutableLiveData()
|
||||
val loading: MutableLiveData<Boolean> = MutableLiveData(false)
|
||||
val sendingPost: MutableLiveData<Boolean> = MutableLiveData(false)
|
||||
val postToEdit: MutableLiveData<EditPostDto> = MutableLiveData()
|
||||
val liveContent: MutableLiveData<String> = MutableLiveData()
|
||||
var content: String = ""
|
||||
set(value) {field = value; liveContent.value = value} // TODO remove this and use only liveContent
|
||||
|
||||
fun postDiscussion() {
|
||||
val audioAttachment: MutableLiveData<Playable?> = MutableLiveData()
|
||||
|
||||
private var audioDraft: Long? = null
|
||||
private var audioId: String? = null
|
||||
|
||||
/**
|
||||
* postDiscussion() and editDiscussion() will check for audios pending to upload (drafts). It will
|
||||
* upload it (if any) and then send the request to post.
|
||||
*/
|
||||
|
||||
private fun sendDiscussion() {
|
||||
Log.d(LOG_TAG, "postDiscussion#send()")
|
||||
viewModelScope.launch {
|
||||
makePost(EditPostDto.PRIVACY_ISOLAATTI, content, null, null).onEach {
|
||||
makePost(EditPostDto.PRIVACY_ISOLAATTI, content, audioId, null).onEach {
|
||||
when(it) {
|
||||
is Resource.Success -> {
|
||||
loading.postValue(false)
|
||||
sendingPost.postValue(false)
|
||||
posted.postValue(Post.fromPostDto(it.data!!))
|
||||
}
|
||||
is Resource.Error -> {
|
||||
loading.postValue(false)
|
||||
sendingPost.postValue(false)
|
||||
error.postValue(it.errorType)
|
||||
}
|
||||
is Resource.Loading -> {
|
||||
loading.postValue(true)
|
||||
sendingPost.postValue(true)
|
||||
}
|
||||
}
|
||||
}.flowOn(Dispatchers.IO).launchIn(this)
|
||||
}
|
||||
}
|
||||
fun postDiscussion() {
|
||||
|
||||
viewModelScope.launch {
|
||||
|
||||
if(audioDraft != null) {
|
||||
uploadAudioUC(audioDraft!!).onEach { upLoadAudioResource ->
|
||||
Log.d(LOG_TAG, upLoadAudioResource.toString())
|
||||
when(upLoadAudioResource) {
|
||||
is Resource.Success -> {
|
||||
Log.d(LOG_TAG, "uploadAudioResource: Success")
|
||||
audioId = upLoadAudioResource.data?.id
|
||||
sendDiscussion()
|
||||
}
|
||||
|
||||
is Resource.Error -> {}
|
||||
is Resource.Loading -> {
|
||||
sendingPost.postValue(true)
|
||||
}
|
||||
}
|
||||
}.flowOn(Dispatchers.IO).launchIn(this)
|
||||
} else {
|
||||
sendDiscussion()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendEditDiscussion(postId: Long) {
|
||||
viewModelScope.launch {
|
||||
editPost(postId, EditPostDto.PRIVACY_ISOLAATTI, content, audioId, null).onEach {
|
||||
when(it) {
|
||||
is Resource.Success -> {
|
||||
sendingPost.postValue(false)
|
||||
posted.postValue(Post.fromPostDto(it.data!!))
|
||||
}
|
||||
is Resource.Error -> {
|
||||
sendingPost.postValue(false)
|
||||
error.postValue(it.errorType)
|
||||
}
|
||||
is Resource.Loading -> {
|
||||
sendingPost.postValue(true)
|
||||
}
|
||||
}
|
||||
}.flowOn(Dispatchers.IO).launchIn(this)
|
||||
@ -52,22 +125,25 @@ class CreatePostViewModel @Inject constructor(private val makePost: MakePost, pr
|
||||
}
|
||||
|
||||
fun editDiscussion(postId: Long) {
|
||||
|
||||
viewModelScope.launch {
|
||||
editPost(postId, EditPostDto.PRIVACY_ISOLAATTI, content, null, null).onEach {
|
||||
when(it) {
|
||||
is Resource.Success -> {
|
||||
loading.postValue(false)
|
||||
posted.postValue(Post.fromPostDto(it.data!!))
|
||||
if(audioDraft != null) {
|
||||
uploadAudioUC(audioDraft!!).onEach { upLoadAudioResource ->
|
||||
when(upLoadAudioResource) {
|
||||
is Resource.Success -> {
|
||||
audioId = upLoadAudioResource.data?.id
|
||||
sendEditDiscussion(postId)
|
||||
}
|
||||
|
||||
is Resource.Error -> {}
|
||||
is Resource.Loading -> {
|
||||
sendingPost.postValue(true)
|
||||
}
|
||||
}
|
||||
is Resource.Error -> {
|
||||
loading.postValue(false)
|
||||
error.postValue(it.errorType)
|
||||
}
|
||||
is Resource.Loading -> {
|
||||
loading.postValue(true)
|
||||
}
|
||||
}
|
||||
}.flowOn(Dispatchers.IO).launchIn(this)
|
||||
}.flowOn(Dispatchers.IO).launchIn(this)
|
||||
} else {
|
||||
sendEditDiscussion(postId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,10 +153,38 @@ class CreatePostViewModel @Inject constructor(private val makePost: MakePost, pr
|
||||
if(postRes is Resource.Success) {
|
||||
postRes.data?.let {
|
||||
postToEdit.postValue(EditPostDto(PRIVACY_ISOLAATTI, content = it.textContent, postId = it.id))
|
||||
it.audio?.let { audio -> audioAttachment.postValue(audio) }
|
||||
}
|
||||
}
|
||||
}.flowOn(Dispatchers.IO).launchIn(this)
|
||||
}
|
||||
}
|
||||
|
||||
// call this when user has recorded or selected a draft
|
||||
fun putAudioDraft(draftId: Long) {
|
||||
viewModelScope.launch {
|
||||
audioDraftsRepository.getAudioDraftById(draftId).onEach { draft ->
|
||||
when(draft) {
|
||||
is Resource.Error -> {}
|
||||
is Resource.Loading -> {}
|
||||
is Resource.Success -> {
|
||||
audioAttachment.postValue(draft.data)
|
||||
this@CreatePostViewModel.audioDraft = draftId
|
||||
}
|
||||
}
|
||||
|
||||
}.flowOn(Dispatchers.IO).launchIn(this)
|
||||
}
|
||||
}
|
||||
|
||||
// call this when user selects an existing audio (not a draft)
|
||||
fun putAudio(audio: Audio) {
|
||||
audioAttachment.value = audio
|
||||
}
|
||||
|
||||
// clear user input
|
||||
fun removeAudio() {
|
||||
audioAttachment.value = null
|
||||
}
|
||||
|
||||
}
|
||||
@ -5,12 +5,10 @@ import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.navigation.findNavController
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.isolaatti.R
|
||||
import com.isolaatti.common.IsolaattiBaseActivity
|
||||
@ -129,7 +127,7 @@ class CreatePostActivity : IsolaattiBaseActivity() {
|
||||
finish()
|
||||
}
|
||||
|
||||
viewModel.loading.observe(this@CreatePostActivity) {
|
||||
viewModel.sendingPost.observe(this@CreatePostActivity) {
|
||||
binding.progressBarLoading.visibility = if(it) View.VISIBLE else View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,16 +1,24 @@
|
||||
package com.isolaatti.posting.posts.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import com.isolaatti.audio.recorder.ui.AudioRecorderActivity
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.isolaatti.R
|
||||
import com.isolaatti.audio.common.domain.Audio
|
||||
import com.isolaatti.audio.common.domain.Playable
|
||||
import com.isolaatti.audio.drafts.domain.AudioDraft
|
||||
import com.isolaatti.audio.player.AudioPlayerConnector
|
||||
import com.isolaatti.audio.recorder.ui.AudioRecorderContract
|
||||
import com.isolaatti.databinding.FragmentMarkdownEditingBinding
|
||||
import com.isolaatti.images.image_chooser.ui.ImageChooserContract
|
||||
@ -19,13 +27,21 @@ import com.isolaatti.posting.link_creator.ui.LinkCreatorFragment
|
||||
import com.isolaatti.posting.posts.presentation.CreatePostViewModel
|
||||
|
||||
class MarkdownEditingFragment : Fragment(){
|
||||
companion object {
|
||||
const val LOG_TAG = "MarkdownEditingFragment"
|
||||
}
|
||||
|
||||
|
||||
private lateinit var binding: FragmentMarkdownEditingBinding
|
||||
private val viewModel: CreatePostViewModel by activityViewModels()
|
||||
private val linkCreatorViewModel: LinkCreatorViewModel by viewModels()
|
||||
|
||||
private var audioPlayerConnector: AudioPlayerConnector? = null
|
||||
|
||||
private val audioRecorderLauncher = registerForActivityResult(AudioRecorderContract()) { draftId ->
|
||||
if(draftId != null) {
|
||||
|
||||
viewModel.putAudioDraft(draftId)
|
||||
binding.viewAnimator.displayedChild = 1
|
||||
}
|
||||
}
|
||||
|
||||
@ -40,6 +56,34 @@ class MarkdownEditingFragment : Fragment(){
|
||||
|
||||
}
|
||||
|
||||
private val audioListener = object: AudioPlayerConnector.Listener {
|
||||
override fun durationChanged(duration: Int, audio: Playable) {
|
||||
binding.audioItem.audioProgress.max = duration
|
||||
}
|
||||
|
||||
override fun onEnded(audio: Playable) {
|
||||
binding.audioItem.audioProgress.progress = 0
|
||||
}
|
||||
|
||||
override fun progressChanged(second: Int, audio: Playable) {
|
||||
binding.audioItem.audioProgress.progress = second
|
||||
}
|
||||
|
||||
override fun onPlaying(isPlaying: Boolean, audio: Playable) {
|
||||
binding.audioItem.playButton.icon = AppCompatResources.getDrawable(requireContext(), if(isPlaying) R.drawable.baseline_pause_circle_24 else R.drawable.baseline_play_circle_24)
|
||||
}
|
||||
|
||||
override fun isLoading(isLoading: Boolean, audio: Playable) {
|
||||
binding.audioItem.audioProgress.isIndeterminate = isLoading
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
audioPlayerConnector = AudioPlayerConnector(requireContext())
|
||||
audioPlayerConnector?.addListener(audioListener)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
@ -53,11 +97,23 @@ class MarkdownEditingFragment : Fragment(){
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
viewLifecycleOwner.lifecycle.addObserver(audioPlayerConnector!!)
|
||||
|
||||
viewLifecycleOwner.lifecycle.addObserver(object: LifecycleEventObserver {
|
||||
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
|
||||
Log.d(LOG_TAG, event.toString())
|
||||
}
|
||||
})
|
||||
|
||||
setupListeners()
|
||||
setupObservers()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
viewLifecycleOwner.lifecycle.removeObserver(audioPlayerConnector!!)
|
||||
}
|
||||
|
||||
private fun setupListeners() {
|
||||
binding.filledTextField.editText?.setText(viewModel.content)
|
||||
binding.filledTextField.requestFocus()
|
||||
@ -74,7 +130,30 @@ class MarkdownEditingFragment : Fragment(){
|
||||
}
|
||||
|
||||
binding.addAudioButton.setOnClickListener {
|
||||
audioRecorderLauncher.launch(null)
|
||||
|
||||
val popupMenu = PopupMenu(requireContext(), it)
|
||||
|
||||
popupMenu.inflate(R.menu.attach_audio_menu)
|
||||
|
||||
popupMenu.setOnMenuItemClickListener { menuItem ->
|
||||
when(menuItem.itemId){
|
||||
R.id.record_new_audio_item -> {
|
||||
audioRecorderLauncher.launch(null)
|
||||
true
|
||||
}
|
||||
R.id.select_from_audios -> {
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
popupMenu.show()
|
||||
}
|
||||
|
||||
binding.removeAudio.setOnClickListener {
|
||||
binding.viewAnimator.displayedChild = 0
|
||||
viewModel.removeAudio()
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,8 +168,27 @@ class MarkdownEditingFragment : Fragment(){
|
||||
linkCreatorViewModel.inserted.value = false
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.audioAttachment.observe(viewLifecycleOwner) { playable ->
|
||||
if(playable != null) {
|
||||
audioPlayerConnector?.stopPlayback()
|
||||
binding.audioItem.playButton.setOnClickListener {
|
||||
audioPlayerConnector?.playPauseAudio(playable)
|
||||
}
|
||||
|
||||
when(playable) {
|
||||
is Audio -> {
|
||||
binding.audioItem.textViewDescription.text = playable.name
|
||||
}
|
||||
is AudioDraft -> {
|
||||
binding.audioItem.textViewDescription.text = playable.name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun insertImage() {
|
||||
imageChooserLauncher.launch(ImageChooserContract.Requester.UserPost)
|
||||
}
|
||||
|
||||
62
app/src/main/res/layout/audio_attachment.xml
Normal file
62
app/src/main/res/layout/audio_attachment.xml
Normal file
@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/play_button_container"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/audio_progress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/play_button"
|
||||
style="?attr/materialIconButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
app:icon="@drawable/baseline_play_circle_24" />
|
||||
</RelativeLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_view_description"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:maxLines="4"
|
||||
android:textAlignment="center"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/description_options_button"
|
||||
app:layout_constraintStart_toEndOf="@id/play_button_container"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Hi there, I am software developer!" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/description_options_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
style="?attr/materialIconButtonStyle"
|
||||
android:visibility="gone"
|
||||
app:icon="@drawable/baseline_more_horiz_24"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
@ -55,12 +55,36 @@
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Audio attachment"/>
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/add_audio_button"
|
||||
android:text="@string/audio_attachment"/>
|
||||
<ViewAnimator
|
||||
android:id="@+id/view_animator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Attach audio" />
|
||||
android:layout_marginTop="4dp">
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/add_audio_button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:text="Attach audio" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<include android:id="@+id/audio_item" layout="@layout/audio_attachment"/>
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/remove_audio"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/audio_item"
|
||||
android:layout_marginTop="4dp"
|
||||
style="?attr/materialIconButtonFilledTonalStyle"
|
||||
app:icon="@drawable/baseline_close_24"
|
||||
android:text="@string/remove"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ViewAnimator>
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
</LinearLayout>
|
||||
|
||||
7
app/src/main/res/menu/attach_audio_menu.xml
Normal file
7
app/src/main/res/menu/attach_audio_menu.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:id="@+id/record_new_audio_item"
|
||||
android:title="@string/record_new_audio"/>
|
||||
<item android:id="@+id/select_from_audios"
|
||||
android:title="@string/select_from_existing"/>
|
||||
</menu>
|
||||
@ -179,4 +179,8 @@
|
||||
<string name="new_password_is_invalid">New password is invalid. Please check it meets the requirements</string>
|
||||
<string name="rename">Rename</string>
|
||||
<string name="set_as_profile_audio">Set as profile audio</string>
|
||||
<string name="record_new_audio">Record new audio</string>
|
||||
<string name="select_from_existing">Select from existing</string>
|
||||
<string name="audio_attachment">Audio attachment</string>
|
||||
<string name="remove">Remove</string>
|
||||
</resources>
|
||||
@ -17,4 +17,6 @@ plugins {
|
||||
id 'com.android.library' version '8.2.2' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '1.8.0' apply false
|
||||
id 'com.google.dagger.hilt.android' version '2.47' apply false
|
||||
id 'com.google.gms.google-services' version '4.4.0' apply false
|
||||
id 'com.google.firebase.crashlytics' version '2.9.9' apply false
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user