WIP grabador de audio

This commit is contained in:
erik-everardo 2024-01-31 01:06:45 -06:00
parent 3ff49d73ce
commit 5084fe337d
23 changed files with 525 additions and 86 deletions

2
.idea/navEditor.xml generated
View File

@ -8,7 +8,7 @@
<LayoutPositions>
<option name="myPositions">
<map>
<entry key="audiosFragment2">
<entry key="audioDraftsFragment">
<value>
<LayoutPositions>
<option name="myPosition">

View File

@ -4,7 +4,11 @@ import androidx.media3.common.Player
import com.isolaatti.audio.common.data.AudiosApi
import com.isolaatti.audio.common.data.AudiosRepositoryImpl
import com.isolaatti.audio.common.domain.AudiosRepository
import com.isolaatti.audio.drafts.data.AudioDraftsRepositoryImpl
import com.isolaatti.audio.drafts.data.AudiosDraftsDao
import com.isolaatti.audio.drafts.domain.repository.AudioDraftsRepository
import com.isolaatti.connectivity.RetrofitClient
import com.isolaatti.database.AppDatabase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -22,4 +26,14 @@ class Module {
fun provideAudiosRepository(audiosApi: AudiosApi): AudiosRepository {
return AudiosRepositoryImpl(audiosApi)
}
@Provides
fun provideAudioDraftsDao(database: AppDatabase): AudiosDraftsDao {
return database.audioDrafts()
}
@Provides
fun provideAudioDraftsRepository(audiosDraftsDao: AudiosDraftsDao): AudioDraftsRepository {
return AudioDraftsRepositoryImpl(audiosDraftsDao)
}
}

View File

@ -10,4 +10,21 @@ abstract class Playable {
* Image url, null indicating no image should be shown
*/
abstract val thumbnail: String?
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Playable
if (uri != other.uri) return false
return thumbnail == other.thumbnail
}
override fun hashCode(): Int {
var result = uri.hashCode()
result = 31 * result + (thumbnail?.hashCode() ?: 0)
return result
}
}

View File

@ -5,7 +5,7 @@ import androidx.room.PrimaryKey
@Entity(tableName = "audio_drafts")
data class AudioDraftEntity(
@PrimaryKey val id: Long,
@PrimaryKey(autoGenerate = true) val id: Long,
val name: String,
val audioLocalPath: String
)

View File

@ -0,0 +1,42 @@
package com.isolaatti.audio.drafts.data
import android.util.Log
import com.isolaatti.MyApplication
import com.isolaatti.audio.drafts.domain.AudioDraft
import com.isolaatti.audio.drafts.domain.repository.AudioDraftsRepository
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))
emit(AudioDraft.fromEntity(entity.copy(id = insertedEntityId)))
}
override fun getAudioDrafts(): Flow<List<AudioDraft>> = flow {
emit(audiosDraftsDao.getDrafts().map { AudioDraft.fromEntity(it) })
}
override fun deleteDrafts(draftIds: LongArray): Flow<Boolean> = flow {
val drafts = audiosDraftsDao.getAudioDraftsByIds(draftIds)
audiosDraftsDao.deleteDrafts(drafts)
try {
for(draft in drafts) {
File(MyApplication.myApp.applicationContext.filesDir, draft.audioLocalPath).delete()
}
} catch(securityException: SecurityException) {
Log.e("AudioDraftsRepositoryImpl", "Could not delete file\n${securityException.message}")
}
emit(true)
}
override fun renameDraft(draftId: Long, name: String): Flow<Boolean> = flow {
val rowsAffected = audiosDraftsDao.renameDraft(draftId, name)
emit(rowsAffected > 0)
}
}

View File

