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 'org.jetbrains.kotlin.plugin.serialization' version '1.8.0'
|
||||||
id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
|
id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
|
||||||
id 'androidx.navigation.safeargs.kotlin'
|
id 'androidx.navigation.safeargs.kotlin'
|
||||||
|
id 'com.google.gms.google-services'
|
||||||
|
id 'com.google.firebase.crashlytics'
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@ -125,4 +127,9 @@ dependencies {
|
|||||||
// QR
|
// QR
|
||||||
implementation 'com.github.androidmads:QRGenerator:1.0.1'
|
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
|
@Provides
|
||||||
fun provideAudiosRepository(audiosApi: AudiosApi): AudiosRepository {
|
fun provideAudiosRepository(audiosApi: AudiosApi, audiosDraftsDao: AudiosDraftsDao): AudiosRepository {
|
||||||
return AudiosRepositoryImpl(audiosApi)
|
return AudiosRepositoryImpl(audiosApi, audiosDraftsDao)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|||||||
@ -1,11 +1,24 @@
|
|||||||
package com.isolaatti.audio.common.data
|
package com.isolaatti.audio.common.data
|
||||||
|
|
||||||
|
import okhttp3.MultipartBody
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Multipart
|
||||||
|
import retrofit2.http.POST
|
||||||
|
import retrofit2.http.Part
|
||||||
import retrofit2.http.Path
|
import retrofit2.http.Path
|
||||||
import retrofit2.http.Query
|
import retrofit2.http.Query
|
||||||
|
|
||||||
interface AudiosApi {
|
interface AudiosApi {
|
||||||
|
companion object {
|
||||||
|
const val AudioParam= "audioFile"
|
||||||
|
const val NameParam = "name"
|
||||||
|
const val DurationParam = "duration"
|
||||||
|
}
|
||||||
@GET("/api/Audios/OfUser/{userId}")
|
@GET("/api/Audios/OfUser/{userId}")
|
||||||
fun getAudiosOfUser(@Path("userId") userId: Int, @Query("lastAudioId") lastAudioId: String?): Call<AudiosDto>
|
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
|
package com.isolaatti.audio.common.data
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.isolaatti.MyApplication
|
||||||
import com.isolaatti.audio.common.domain.Audio
|
import com.isolaatti.audio.common.domain.Audio
|
||||||
import com.isolaatti.audio.common.domain.AudiosRepository
|
import com.isolaatti.audio.common.domain.AudiosRepository
|
||||||
|
import com.isolaatti.audio.drafts.data.AudiosDraftsDao
|
||||||
import com.isolaatti.utils.Resource
|
import com.isolaatti.utils.Resource
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
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 retrofit2.awaitResponse
|
||||||
|
import java.io.File
|
||||||
import javax.inject.Inject
|
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 {
|
override fun getAudiosOfUser(userId: Int, lastId: String?): Flow<Resource<List<Audio>>> = flow {
|
||||||
emit(Resource.Loading())
|
emit(Resource.Loading())
|
||||||
try {
|
try {
|
||||||
@ -29,4 +38,46 @@ class AudiosRepositoryImpl @Inject constructor(private val audiosApi: AudiosApi)
|
|||||||
emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
|
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 {
|
interface AudiosRepository {
|
||||||
fun getAudiosOfUser(userId: Int, lastId: String?): Flow<Resource<List<Audio>>>
|
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(
|
data class AudioDraftEntity(
|
||||||
@PrimaryKey(autoGenerate = true) val id: Long,
|
@PrimaryKey(autoGenerate = true) val id: Long,
|
||||||
val name: String,
|
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.MyApplication
|
||||||
import com.isolaatti.audio.drafts.domain.AudioDraft
|
import com.isolaatti.audio.drafts.domain.AudioDraft
|
||||||
import com.isolaatti.audio.drafts.domain.repository.AudioDraftsRepository
|
import com.isolaatti.audio.drafts.domain.repository.AudioDraftsRepository
|
||||||
|
import com.isolaatti.utils.Resource
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class AudioDraftsRepositoryImpl(private val audiosDraftsDao: AudiosDraftsDao) : AudioDraftsRepository {
|
class AudioDraftsRepositoryImpl(private val audiosDraftsDao: AudiosDraftsDao) : AudioDraftsRepository {
|
||||||
override fun saveAudioDraft(name: String, relativePath: String): Flow<AudioDraft> = flow {
|
override fun saveAudioDraft(name: String, relativePath: String, size: Long): Flow<AudioDraft> = flow {
|
||||||
val entity = AudioDraftEntity(0, name, relativePath)
|
val entity = AudioDraftEntity(0, name, relativePath, size)
|
||||||
val insertedEntityId = audiosDraftsDao.insertAudioDraft(AudioDraftEntity(0, name, relativePath))
|
val insertedEntityId = audiosDraftsDao.insertAudioDraft(entity)
|
||||||
|
|
||||||
emit(AudioDraft.fromEntity(entity.copy(id = insertedEntityId)))
|
emit(AudioDraft.fromEntity(entity.copy(id = insertedEntityId)))
|
||||||
}
|
}
|
||||||
@ -39,4 +40,14 @@ class AudioDraftsRepositoryImpl(private val audiosDraftsDao: AudiosDraftsDao) :
|
|||||||
|
|
||||||
emit(rowsAffected > 0)
|
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
|
suspend fun insertAudioDraft(audioDraftEntity: AudioDraftEntity): Long
|
||||||
|
|
||||||
@Query("SELECT * FROM audio_drafts WHERE id = :draftId")
|
@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)")
|
@Query("SELECT * FROM audio_drafts WHERE id in (:draftId)")
|
||||||
suspend fun getAudioDraftsByIds(draftId: LongArray): Array<AudioDraftEntity>
|
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 com.isolaatti.audio.drafts.data.AudioDraftEntity
|
||||||
import java.io.File
|
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?
|
override val thumbnail: String?
|
||||||
get() = null
|
get() = null
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ data class AudioDraft(val id: Long, val name: String, val localStorageRelativePa
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromEntity(audioDraftEntity: AudioDraftEntity): AudioDraft {
|
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
|
package com.isolaatti.audio.drafts.domain.repository
|
||||||
|
|
||||||
import com.isolaatti.audio.drafts.domain.AudioDraft
|
import com.isolaatti.audio.drafts.domain.AudioDraft
|
||||||
|
import com.isolaatti.utils.Resource
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface AudioDraftsRepository {
|
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 getAudioDrafts(): Flow<List<AudioDraft>>
|
||||||
|
|
||||||
fun deleteDrafts(draftIds: LongArray): Flow<Boolean>
|
fun deleteDrafts(draftIds: LongArray): Flow<Boolean>
|
||||||
|
|
||||||
fun renameDraft(draftId: Long, name: String): 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
|
import javax.inject.Inject
|
||||||
|
|
||||||
class SaveAudioDraft @Inject constructor(private val audioDraftsRepository: AudioDraftsRepository) {
|
class SaveAudioDraft @Inject constructor(private val audioDraftsRepository: AudioDraftsRepository) {
|
||||||
operator fun invoke(name: String, relativePath: String): Flow<AudioDraft> {
|
operator fun invoke(name: String, relativePath: String, size: Long): Flow<AudioDraft> {
|
||||||
return audioDraftsRepository.saveAudioDraft(name, relativePath)
|
return audioDraftsRepository.saveAudioDraft(name, relativePath, size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -8,8 +8,14 @@ import androidx.lifecycle.Lifecycle
|
|||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
|
import androidx.media3.common.PlaybackException
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
|
import androidx.media3.datasource.DefaultDataSource
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
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.Audio
|
||||||
import com.isolaatti.audio.common.domain.Playable
|
import com.isolaatti.audio.common.domain.Playable
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@ -63,6 +69,9 @@ class AudioPlayerConnector(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val playerListener: Player.Listener = object: Player.Listener {
|
private val playerListener: Player.Listener = object: Player.Listener {
|
||||||
|
override fun onPlayerError(error: PlaybackException) {
|
||||||
|
Log.e(TAG, error.message.toString())
|
||||||
|
}
|
||||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||||
if(audio != null) {
|
if(audio != null) {
|
||||||
listeners.forEach { listener -> listener.onPlaying(isPlaying, audio!!)}
|
listeners.forEach { listener -> listener.onPlaying(isPlaying, audio!!)}
|
||||||
@ -119,7 +128,6 @@ class AudioPlayerConnector(
|
|||||||
|
|
||||||
private fun initializePlayer() {
|
private fun initializePlayer() {
|
||||||
player = ExoPlayer.Builder(context).build()
|
player = ExoPlayer.Builder(context).build()
|
||||||
player?.playWhenReady = true
|
|
||||||
player?.addListener(playerListener)
|
player?.addListener(playerListener)
|
||||||
player?.prepare()
|
player?.prepare()
|
||||||
}
|
}
|
||||||
@ -137,16 +145,14 @@ class AudioPlayerConnector(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
|
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
|
||||||
|
Log.d(TAG, event.toString())
|
||||||
when(event) {
|
when(event) {
|
||||||
Lifecycle.Event.ON_START -> {
|
|
||||||
initializePlayer()
|
|
||||||
}
|
|
||||||
Lifecycle.Event.ON_RESUME -> {
|
Lifecycle.Event.ON_RESUME -> {
|
||||||
if(player == null) {
|
if(player == null) {
|
||||||
initializePlayer()
|
initializePlayer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Lifecycle.Event.ON_STOP, Lifecycle.Event.ON_DESTROY -> {
|
Lifecycle.Event.ON_DESTROY -> {
|
||||||
releasePlayer()
|
releasePlayer()
|
||||||
listeners.clear()
|
listeners.clear()
|
||||||
}
|
}
|
||||||
@ -173,6 +179,7 @@ class AudioPlayerConnector(
|
|||||||
mediaItem = MediaItem.fromUri(audio.uri)
|
mediaItem = MediaItem.fromUri(audio.uri)
|
||||||
|
|
||||||
player?.setMediaItem(mediaItem!!)
|
player?.setMediaItem(mediaItem!!)
|
||||||
|
player?.playWhenReady = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopPlayback() {
|
fun stopPlayback() {
|
||||||
@ -191,20 +198,9 @@ class AudioPlayerConnector(
|
|||||||
|
|
||||||
open class DefaultListener() : Listener {
|
open class DefaultListener() : Listener {
|
||||||
override fun onPlaying(isPlaying: Boolean, audio: Playable) {}
|
override fun onPlaying(isPlaying: Boolean, audio: Playable) {}
|
||||||
|
|
||||||
override fun isLoading(isLoading: Boolean, audio: Playable) {}
|
override fun isLoading(isLoading: Boolean, audio: Playable) {}
|
||||||
|
override fun progressChanged(second: Int, audio: Playable) {}
|
||||||
override fun progressChanged(second: Int, audio: Playable) {
|
override fun durationChanged(duration: Int, audio: Playable) {}
|
||||||
|
override fun onEnded(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.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
@ -23,6 +24,7 @@ class AudioRecorderViewModel @Inject constructor(
|
|||||||
validate()
|
validate()
|
||||||
}
|
}
|
||||||
var relativePath = ""
|
var relativePath = ""
|
||||||
|
var size: Long = 0
|
||||||
private var audioRecorded = false
|
private var audioRecorded = false
|
||||||
|
|
||||||
val audioDraft: MutableLiveData<AudioDraft> = MutableLiveData()
|
val audioDraft: MutableLiveData<AudioDraft> = MutableLiveData()
|
||||||
@ -44,7 +46,7 @@ class AudioRecorderViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
fun saveAudioDraft() {
|
fun saveAudioDraft() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
saveAudioDraftUC(name, relativePath).onEach {
|
saveAudioDraftUC(name, relativePath, size).onEach {
|
||||||
audioDraft.postValue(it)
|
audioDraft.postValue(it)
|
||||||
}.flowOn(Dispatchers.IO).launchIn(this)
|
}.flowOn(Dispatchers.IO).launchIn(this)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -191,6 +191,13 @@ class AudioRecorderActivity : AppCompatActivity() {
|
|||||||
binding.toolbar.setOnMenuItemClickListener {
|
binding.toolbar.setOnMenuItemClickListener {
|
||||||
when(it.itemId) {
|
when(it.itemId) {
|
||||||
R.id.save_draft -> {
|
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()
|
viewModel.saveAudioDraft()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import com.isolaatti.auth.data.local.UserInfoEntity
|
|||||||
import com.isolaatti.settings.data.KeyValueDao
|
import com.isolaatti.settings.data.KeyValueDao
|
||||||
import com.isolaatti.settings.data.KeyValueEntity
|
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 class AppDatabase : RoomDatabase() {
|
||||||
abstract fun keyValueDao(): KeyValueDao
|
abstract fun keyValueDao(): KeyValueDao
|
||||||
abstract fun userInfoDao(): UserInfoDao
|
abstract fun userInfoDao(): UserInfoDao
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package com.isolaatti.posting.posts.domain.entity
|
|||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import com.isolaatti.audio.common.domain.Audio
|
||||||
import com.isolaatti.common.Ownable
|
import com.isolaatti.common.Ownable
|
||||||
import com.isolaatti.posting.posts.data.remote.FeedDto
|
import com.isolaatti.posting.posts.data.remote.FeedDto
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
@ -18,7 +19,8 @@ data class Post(
|
|||||||
var numberOfComments: Int,
|
var numberOfComments: Int,
|
||||||
val userName: String,
|
val userName: String,
|
||||||
val squadName: String?,
|
val squadName: String?,
|
||||||
var liked: Boolean
|
var liked: Boolean,
|
||||||
|
val audio: Audio? = null
|
||||||
) : Ownable, Parcelable {
|
) : Ownable, Parcelable {
|
||||||
constructor(parcel: Parcel) : this(
|
constructor(parcel: Parcel) : this(
|
||||||
parcel.readLong(),
|
parcel.readLong(),
|
||||||
@ -32,7 +34,8 @@ data class Post(
|
|||||||
parcel.readInt(),
|
parcel.readInt(),
|
||||||
parcel.readString()!!,
|
parcel.readString()!!,
|
||||||
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(userName)
|
||||||
parcel.writeString(squadName)
|
parcel.writeString(squadName)
|
||||||
parcel.writeByte(if (liked) 1 else 0)
|
parcel.writeByte(if (liked) 1 else 0)
|
||||||
|
parcel.writeSerializable(audio)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun describeContents(): Int {
|
override fun describeContents(): Int {
|
||||||
|
|||||||
@ -1,8 +1,13 @@
|
|||||||
package com.isolaatti.posting.posts.presentation
|
package com.isolaatti.posting.posts.presentation
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
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.CreatePostDto
|
||||||
import com.isolaatti.posting.posts.data.remote.EditPostDto
|
import com.isolaatti.posting.posts.data.remote.EditPostDto
|
||||||
import com.isolaatti.posting.posts.data.remote.EditPostDto.Companion.PRIVACY_ISOLAATTI
|
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.posting.posts.domain.use_case.MakePost
|
||||||
import com.isolaatti.utils.Resource
|
import com.isolaatti.utils.Resource
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
@ -21,30 +27,97 @@ import kotlinx.coroutines.launch
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@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 validation: MutableLiveData<Boolean> = MutableLiveData(false)
|
||||||
val posted: MutableLiveData<Post?> = MutableLiveData()
|
val posted: MutableLiveData<Post?> = MutableLiveData()
|
||||||
val error: MutableLiveData<Resource.Error.ErrorType?> = 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 postToEdit: MutableLiveData<EditPostDto> = MutableLiveData()
|
||||||
val liveContent: MutableLiveData<String> = MutableLiveData()
|
val liveContent: MutableLiveData<String> = MutableLiveData()
|
||||||
var content: String = ""
|
var content: String = ""
|
||||||
set(value) {field = value; liveContent.value = value} // TODO remove this and use only liveContent
|
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 {
|
viewModelScope.launch {
|
||||||
makePost(EditPostDto.PRIVACY_ISOLAATTI, content, null, null).onEach {
|
makePost(EditPostDto.PRIVACY_ISOLAATTI, content, audioId, null).onEach {
|
||||||
when(it) {
|
when(it) {
|
||||||
is Resource.Success -> {
|
is Resource.Success -> {
|
||||||
loading.postValue(false)
|
sendingPost.postValue(false)
|
||||||
posted.postValue(Post.fromPostDto(it.data!!))
|
posted.postValue(Post.fromPostDto(it.data!!))
|
||||||
}
|
}
|
||||||
is Resource.Error -> {
|
is Resource.Error -> {
|
||||||
loading.postValue(false)
|
sendingPost.postValue(false)
|
||||||
error.postValue(it.errorType)
|
error.postValue(it.errorType)
|
||||||
}
|
}
|
||||||
is Resource.Loading -> {
|
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)
|
}.flowOn(Dispatchers.IO).launchIn(this)
|
||||||
@ -52,22 +125,25 @@ class CreatePostViewModel @Inject constructor(private val makePost: MakePost, pr
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun editDiscussion(postId: Long) {
|
fun editDiscussion(postId: Long) {
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
editPost(postId, EditPostDto.PRIVACY_ISOLAATTI, content, null, null).onEach {
|
if(audioDraft != null) {
|
||||||
when(it) {
|
uploadAudioUC(audioDraft!!).onEach { upLoadAudioResource ->
|
||||||
|
when(upLoadAudioResource) {
|
||||||
is Resource.Success -> {
|
is Resource.Success -> {
|
||||||
loading.postValue(false)
|
audioId = upLoadAudioResource.data?.id
|
||||||
posted.postValue(Post.fromPostDto(it.data!!))
|
sendEditDiscussion(postId)
|
||||||
}
|
|
||||||
is Resource.Error -> {
|
|
||||||
loading.postValue(false)
|
|
||||||
error.postValue(it.errorType)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is Resource.Error -> {}
|
||||||
is Resource.Loading -> {
|
is Resource.Loading -> {
|
||||||
loading.postValue(true)
|
sendingPost.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) {
|
if(postRes is Resource.Success) {
|
||||||
postRes.data?.let {
|
postRes.data?.let {
|
||||||
postToEdit.postValue(EditPostDto(PRIVACY_ISOLAATTI, content = it.textContent, postId = it.id))
|
postToEdit.postValue(EditPostDto(PRIVACY_ISOLAATTI, content = it.textContent, postId = it.id))
|
||||||
|
it.audio?.let { audio -> audioAttachment.postValue(audio) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.flowOn(Dispatchers.IO).launchIn(this)
|
}.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.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.core.widget.doOnTextChanged
|
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.MediatorLiveData
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.navigation.findNavController
|
|
||||||
import com.google.android.material.tabs.TabLayout
|
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import com.isolaatti.R
|
import com.isolaatti.R
|
||||||
import com.isolaatti.common.IsolaattiBaseActivity
|
import com.isolaatti.common.IsolaattiBaseActivity
|
||||||
@ -129,7 +127,7 @@ class CreatePostActivity : IsolaattiBaseActivity() {
|
|||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.loading.observe(this@CreatePostActivity) {
|
viewModel.sendingPost.observe(this@CreatePostActivity) {
|
||||||
binding.progressBarLoading.visibility = if(it) View.VISIBLE else View.GONE
|
binding.progressBarLoading.visibility = if(it) View.VISIBLE else View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,24 @@
|
|||||||
package com.isolaatti.posting.posts.ui
|
package com.isolaatti.posting.posts.ui
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
|
import androidx.appcompat.widget.PopupMenu
|
||||||
import androidx.core.widget.doOnTextChanged
|
import androidx.core.widget.doOnTextChanged
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.fragment.app.viewModels
|
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.audio.recorder.ui.AudioRecorderContract
|
||||||
import com.isolaatti.databinding.FragmentMarkdownEditingBinding
|
import com.isolaatti.databinding.FragmentMarkdownEditingBinding
|
||||||
import com.isolaatti.images.image_chooser.ui.ImageChooserContract
|
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
|
import com.isolaatti.posting.posts.presentation.CreatePostViewModel
|
||||||
|
|
||||||
class MarkdownEditingFragment : Fragment(){
|
class MarkdownEditingFragment : Fragment(){
|
||||||
|
companion object {
|
||||||
|
const val LOG_TAG = "MarkdownEditingFragment"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private lateinit var binding: FragmentMarkdownEditingBinding
|
private lateinit var binding: FragmentMarkdownEditingBinding
|
||||||
private val viewModel: CreatePostViewModel by activityViewModels()
|
private val viewModel: CreatePostViewModel by activityViewModels()
|
||||||
private val linkCreatorViewModel: LinkCreatorViewModel by viewModels()
|
private val linkCreatorViewModel: LinkCreatorViewModel by viewModels()
|
||||||
|
|
||||||
|
private var audioPlayerConnector: AudioPlayerConnector? = null
|
||||||
|
|
||||||
private val audioRecorderLauncher = registerForActivityResult(AudioRecorderContract()) { draftId ->
|
private val audioRecorderLauncher = registerForActivityResult(AudioRecorderContract()) { draftId ->
|
||||||
if(draftId != null) {
|
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(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
@ -53,11 +97,23 @@ class MarkdownEditingFragment : Fragment(){
|
|||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
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()
|
setupListeners()
|
||||||
setupObservers()
|
setupObservers()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
viewLifecycleOwner.lifecycle.removeObserver(audioPlayerConnector!!)
|
||||||
|
}
|
||||||
|
|
||||||
private fun setupListeners() {
|
private fun setupListeners() {
|
||||||
binding.filledTextField.editText?.setText(viewModel.content)
|
binding.filledTextField.editText?.setText(viewModel.content)
|
||||||
binding.filledTextField.requestFocus()
|
binding.filledTextField.requestFocus()
|
||||||
@ -74,7 +130,30 @@ class MarkdownEditingFragment : Fragment(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.addAudioButton.setOnClickListener {
|
binding.addAudioButton.setOnClickListener {
|
||||||
|
|
||||||
|
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)
|
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
|
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() {
|
private fun insertImage() {
|
||||||
imageChooserLauncher.launch(ImageChooserContract.Requester.UserPost)
|
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
|
<TextView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="Audio attachment"/>
|
android:text="@string/audio_attachment"/>
|
||||||
|
<ViewAnimator
|
||||||
|
android:id="@+id/view_animator"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp">
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/add_audio_button"
|
android:id="@+id/add_audio_button"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
android:text="Attach audio" />
|
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>
|
</LinearLayout>
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
</LinearLayout>
|
</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="new_password_is_invalid">New password is invalid. Please check it meets the requirements</string>
|
||||||
<string name="rename">Rename</string>
|
<string name="rename">Rename</string>
|
||||||
<string name="set_as_profile_audio">Set as profile audio</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>
|
</resources>
|
||||||
@ -17,4 +17,6 @@ plugins {
|
|||||||
id 'com.android.library' version '8.2.2' apply false
|
id 'com.android.library' version '8.2.2' apply false
|
||||||
id 'org.jetbrains.kotlin.android' version '1.8.0' 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.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