@ -4,18 +4,25 @@ import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
@Dao
interface AudiosDraftsDao {
@Insert
fun insertAudioDraft(audioDraftEntity: AudioDraftEntity): Long
suspend fun insertAudioDraft(audioDraftEntity: AudioDraftEntity): Long
@Query("SELECT * FROM audio_drafts WHERE id = :draftId")
fun getAudioDraftById(draftId: Long): AudioDraftEntity
suspend fun getAudioDraftById(draftId: Long): AudioDraftEntity
@Query("SELECT * FROM audio_drafts WHERE id < :before ORDER BY id DESC LIMIT :count")
fun getDrafts(count: Int, before: Long): List<AudioDraftEntity>
@Query("SELECT * FROM audio_drafts WHERE id in (:draftId)")
suspend fun getAudioDraftsByIds(draftId: LongArray): Array<AudioDraftEntity>
@Query("DELETE FROM audio_drafts WHERE id in (:draftIds)")
fun deleteDrafts(draftIds: LongArray)
@Query("SELECT * FROM audio_drafts ORDER BY id DESC")
suspend fun getDrafts(): List<AudioDraftEntity>
@Delete
suspend fun deleteDrafts(draft: Array<AudioDraftEntity>)
@Query("UPDATE audio_drafts SET name = :name WHERE id = :id")
suspend fun renameDraft(id: Long, name: String): Int
}

View File

@ -4,6 +4,7 @@ import android.net.Uri
import androidx.core.net.toUri
import com.isolaatti.MyApplication
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() {
@ -15,4 +16,10 @@ data class AudioDraft(val id: Long, val name: String, val localStorageRelativePa
val appFiles = MyApplication.myApp.applicationContext.filesDir
return File(appFiles, localStorageRelativePath).toUri()
}
companion object {
fun fromEntity(audioDraftEntity: AudioDraftEntity): AudioDraft {
return AudioDraft(audioDraftEntity.id, audioDraftEntity.name, audioDraftEntity.audioLocalPath)
}
}
}

View File

@ -0,0 +1,14 @@
package com.isolaatti.audio.drafts.domain.repository
import com.isolaatti.audio.drafts.domain.AudioDraft
import kotlinx.coroutines.flow.Flow
interface AudioDraftsRepository {
fun saveAudioDraft(name: String, relativePath: String): Flow<AudioDraft>
fun getAudioDrafts(): Flow<List<AudioDraft>>
fun deleteDrafts(draftIds: LongArray): Flow<Boolean>
fun renameDraft(draftId: Long, name: String): Flow<Boolean>
}

View File

@ -0,0 +1,12 @@
package com.isolaatti.audio.drafts.domain.use_case
import com.isolaatti.audio.drafts.domain.AudioDraft
import com.isolaatti.audio.drafts.domain.repository.AudioDraftsRepository
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)
}
}

View File

@ -11,6 +11,7 @@ import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer
import com.isolaatti.audio.common.domain.Audio
import com.isolaatti.audio.common.domain.Playable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -28,7 +29,7 @@ class AudioPlayerConnector(
const val TAG = "AudioPlayerConnector"
}
private var player: Player? = null
private var audio: Audio? = null
private var audio: Playable? = null
private var mediaItem: MediaItem? = null
private var ended = false
@ -83,12 +84,23 @@ class AudioPlayerConnector(
Player.STATE_ENDED -> {
Log.d(TAG, "STATE_ENDED")
audio?.let {
listeners.forEach { listener -> listener.onPlaying(false, it)}
listeners.forEach { listener ->
listener.onPlaying(false, it)
listener.onEnded(it)
}
}
stopTimer()
ended = true
}
Player.STATE_BUFFERING -> {}
Player.STATE_BUFFERING -> {
player?.totalBufferedDuration?.let {
val seconds = (it / 1000).toInt()
Log.d(TAG, "Duration $it")
audio?.let {
listeners.forEach { listener -> listener.durationChanged(seconds, it) }
}
}
}
Player.STATE_IDLE -> {}
Player.STATE_READY -> {
Log.d(TAG, "STATE_READY")
@ -136,12 +148,13 @@ class AudioPlayerConnector(
}
Lifecycle.Event.ON_STOP, Lifecycle.Event.ON_DESTROY -> {
releasePlayer()
listeners.clear()
}
else -> {}
}
}
fun playPauseAudio(audio: Audio) {
fun playPauseAudio(audio: Playable) {
// intention is to pause current audio
if(audio == this.audio && player?.isPlaying == true) {
@ -162,10 +175,36 @@ class AudioPlayerConnector(
player?.setMediaItem(mediaItem!!)
}
fun stopPlayback() {
ended = true
player?.pause()
stopTimer()
}
interface Listener {
fun onPlaying(isPlaying: Boolean, audio: Audio)
fun isLoading(isLoading: Boolean, audio: Audio)
fun progressChanged(second: Int, audio: Audio)
fun durationChanged(duration: Int, audio: Audio)
fun onPlaying(isPlaying: Boolean, audio: Playable)
fun isLoading(isLoading: Boolean, audio: Playable)
fun progressChanged(second: Int, audio: Playable)
fun durationChanged(duration: Int, audio: Playable)
fun onEnded(audio: Playable)
}
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) {
}
}
}

View File

@ -0,0 +1,52 @@
package com.isolaatti.audio.recorder.presentation
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.isolaatti.audio.drafts.domain.AudioDraft
import com.isolaatti.audio.drafts.domain.use_case.SaveAudioDraft
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class AudioRecorderViewModel @Inject constructor(
private val saveAudioDraftUC: SaveAudioDraft
) : ViewModel() {
var name: String = ""
set(value) {
field = value
validate()
}
var relativePath = ""
private var audioRecorded = false
val audioDraft: MutableLiveData<AudioDraft> = MutableLiveData()
val canSave: MutableLiveData<Boolean> = MutableLiveData()
private fun validate() {
canSave.value = audioRecorded && name.isNotBlank()
}
fun onAudioRecorded() {
audioRecorded = true
validate()
}
fun onClearAudio() {
audioRecorded = false
validate()
}
fun saveAudioDraft() {
viewModelScope.launch {
saveAudioDraftUC(name, relativePath).onEach {
audioDraft.postValue(it)
}.flowOn(Dispatchers.IO).launchIn(this)
}
}
}

View File

@ -0,0 +1,21 @@
package com.isolaatti.audio.recorder.presentation
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.isolaatti.audio.drafts.ui.AudioDraftsFragment
import com.isolaatti.audio.recorder.ui.AudioRecorderHelperNotesFragment
class RecorderPagerAdapter(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) {
override fun getItemCount(): Int {
return 2
}
override fun createFragment(position: Int): Fragment {
return when (position) {
0 -> AudioRecorderHelperNotesFragment()
1 -> AudioDraftsFragment()
else -> Fragment()
}
}
}

View File

@ -2,18 +2,36 @@ package com.isolaatti.audio.recorder.ui
import android.Manifest
import android.content.Intent
import android.content.res.ColorStateList
import android.content.res.Resources.Theme
import android.media.AudioManager
import android.media.MediaRecorder
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.util.Log
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.PermissionChecker
import androidx.core.content.PermissionChecker.PERMISSION_GRANTED
import androidx.core.content.res.ResourcesCompat
import androidx.core.net.toUri
import androidx.core.view.get
import androidx.core.widget.doOnTextChanged
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.dialog.MaterialDialogs
import com.google.android.material.tabs.TabLayoutMediator
import com.isolaatti.R
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.presentation.AudioRecorderViewModel
import com.isolaatti.audio.recorder.presentation.RecorderPagerAdapter
import com.isolaatti.databinding.ActivityAudioRecorderBinding
import com.isolaatti.utils.Resource
import com.isolaatti.utils.clockFormat
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -29,13 +47,18 @@ class AudioRecorderActivity : AppCompatActivity() {
companion object {
const val LOG_TAG = "AudioRecorderActivity"
const val IN_EXTRA_DRAFT_ID = "in_draft_id"
const val OUT_EXTRA_DRAFT_ID = "out_draft_id"
}
private lateinit var binding: ActivityAudioRecorderBinding
private var audioRecorder: MediaRecorder? = null
private var recordingPaused = false
private var audioPlayerConnector: AudioPlayerConnector? = null
private val audioUID = UUID.randomUUID()
private val viewModel: AudioRecorderViewModel by viewModels()
private lateinit var outputFile: String
private val requestAudioPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
@ -46,20 +69,59 @@ class AudioRecorderActivity : AppCompatActivity() {
}
}
private val audioPlayerListener = object: AudioPlayerConnector.DefaultListener() {
override fun onPlaying(isPlaying: Boolean, audio: Playable) {
if(isPlaying) {
binding.seekbar.isEnabled = true
binding.playPauseButton.icon = ResourcesCompat.getDrawable(resources, R.drawable.baseline_pause_24, theme)
} else {
binding.playPauseButton.icon = ResourcesCompat.getDrawable(resources, R.drawable.baseline_play_arrow_24, theme)
}
}
override fun onEnded(audio: Playable) {
binding.seekbar.isEnabled = false
binding.seekbar.progress = 0
}
override fun durationChanged(duration: Int, audio: Playable) {
super.durationChanged(duration, audio)
binding.seekbar.max = duration
totalTime = duration
}
override fun progressChanged(second: Int, audio: Playable) {
binding.seekbar.progress = second
setDisplayTime(second, true)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityAudioRecorderBinding.inflate(layoutInflater)
setContentView(binding.root)
// Path where results are stored
File("${filesDir.absolutePath}/audios/").let {
if(!it.isDirectory) {
it.mkdir()
}
}
outputFile = "${filesDir.absolutePath}/audios/${audioUID}.3gp"
binding.seekbar.isEnabled = false
outputFile = "${cacheDir.absolutePath}/audio_recorder.3gp"
setupListeners()
setupObservers()
audioPlayerConnector = AudioPlayerConnector(this)
audioPlayerConnector?.addListener(audioPlayerListener)
lifecycle.addObserver(audioPlayerConnector!!)
}
private fun checkRecordAudioPermission(): Boolean {
@ -112,25 +174,60 @@ class AudioRecorderActivity : AppCompatActivity() {
pauseRecording()
}
binding.acceptButton.setOnClickListener {
acceptRecording()
binding.playPauseButton.setOnClickListener {
playPauseRecording(object: Playable() {
override val uri: Uri
get() = outputFile.toUri()
override val thumbnail: String?
get() = null
})
}
binding.playPauseButton.setOnClickListener {
binding.inputDraftName.editText?.doOnTextChanged { text, _, _, _ ->
viewModel.name = text.toString()
}
binding.toolbar.setOnMenuItemClickListener {
when(it.itemId) {
R.id.save_draft -> {
viewModel.saveAudioDraft()
true
}
else -> false
}
}
}
private fun setupObservers() {
viewModel.canSave.observe(this) {
binding.toolbar.menu.findItem(R.id.save_draft).isEnabled = it
}
// audio draft is saved!
viewModel.audioDraft.observe(this) {
val result = Intent().apply {
putExtra(OUT_EXTRA_DRAFT_ID, it.id)
}
setResult(RESULT_OK, result)
finish()
}
}
// region timer
private var timer: Job? = null
private var timerValue = 0
private var totalTime = 0
private fun setDisplayTime(seconds: Int, showTotalTime: Boolean) {
val stringBuilder = StringBuilder()
binding.time.text = buildString {
append(seconds.clockFormat())
stringBuilder.append(seconds)
binding.time.text = stringBuilder.toString()
if(showTotalTime) {
append("/")
append(totalTime.clockFormat())
}
}
}
private fun startTimerRecorder() {
@ -142,14 +239,6 @@ class AudioRecorderActivity : AppCompatActivity() {
}
}
private fun startTimerPlayer() {
timer?.cancel()
timer = CoroutineScope(Dispatchers.Main).launch {
setDisplayTime(timerValue, true)
delay(1000)
timerValue++
}
}
private fun stopTimer() {
timer?.cancel()
@ -159,6 +248,8 @@ class AudioRecorderActivity : AppCompatActivity() {
// region record functions
private fun startRecording() {
viewModel.onClearAudio()
recordingPaused = false
audioRecorder = MediaRecorder().apply {
setAudioSource(MediaRecorder.AudioSource.MIC)
setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP)
@ -171,6 +262,7 @@ class AudioRecorderActivity : AppCompatActivity() {
timerValue = 0
startTimerRecorder()
binding.viewAnimator.displayedChild = 1
} catch(e: IOException) {
Log.e(LOG_TAG, "prepare() failed\n${e.message}")
}
@ -187,11 +279,25 @@ class AudioRecorderActivity : AppCompatActivity() {
// shows third state: audio recorded
binding.viewAnimator.displayedChild = 2
recordingPaused = false
binding.pauseRecording.icon = ResourcesCompat.getDrawable(resources, R.drawable.baseline_pause_24, theme)
binding.pauseRecording.setIconTintResource(com.google.android.material.R.color.m3_icon_button_icon_color_selector)
viewModel.onAudioRecorded()
}
private fun pauseRecording() {
audioRecorder?.pause()
stopTimer()
if(recordingPaused) {
binding.pauseRecording.icon = ResourcesCompat.getDrawable(resources, R.drawable.baseline_pause_24, theme)
binding.pauseRecording.setIconTintResource(com.google.android.material.R.color.m3_icon_button_icon_color_selector)
audioRecorder?.resume()
startTimerRecorder()
} else {
binding.pauseRecording.icon = ResourcesCompat.getDrawable(resources, R.drawable.baseline_circle_24, theme)
binding.pauseRecording.setIconTintResource(R.color.danger)
audioRecorder?.pause()
stopTimer()
}
recordingPaused = !recordingPaused
}
private fun discardRecording(){
@ -203,16 +309,17 @@ class AudioRecorderActivity : AppCompatActivity() {
}
}
binding.viewAnimator.displayedChild = 0
viewModel.onClearAudio()
totalTime = 0
timerValue = 0
binding.time.text = ""
audioPlayerConnector?.stopPlayback()
}
// end region
private fun acceptRecording() {
}
private fun playPauseRecording() {
private fun playPauseRecording(audioDraft: Playable) {
audioPlayerConnector?.playPauseAudio(audioDraft)
}

View File

@ -0,0 +1,28 @@
package com.isolaatti.audio.recorder.ui
import android.app.Activity
import android.content.Context
import android.content.Intent
import androidx.activity.result.contract.ActivityResultContract
class AudioRecorderContract : ActivityResultContract<Long?, Long?>() {
override fun createIntent(context: Context, input: Long?): Intent {
val intent = Intent(context, AudioRecorderActivity::class.java).apply {
if(input != null) {
putExtra(AudioRecorderActivity.IN_EXTRA_DRAFT_ID, input)
}
}
return intent
}
override fun parseResult(resultCode: Int, intent: Intent?): Long? {
return when(resultCode){
Activity.RESULT_OK -> {
intent?.getLongExtra(AudioRecorderActivity.OUT_EXTRA_DRAFT_ID, 0)?.takeUnless { it == 0L }
}
else -> null
}
}
}

View File

@ -0,0 +1,25 @@
package com.isolaatti.audio.recorder.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.fragment.app.Fragment
class AudioRecorderHelperNotesFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return LinearLayout(requireContext()).apply {
addView(TextView(requireContext()).apply {
text = "Allow the user to place some info here to be " +
"reading while recording. If this screen is launched from comments, show here the post"
})
}
}
}

View File

@ -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 = 3)
@Database(entities = [KeyValueEntity::class, UserInfoEntity::class, AudioDraftEntity::class], version = 4)
abstract class AppDatabase : RoomDatabase() {
abstract fun keyValueDao(): KeyValueDao
abstract fun userInfoDao(): UserInfoDao

View File

@ -11,6 +11,7 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import com.isolaatti.audio.recorder.ui.AudioRecorderActivity
import com.isolaatti.audio.recorder.ui.AudioRecorderContract
import com.isolaatti.databinding.FragmentMarkdownEditingBinding
import com.isolaatti.images.image_chooser.ui.ImageChooserContract
import com.isolaatti.posting.link_creator.presentation.LinkCreatorViewModel
@ -22,6 +23,12 @@ class MarkdownEditingFragment : Fragment(){
private val viewModel: CreatePostViewModel by activityViewModels()
private val linkCreatorViewModel: LinkCreatorViewModel by viewModels()
private val audioRecorderLauncher = registerForActivityResult(AudioRecorderContract()) { draftId ->
if(draftId != null) {
}
}
private val imageChooserLauncher = registerForActivityResult(ImageChooserContract()) { image ->
Log.d("MarkdownEditingFragment", "${image?.markdown}")
@ -67,7 +74,7 @@ class MarkdownEditingFragment : Fragment(){
}
binding.addAudioButton.setOnClickListener {
requireContext().startActivity(Intent(requireContext(), AudioRecorderActivity::class.java))
audioRecorderLauncher.launch(null)
}
}

View File

@ -23,6 +23,7 @@ import com.isolaatti.BuildConfig
import com.isolaatti.R
import com.isolaatti.audio.audios_list.ui.AudiosFragment
import com.isolaatti.audio.common.domain.Audio
import com.isolaatti.audio.common.domain.Playable
import com.isolaatti.audio.player.AudioPlayerConnector
import com.isolaatti.common.CoilImageLoader.imageLoader
import com.isolaatti.common.Dialogs
@ -106,24 +107,28 @@ class ProfileMainFragment : Fragment() {
}
private val audioPlayerConnectorListener = object: AudioPlayerConnector.Listener {
override fun onPlaying(isPlaying: Boolean, audio: Audio) {
override fun onPlaying(isPlaying: Boolean, audio: Playable) {
viewBinding.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: Audio) {
override fun isLoading(isLoading: Boolean, audio: Playable) {
viewBinding.playButton.isEnabled = !isLoading
viewBinding.audioProgress.isIndeterminate = isLoading
}
override fun progressChanged(second: Int, audio: Audio) {
override fun progressChanged(second: Int, audio: Playable) {
viewBinding.audioProgress.setProgress(second, true)
}
override fun durationChanged(duration: Int, audio: Audio) {
override fun durationChanged(duration: Int, audio: Playable) {
viewBinding.audioProgress.max = duration
}
override fun onEnded(audio: Playable) {
viewBinding.audioProgress.progress = 0
}
}
private val profileObserver = Observer<UserProfile> { profile ->

View File

@ -0,0 +1,8 @@
package com.isolaatti.utils
fun Int.clockFormat(): String {
val minutes = this / 60
val seconds = this % 60
return "${minutes}:${seconds.toString().padStart(2, '0')}"
}

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
@ -12,23 +12,41 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:navigationIcon="@drawable/baseline_close_24"/>
app:title="@string/audio_recorder"
app:navigationIcon="@drawable/baseline_close_24"
app:menu="@menu/audio_recorder_menu"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/input_draft_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:layout_constraintTop_toBottomOf="@id/toolbar"
android:hint="Name"
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.card.MaterialCardView
android:id="@+id/materialCardView2"
style="?attr/materialCardViewElevatedStyle"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/time"
app:layout_constraintTop_toBottomOf="@id/toolbar"
android:layout_margin="24dp"
style="?attr/materialCardViewFilledStyle">
app:layout_constraintBottom_toTopOf="@id/time"
app:layout_constraintTop_toBottomOf="@id/input_draft_name"
tools:layout_editor_absoluteX="24dp">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container"
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:navGraph="@navigation/audio_recorder_navigation"
android:name="androidx.navigation.fragment.NavHostFragment"
app:defaultNavHost="true"/>
android:layout_height="match_parent">
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
@ -36,22 +54,22 @@
android:id="@+id/time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@id/card_controls"
android:layout_marginHorizontal="24dp"
android:layout_marginBottom="16dp"
android:textAlignment="center"
android:textSize="24sp"
tools:text="00:00"
android:textAlignment="center"/>
app:layout_constraintBottom_toTopOf="@id/card_controls"
tools:text="00:00" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_controls"
style="?attr/materialCardViewFilledStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="24dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
style="?attr/materialCardViewFilledStyle"
android:layout_margin="24dp">
app:layout_constraintStart_toStartOf="parent">
<ViewAnimator
android:id="@+id/viewAnimator"
@ -115,16 +133,25 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="8dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/cancel_button"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/baseline_close_24"
app:layout_constraintEnd_toStartOf="@+id/play_pause_button"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
app:layout_constraintTop_toTopOf="parent" />
<SeekBar
android:id="@+id/seekbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toEndOf="@id/cancel_button"
app:layout_constraintEnd_toStartOf="@id/play_pause_button"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/play_pause_button"
@ -132,26 +159,12 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/baseline_play_arrow_24"
app:layout_constraintEnd_toStartOf="@+id/accept_button"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/cancel_button"
app:layout_constraintTop_toTopOf="parent"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/accept_button"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/baseline_check_24"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/play_pause_button"
app:layout_constraintTop_toTopOf="parent"/>
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ViewAnimator>
</com.google.android.material.card.MaterialCardView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,7 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Audio drafts"
app:layout_constraintTop_toTopOf="parent"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler"

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/save_draft"
android:title="Save"
app:showAsAction="always"
android:enabled="false"/>
</menu>

View File

@ -1,7 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/audio_recorder_navigation">
android:id="@+id/audio_recorder_navigation"
app:startDestination="@id/audioDraftsFragment">
<fragment
android:id="@+id/audioDraftsFragment"
android:name="com.isolaatti.audio.drafts.ui.AudioDraftsFragment"
android:label="AudioDraftsFragment" />
</navigation>