WIP
This commit is contained in:
parent
34326cc8d4
commit
38081f4837
@ -15,7 +15,7 @@ plugins {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'com.isolaatti'
|
namespace 'com.isolaatti'
|
||||||
compileSdk 34
|
compileSdk 35
|
||||||
viewBinding {
|
viewBinding {
|
||||||
enabled = true
|
enabled = true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,7 +41,8 @@ fun AudioPlayer(
|
|||||||
durationMs: Long,
|
durationMs: Long,
|
||||||
onPlay: () -> Unit = {},
|
onPlay: () -> Unit = {},
|
||||||
onPause: () -> Unit = {},
|
onPause: () -> Unit = {},
|
||||||
onSeek: (position: Float) -> Unit = {}
|
onSeek: (position: Float) -> Unit = {},
|
||||||
|
options: @Composable () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|
||||||
@ -79,11 +80,13 @@ fun AudioPlayer(
|
|||||||
onSeek(value)
|
onSeek(value)
|
||||||
},
|
},
|
||||||
modifier = Modifier.padding(horizontal = 4.dp).weight(1f),
|
modifier = Modifier.padding(horizontal = 4.dp).weight(1f),
|
||||||
valueRange = 0f..durationMs.toFloat()
|
valueRange = 0f..durationMs.toFloat().coerceAtLeast(0f)
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Text("${positionMs.milliseconds.clockFormat()}/${durationMs.milliseconds.clockFormat()}")
|
Text("${positionMs.milliseconds.clockFormat()}/${durationMs.milliseconds.clockFormat()}")
|
||||||
|
options()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,7 +19,6 @@ import androidx.compose.material3.IconButton
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableLongStateOf
|
import androidx.compose.runtime.mutableLongStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@ -50,15 +49,14 @@ class AudioRecorderState(
|
|||||||
isPlaying: Boolean = false,
|
isPlaying: Boolean = false,
|
||||||
isLoading: Boolean = false,
|
isLoading: Boolean = false,
|
||||||
durationSeconds: Long = 0L,
|
durationSeconds: Long = 0L,
|
||||||
position: Long = 0L,
|
position: Long = 0L
|
||||||
|
|
||||||
) {
|
) {
|
||||||
var isRecording by mutableStateOf(isRecording)
|
var isRecording by mutableStateOf(isRecording)
|
||||||
var recordIsStopped by mutableStateOf(recordIsStopped)
|
var recordIsStopped by mutableStateOf(recordIsStopped)
|
||||||
var recordIsPaused by mutableStateOf(recordIsPaused)
|
var recordIsPaused by mutableStateOf(recordIsPaused)
|
||||||
var isPlaying by mutableStateOf(isPlaying)
|
var isPlaying by mutableStateOf(isPlaying)
|
||||||
var isLoading by mutableStateOf(isLoading)
|
var isLoading by mutableStateOf(isLoading)
|
||||||
var durationSeconds by mutableStateOf(durationSeconds)
|
var durationMilliseconds by mutableStateOf(durationSeconds)
|
||||||
var position by mutableStateOf(position)
|
var position by mutableStateOf(position)
|
||||||
|
|
||||||
fun clearState() {
|
fun clearState() {
|
||||||
@ -66,7 +64,7 @@ class AudioRecorderState(
|
|||||||
isRecording = false
|
isRecording = false
|
||||||
recordIsPaused = false
|
recordIsPaused = false
|
||||||
isPlaying = false
|
isPlaying = false
|
||||||
durationSeconds = 0L
|
durationMilliseconds = 0L
|
||||||
position = 0L
|
position = 0L
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -74,6 +72,7 @@ class AudioRecorderState(
|
|||||||
@OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AudioRecorder(
|
fun AudioRecorder(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
onDismiss: () -> Unit = {},
|
onDismiss: () -> Unit = {},
|
||||||
onPlay: () -> Unit = {},
|
onPlay: () -> Unit = {},
|
||||||
onPause: () -> Unit = {},
|
onPause: () -> Unit = {},
|
||||||
@ -82,7 +81,6 @@ fun AudioRecorder(
|
|||||||
onStartRecording: (fromPaused: Boolean) -> Unit,
|
onStartRecording: (fromPaused: Boolean) -> Unit,
|
||||||
state: AudioRecorderState = AudioRecorderState(),
|
state: AudioRecorderState = AudioRecorderState(),
|
||||||
onSeek: (position: Float) -> Unit = {},
|
onSeek: (position: Float) -> Unit = {},
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|
||||||
@ -174,7 +172,7 @@ fun AudioRecorder(
|
|||||||
positionMs = state.position,
|
positionMs = state.position,
|
||||||
isLoading = state.isLoading,
|
isLoading = state.isLoading,
|
||||||
isPlaying = state.isPlaying,
|
isPlaying = state.isPlaying,
|
||||||
durationMs = state.durationSeconds,
|
durationMs = state.durationMilliseconds,
|
||||||
onPlay = onPlay,
|
onPlay = onPlay,
|
||||||
onPause = onPause,
|
onPause = onPause,
|
||||||
onSeek = onSeek
|
onSeek = onSeek
|
||||||
|
|||||||
@ -6,9 +6,7 @@ import java.time.ZonedDateTime
|
|||||||
data class AudiosDto(val data: List<AudioDto>)
|
data class AudiosDto(val data: List<AudioDto>)
|
||||||
data class AudioDto(
|
data class AudioDto(
|
||||||
val id: String,
|
val id: String,
|
||||||
val name: String,
|
|
||||||
val creationTime: ZonedDateTime,
|
val creationTime: ZonedDateTime,
|
||||||
val userId: Int,
|
val userId: Int,
|
||||||
val firestoreObjectPath: String,
|
val durationSeconds: Long
|
||||||
val userName: String
|
|
||||||
): Serializable
|
): Serializable
|
||||||
@ -11,14 +11,12 @@ import java.time.ZonedDateTime
|
|||||||
|
|
||||||
data class Audio(
|
data class Audio(
|
||||||
val id: String,
|
val id: String,
|
||||||
val name: String,
|
|
||||||
val creationTime: ZonedDateTime,
|
val creationTime: ZonedDateTime,
|
||||||
override val userId: Int,
|
override val userId: Int,
|
||||||
val userName: String
|
|
||||||
): Ownable, Playable(), Serializable {
|
): Ownable, Playable(), Serializable {
|
||||||
|
|
||||||
override val uri: Uri get() {
|
override val uri: Uri get() {
|
||||||
return "${BASE_URL}audios/$id.webm".toUri()
|
return "${BASE_URL}audios/$id.aac".toUri()
|
||||||
}
|
}
|
||||||
|
|
||||||
override val thumbnail: String get() {
|
override val thumbnail: String get() {
|
||||||
@ -29,10 +27,8 @@ data class Audio(
|
|||||||
fun fromDto(audioDto: AudioDto): Audio {
|
fun fromDto(audioDto: AudioDto): Audio {
|
||||||
return Audio(
|
return Audio(
|
||||||
id = audioDto.id,
|
id = audioDto.id,
|
||||||
name = audioDto.name,
|
|
||||||
creationTime = audioDto.creationTime,
|
creationTime = audioDto.creationTime,
|
||||||
userId = audioDto.userId,
|
userId = audioDto.userId,
|
||||||
userName = audioDto.userName
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,18 @@
|
|||||||
package com.isolaatti.audio.player
|
package com.isolaatti.audio.player
|
||||||
|
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import com.isolaatti.audio.common.domain.Audio
|
import com.isolaatti.audio.common.domain.Audio
|
||||||
|
|
||||||
data class AudioPlaybackState(
|
class AudioPlaybackState(
|
||||||
val currentAudio: Audio? = null,
|
currentAudio: Audio? = null,
|
||||||
var isPlaying: Boolean = false,
|
isPlaying: Boolean = false,
|
||||||
val duration: Long = 0L,
|
duration: Long = 0L,
|
||||||
val progress: Long = 0L
|
progress: Long = 0L
|
||||||
)
|
) {
|
||||||
|
var currentAudio by mutableStateOf(currentAudio)
|
||||||
|
var isPlaying by mutableStateOf(isPlaying)
|
||||||
|
var duration by mutableStateOf(duration)
|
||||||
|
var progress by mutableStateOf(progress)
|
||||||
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package com.isolaatti.posting.posts.presentation
|
package com.isolaatti.posting.posts.presentation
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
@ -45,11 +46,13 @@ abstract class PostListingViewModelBase : ViewModel() {
|
|||||||
|
|
||||||
val mediaControllerIsAvailable: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
val mediaControllerIsAvailable: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||||
|
|
||||||
val audioState: MutableStateFlow<AudioPlaybackState> = MutableStateFlow(AudioPlaybackState())
|
val audioState = AudioPlaybackState()
|
||||||
|
|
||||||
fun getLastId(): Long = try { posts.value?.last()?.id ?: 0 } catch (e: NoSuchElementException) { 0 }
|
fun getLastId(): Long = try { posts.value?.last()?.id ?: 0 } catch (e: NoSuchElementException) { 0 }
|
||||||
|
|
||||||
val audioRecorderState = AudioRecorderState()
|
val audioRecorderState = AudioRecorderState()
|
||||||
|
var uploadingAudio: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
abstract fun getFeed(refresh: Boolean, hashtag: String?)
|
abstract fun getFeed(refresh: Boolean, hashtag: String?)
|
||||||
@ -124,16 +127,4 @@ abstract class PostListingViewModelBase : ViewModel() {
|
|||||||
fun onPostAddedAtTheBeginning(post: Post) {
|
fun onPostAddedAtTheBeginning(post: Post) {
|
||||||
posts.value = listOf(post) + posts.value
|
posts.value = listOf(post) + posts.value
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setAudioToPlay(audio: Audio) {
|
|
||||||
audioState.value = AudioPlaybackState(currentAudio = audio)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setIsPlaying(isPlaying: Boolean) {
|
|
||||||
audioState.value = audioState.value.copy(isPlaying = isPlaying)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateProgress(progress: Long, duration: Long) {
|
|
||||||
audioState.value = audioState.value.copy(progress = progress, duration = duration)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -132,7 +132,7 @@ class CreatePostActivity : IsolaattiBaseActivity() {
|
|||||||
Player.STATE_READY -> {
|
Player.STATE_READY -> {
|
||||||
viewModel.ended = false
|
viewModel.ended = false
|
||||||
viewModel.audioRecorderState.isLoading = false
|
viewModel.audioRecorderState.isLoading = false
|
||||||
viewModel.audioRecorderState.durationSeconds = this@CreatePostActivity.mediaController.duration
|
viewModel.audioRecorderState.durationMilliseconds = this@CreatePostActivity.mediaController.duration
|
||||||
}
|
}
|
||||||
Player.STATE_ENDED -> {
|
Player.STATE_ENDED -> {
|
||||||
viewModel.ended = true
|
viewModel.ended = true
|
||||||
|
|||||||
@ -1,11 +1,14 @@
|
|||||||
package com.isolaatti.profile.data.remote
|
package com.isolaatti.profile.data.remote
|
||||||
|
|
||||||
|
import com.isolaatti.audio.common.data.AudioDto
|
||||||
|
import okhttp3.MultipartBody
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.http.Body
|
import retrofit2.http.Body
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Multipart
|
||||||
import retrofit2.http.POST
|
import retrofit2.http.POST
|
||||||
|
import retrofit2.http.Part
|
||||||
import retrofit2.http.Path
|
import retrofit2.http.Path
|
||||||
import retrofit2.http.Query
|
|
||||||
|
|
||||||
interface ProfileApi {
|
interface ProfileApi {
|
||||||
|
|
||||||
@ -19,4 +22,8 @@ interface ProfileApi {
|
|||||||
@POST("EditProfile/RemoveProfileImage")
|
@POST("EditProfile/RemoveProfileImage")
|
||||||
fun removeCurrentProfilePicture(): Call<Void>
|
fun removeCurrentProfilePicture(): Call<Void>
|
||||||
|
|
||||||
|
@Multipart
|
||||||
|
@POST("EditProfile/SetAudioDescription")
|
||||||
|
fun updateAudioDescription(@Part file: MultipartBody.Part, @Part duration: MultipartBody.Part): Call<AudioDto>
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -17,5 +17,5 @@ data class UserProfileDto(
|
|||||||
val profileImageId: String?,
|
val profileImageId: String?,
|
||||||
val descriptionText: String?,
|
val descriptionText: String?,
|
||||||
val descriptionAudioId: String?,
|
val descriptionAudioId: String?,
|
||||||
val audio: AudioDto?
|
val descriptionAudio: AudioDto?
|
||||||
)
|
)
|
||||||
|
|||||||
@ -3,8 +3,11 @@ package com.isolaatti.profile.data.repository
|
|||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.core.net.toFile
|
||||||
|
import com.isolaatti.audio.common.domain.Audio
|
||||||
import com.isolaatti.auth.data.local.UserInfoDao
|
import com.isolaatti.auth.data.local.UserInfoDao
|
||||||
import com.isolaatti.auth.data.local.UserInfoEntity
|
import com.isolaatti.auth.data.local.UserInfoEntity
|
||||||
import com.isolaatti.images.common.data.remote.ImagesApi
|
import com.isolaatti.images.common.data.remote.ImagesApi
|
||||||
@ -18,10 +21,12 @@ 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.MultipartBody
|
import okhttp3.MultipartBody
|
||||||
|
import okhttp3.RequestBody.Companion.asRequestBody
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import retrofit2.awaitResponse
|
import retrofit2.awaitResponse
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ -123,4 +128,27 @@ class ProfileRepositoryImpl @Inject constructor(
|
|||||||
emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
|
emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setProfileAudio(audioFile: Uri, duration: Long): Flow<Resource<Audio>> = flow {
|
||||||
|
emit(Resource.Loading())
|
||||||
|
|
||||||
|
try {
|
||||||
|
val file = audioFile.toFile()
|
||||||
|
val response = profileApi.updateAudioDescription(
|
||||||
|
file = MultipartBody.Part.createFormData("audioFile", "profile-audio", file.asRequestBody()),
|
||||||
|
duration = MultipartBody.Part.createFormData("duration", duration.toString())
|
||||||
|
).awaitResponse()
|
||||||
|
|
||||||
|
if(response.isSuccessful) {
|
||||||
|
emit(Resource.Success(Audio.fromDto(response.body()!!)))
|
||||||
|
} else {
|
||||||
|
emit(Resource.Error(Resource.Error.mapErrorCode(response.code())))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
} catch(e: IOException) {
|
||||||
|
emit(Resource.Error(Resource.Error.ErrorType.OtherError))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -1,5 +1,7 @@
|
|||||||
package com.isolaatti.profile.domain
|
package com.isolaatti.profile.domain
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import com.isolaatti.audio.common.domain.Audio
|
||||||
import com.isolaatti.images.common.data.remote.ImageDto
|
import com.isolaatti.images.common.data.remote.ImageDto
|
||||||
import com.isolaatti.images.common.domain.entity.LocalImage
|
import com.isolaatti.images.common.domain.entity.LocalImage
|
||||||
import com.isolaatti.images.common.domain.entity.RemoteImage
|
import com.isolaatti.images.common.domain.entity.RemoteImage
|
||||||
@ -15,4 +17,6 @@ interface ProfileRepository {
|
|||||||
fun updateProfile(newDisplayName: String, newDescription: String): Flow<Resource<Boolean>>
|
fun updateProfile(newDisplayName: String, newDescription: String): Flow<Resource<Boolean>>
|
||||||
|
|
||||||
fun removeProfileImage(): Flow<Resource<Boolean>>
|
fun removeProfileImage(): Flow<Resource<Boolean>>
|
||||||
|
|
||||||
|
fun setProfileAudio(audioFile: Uri, duration: Long): Flow<Resource<Audio>>
|
||||||
}
|
}
|
||||||
@ -46,7 +46,7 @@ data class UserProfile(
|
|||||||
profileImageId = userProfileDto.profileImageId,
|
profileImageId = userProfileDto.profileImageId,
|
||||||
descriptionText = userProfileDto.descriptionText,
|
descriptionText = userProfileDto.descriptionText,
|
||||||
descriptionAudioId = userProfileDto.descriptionAudioId,
|
descriptionAudioId = userProfileDto.descriptionAudioId,
|
||||||
descriptionAudio = userProfileDto.audio?.let { Audio.fromDto(it) }
|
descriptionAudio = userProfileDto.descriptionAudio?.let { Audio.fromDto(it) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,13 @@
|
|||||||
|
package com.isolaatti.profile.domain.use_case
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import com.isolaatti.audio.common.domain.Audio
|
||||||
|
import com.isolaatti.profile.domain.ProfileRepository
|
||||||
|
import com.isolaatti.utils.Resource
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class SetProfileAudio @Inject constructor(private val profileRepository: ProfileRepository) {
|
||||||
|
|
||||||
|
operator fun invoke(audioUri: Uri, duration: Long): Flow<Resource<Audio>> = profileRepository.setProfileAudio(audioUri, duration)
|
||||||
|
}
|
||||||
@ -2,10 +2,6 @@ package com.isolaatti.profile.presentation
|
|||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
@ -18,6 +14,7 @@ import com.isolaatti.profile.domain.use_case.FollowUser
|
|||||||
import com.isolaatti.profile.domain.use_case.GetProfile
|
import com.isolaatti.profile.domain.use_case.GetProfile
|
||||||
import com.isolaatti.profile.domain.use_case.GetProfilePosts
|
import com.isolaatti.profile.domain.use_case.GetProfilePosts
|
||||||
import com.isolaatti.profile.domain.use_case.RemoveProfilePicture
|
import com.isolaatti.profile.domain.use_case.RemoveProfilePicture
|
||||||
|
import com.isolaatti.profile.domain.use_case.SetProfileAudio
|
||||||
import com.isolaatti.profile.domain.use_case.SetProfileImage
|
import com.isolaatti.profile.domain.use_case.SetProfileImage
|
||||||
import com.isolaatti.utils.Resource
|
import com.isolaatti.utils.Resource
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
@ -35,7 +32,8 @@ class ProfileViewModel @Inject constructor(
|
|||||||
private val getProfilePostsUseCase: GetProfilePosts,
|
private val getProfilePostsUseCase: GetProfilePosts,
|
||||||
private val setProfileImageUC: SetProfileImage,
|
private val setProfileImageUC: SetProfileImage,
|
||||||
private val followUserUC: FollowUser,
|
private val followUserUC: FollowUser,
|
||||||
private val removeProfilePictureUC: RemoveProfilePicture
|
private val removeProfilePictureUC: RemoveProfilePicture,
|
||||||
|
private val setProfileAudio: SetProfileAudio
|
||||||
) : PostListingViewModelBase() {
|
) : PostListingViewModelBase() {
|
||||||
private val _profile = MutableLiveData<UserProfile>()
|
private val _profile = MutableLiveData<UserProfile>()
|
||||||
val profile: LiveData<UserProfile> get() = _profile
|
val profile: LiveData<UserProfile> get() = _profile
|
||||||
@ -57,8 +55,12 @@ class ProfileViewModel @Inject constructor(
|
|||||||
|
|
||||||
val uploadingProfilePicture: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
val uploadingProfilePicture: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||||
val errorProfilePicture: MutableStateFlow<Resource.Error<RemoteImage>?> = MutableStateFlow(null)
|
val errorProfilePicture: MutableStateFlow<Resource.Error<RemoteImage>?> = MutableStateFlow(null)
|
||||||
val settingUpAudio: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
|
||||||
|
|
||||||
|
var audioDescriptionToUpload: Uri? = null
|
||||||
|
var ended: Boolean = false
|
||||||
|
|
||||||
|
val showAudioRecorder = MutableStateFlow(false)
|
||||||
|
val setAudioDescriptionError = MutableStateFlow(false)
|
||||||
|
|
||||||
// runs the lists of "Runnable" one by one and clears list. After this is executed,
|
// runs the lists of "Runnable" one by one and clears list. After this is executed,
|
||||||
// caller should report as handled
|
// caller should report as handled
|
||||||
@ -214,6 +216,33 @@ class ProfileViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setAudioDescriptionProfile() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
audioDescriptionToUpload?.let {
|
||||||
|
setProfileAudio(it, audioRecorderState.durationMilliseconds).onEach { response ->
|
||||||
|
when(response) {
|
||||||
|
is Resource.Error -> {
|
||||||
|
Log.d(TAG, "Error")
|
||||||
|
setAudioDescriptionError.value = true
|
||||||
|
uploadingAudio.value = false
|
||||||
|
}
|
||||||
|
is Resource.Loading -> {
|
||||||
|
setAudioDescriptionError.value = false
|
||||||
|
uploadingAudio.value = true
|
||||||
|
}
|
||||||
|
is Resource.Success -> {
|
||||||
|
Log.d(TAG, "Success audio = ${response.data}")
|
||||||
|
setAudioDescriptionError.value = false
|
||||||
|
uploadingAudio.value = false
|
||||||
|
showAudioRecorder.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.flowOn(Dispatchers.IO).launchIn(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun hideConfirmPictureSheet() {
|
fun hideConfirmPictureSheet() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
showConfirmChangeProfilePictureBottomSheet.value = false
|
showConfirmChangeProfilePictureBottomSheet.value = false
|
||||||
|
|||||||
@ -4,9 +4,7 @@ import android.content.ComponentName
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.media.MediaRecorder
|
import android.media.MediaRecorder
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.media.MediaRecorder
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
@ -18,26 +16,37 @@ import androidx.activity.result.contract.ActivityResultContracts
|
|||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.slideInVertically
|
import androidx.compose.animation.slideInVertically
|
||||||
import androidx.compose.animation.slideOutVertically
|
import androidx.compose.animation.slideOutVertically
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.FilledTonalIconButton
|
||||||
import androidx.compose.material3.FloatingActionButton
|
import androidx.compose.material3.FloatingActionButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
@ -50,7 +59,6 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
@ -60,7 +68,9 @@ import androidx.compose.ui.platform.ComposeView
|
|||||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
@ -69,10 +79,12 @@ import androidx.fragment.app.viewModels
|
|||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import androidx.media3.common.MediaItem
|
||||||
|
import androidx.media3.common.MediaMetadata
|
||||||
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.session.MediaController
|
import androidx.media3.session.MediaController
|
||||||
import androidx.media3.session.SessionToken
|
import androidx.media3.session.SessionToken
|
||||||
import androidx.navigation.findNavController
|
import androidx.navigation.findNavController
|
||||||
import coil3.toUri
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
import com.isolaatti.BuildConfig
|
import com.isolaatti.BuildConfig
|
||||||
@ -90,16 +102,13 @@ import com.isolaatti.common.options_bottom_sheet.presentation.BottomSheetPostOpt
|
|||||||
import com.isolaatti.common.options_bottom_sheet.ui.BottomSheetPostOptionsFragment
|
import com.isolaatti.common.options_bottom_sheet.ui.BottomSheetPostOptionsFragment
|
||||||
import com.isolaatti.followers.domain.FollowingState
|
import com.isolaatti.followers.domain.FollowingState
|
||||||
import com.isolaatti.hashtags.ui.HashtagsPostsActivity
|
import com.isolaatti.hashtags.ui.HashtagsPostsActivity
|
||||||
import com.isolaatti.images.common.domain.entity.LocalImage
|
|
||||||
import com.isolaatti.images.picture_viewer.ui.PictureViewerActivity
|
import com.isolaatti.images.picture_viewer.ui.PictureViewerActivity
|
||||||
import com.isolaatti.posting.comments.ui.BottomSheetPostComments
|
import com.isolaatti.posting.comments.ui.BottomSheetPostComments
|
||||||
import com.isolaatti.posting.posts.components.PostComponent
|
import com.isolaatti.posting.posts.components.PostComponent
|
||||||
import com.isolaatti.posting.posts.domain.entity.Post
|
import com.isolaatti.posting.posts.domain.entity.Post
|
||||||
import com.isolaatti.posting.posts.presentation.CreatePostContract
|
import com.isolaatti.posting.posts.presentation.CreatePostContract
|
||||||
import com.isolaatti.posting.posts.ui.CreatePostActivity
|
|
||||||
import com.isolaatti.posting.posts.ui.PostInfoActivity
|
import com.isolaatti.posting.posts.ui.PostInfoActivity
|
||||||
import com.isolaatti.posting.posts.viewer.ui.PostViewerActivity
|
import com.isolaatti.posting.posts.viewer.ui.PostViewerActivity
|
||||||
import com.isolaatti.profile.domain.entity.UserProfile
|
|
||||||
import com.isolaatti.profile.presentation.EditProfileContract
|
import com.isolaatti.profile.presentation.EditProfileContract
|
||||||
import com.isolaatti.profile.presentation.ProfileViewModel
|
import com.isolaatti.profile.presentation.ProfileViewModel
|
||||||
import com.isolaatti.profile.ui.components.ConfirmChangeProfilePictureBottomSheet
|
import com.isolaatti.profile.ui.components.ConfirmChangeProfilePictureBottomSheet
|
||||||
@ -111,12 +120,13 @@ import com.isolaatti.reports.data.ContentType
|
|||||||
import com.isolaatti.reports.ui.NewReportBottomSheetDialogFragment
|
import com.isolaatti.reports.ui.NewReportBottomSheetDialogFragment
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.guava.await
|
import kotlinx.coroutines.guava.await
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ProfileMainFragment : Fragment() {
|
class ProfileMainFragment : Fragment() {
|
||||||
@ -172,9 +182,6 @@ class ProfileMainFragment : Fragment() {
|
|||||||
private lateinit var mediaController: MediaController
|
private lateinit var mediaController: MediaController
|
||||||
private lateinit var mediaRecorder: MediaRecorder
|
private lateinit var mediaRecorder: MediaRecorder
|
||||||
|
|
||||||
private var mediaRecorder: MediaRecorder? = null
|
|
||||||
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
viewModel.mediaControllerIsAvailable.value = false
|
viewModel.mediaControllerIsAvailable.value = false
|
||||||
@ -190,36 +197,6 @@ class ProfileMainFragment : Fragment() {
|
|||||||
// initialized in onViewCreated
|
// initialized in onViewCreated
|
||||||
private lateinit var directory: File
|
private lateinit var directory: File
|
||||||
|
|
||||||
private fun startRecording(callback: () -> Unit) {
|
|
||||||
recordOutputFile = File(directory, "${System.currentTimeMillis()}.3gp")
|
|
||||||
mediaRecorder = if (Build.VERSION.SDK_INT >= 31) MediaRecorder(requireContext()) else MediaRecorder()
|
|
||||||
mediaRecorder?.run {
|
|
||||||
setAudioSource(MediaRecorder.AudioSource.MIC)
|
|
||||||
setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP)
|
|
||||||
setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB)
|
|
||||||
if (Build.VERSION.SDK_INT >= 26) {
|
|
||||||
setOutputFile(recordOutputFile)
|
|
||||||
} else {
|
|
||||||
setOutputFile(recordOutputFile.path)
|
|
||||||
}
|
|
||||||
|
|
||||||
prepare()
|
|
||||||
start()
|
|
||||||
callback()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun pauseRecording() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stopRecording() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun playRecordedAudio() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun pauseRecordedAudio() {
|
private fun pauseRecordedAudio() {
|
||||||
|
|
||||||
@ -231,15 +208,32 @@ class ProfileMainFragment : Fragment() {
|
|||||||
|
|
||||||
private fun playAudio(descriptionAudio: Audio) {
|
private fun playAudio(descriptionAudio: Audio) {
|
||||||
|
|
||||||
|
if(mediaController.currentMediaItem?.mediaId == descriptionAudio.id) {
|
||||||
|
mediaController.play()
|
||||||
|
} else {
|
||||||
|
val mediaItem = MediaItem.Builder()
|
||||||
|
.setMediaId(descriptionAudio.id)
|
||||||
|
.setUri(descriptionAudio.uri)
|
||||||
|
.build()
|
||||||
|
mediaController.setMediaItem(mediaItem)
|
||||||
|
mediaController.playWhenReady = true
|
||||||
|
mediaController.play()
|
||||||
|
viewModel.audioState.currentAudio = descriptionAudio
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.audioState.isPlaying = true
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun pauseAudio(descriptionAudio: Audio) {
|
private fun pauseAudio(descriptionAudio: Audio) {
|
||||||
|
mediaController.pause()
|
||||||
|
viewModel.audioState.isPlaying = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
@ -257,9 +251,6 @@ class ProfileMainFragment : Fragment() {
|
|||||||
val followingState by viewModel.followingState.observeAsState()
|
val followingState by viewModel.followingState.observeAsState()
|
||||||
val posts by viewModel.posts.collectAsState()
|
val posts by viewModel.posts.collectAsState()
|
||||||
val loadingProfile by viewModel.loadingProfile.collectAsState()
|
val loadingProfile by viewModel.loadingProfile.collectAsState()
|
||||||
val settingUpAudio by viewModel.settingUpAudio.collectAsState()
|
|
||||||
|
|
||||||
val audioPlaybackState by viewModel.audioState.collectAsState()
|
|
||||||
|
|
||||||
// use this to show or hide all audio related UI
|
// use this to show or hide all audio related UI
|
||||||
val mediaControllerIsAvailable by viewModel.mediaControllerIsAvailable.collectAsState()
|
val mediaControllerIsAvailable by viewModel.mediaControllerIsAvailable.collectAsState()
|
||||||
@ -301,7 +292,7 @@ class ProfileMainFragment : Fragment() {
|
|||||||
val newProfilePicture by viewModel.newProfileImage.collectAsState()
|
val newProfilePicture by viewModel.newProfileImage.collectAsState()
|
||||||
val uploadingProfilePicture by viewModel.uploadingProfilePicture.collectAsState()
|
val uploadingProfilePicture by viewModel.uploadingProfilePicture.collectAsState()
|
||||||
|
|
||||||
var showAudioRecorder by remember { mutableStateOf(false) }
|
val showAudioRecorder by viewModel.showAudioRecorder.collectAsState()
|
||||||
|
|
||||||
LaunchedEffect(profile) {
|
LaunchedEffect(profile) {
|
||||||
showAddPostButton = profile?.isUserItself == true
|
showAddPostButton = profile?.isUserItself == true
|
||||||
@ -312,6 +303,20 @@ class ProfileMainFragment : Fragment() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(viewModel.audioState.isPlaying) {
|
||||||
|
while(viewModel.audioState.isPlaying) {
|
||||||
|
delay(500.milliseconds)
|
||||||
|
viewModel.audioState.duration = mediaController.duration
|
||||||
|
viewModel.audioState.progress = mediaController.currentPosition
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAddAudioDescription() {
|
||||||
|
mediaRecorder = if (Build.VERSION.SDK_INT >= 31) MediaRecorder(requireContext()) else MediaRecorder()
|
||||||
|
recordOutputFile = File(directory, "${System.currentTimeMillis()}.3gp")
|
||||||
|
viewModel.showAudioRecorder.value = true
|
||||||
|
}
|
||||||
|
|
||||||
val followingText = when(followingState) {
|
val followingText = when(followingState) {
|
||||||
FollowingState.FollowingThisUser -> {
|
FollowingState.FollowingThisUser -> {
|
||||||
stringResource(R.string.following_user)
|
stringResource(R.string.following_user)
|
||||||
@ -329,9 +334,6 @@ class ProfileMainFragment : Fragment() {
|
|||||||
null -> ""
|
null -> ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
IsolaattiTheme {
|
IsolaattiTheme {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@ -430,25 +432,169 @@ class ProfileMainFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(showAudioRecorder) {
|
if(showAudioRecorder) {
|
||||||
ModalBottomSheet(onDismissRequest = { showAudioRecorder = false }) {
|
val setAudioRecorderModalBottomSheetState = rememberModalBottomSheetState()
|
||||||
AudioRecorder(
|
|
||||||
onDismiss = {},
|
LaunchedEffect(viewModel.audioRecorderState.isPlaying) {
|
||||||
onPlay = TODO(),
|
while(viewModel.audioRecorderState.isPlaying) {
|
||||||
onPause = TODO(),
|
delay(500.milliseconds)
|
||||||
onStopRecording = TODO(),
|
viewModel.audioRecorderState.position = mediaController.currentPosition
|
||||||
onPauseRecording = TODO(),
|
}
|
||||||
onStartRecording = TODO(),
|
|
||||||
isRecording = TODO(),
|
|
||||||
recordIsStopped = TODO(),
|
|
||||||
recordIsPaused = TODO(),
|
|
||||||
isPlaying = TODO(),
|
|
||||||
isLoading = TODO(),
|
|
||||||
durationSeconds = TODO(),
|
|
||||||
position = TODO(),
|
|
||||||
onSeek = TODO(),
|
|
||||||
modifier = TODO()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun dismiss() {
|
||||||
|
viewModel.showAudioRecorder.value = false
|
||||||
|
|
||||||
|
// stop playback
|
||||||
|
mediaController.stop()
|
||||||
|
mediaController.clearMediaItems()
|
||||||
|
|
||||||
|
// delete file
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
if(recordOutputFile.exists()) {
|
||||||
|
recordOutputFile.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// release media recorder
|
||||||
|
mediaRecorder.release()
|
||||||
|
|
||||||
|
// clear references
|
||||||
|
viewModel.audioRecorderState.clearState()
|
||||||
|
viewModel.audioDescriptionToUpload = null
|
||||||
|
}
|
||||||
|
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = {
|
||||||
|
viewModel.showAudioRecorder.value = false
|
||||||
|
dismiss()
|
||||||
|
},
|
||||||
|
sheetState = setAudioRecorderModalBottomSheetState
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.set_audio_description),
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
fontSize = 18.sp
|
||||||
|
)
|
||||||
|
val error by viewModel.setAudioDescriptionError.collectAsState()
|
||||||
|
|
||||||
|
if(error) {
|
||||||
|
Box(
|
||||||
|
Modifier.background(MaterialTheme.colorScheme.errorContainer)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.error_uploading_audio), modifier = Modifier.padding(8.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AudioRecorder(
|
||||||
|
onDismiss = { dismiss() },
|
||||||
|
onPlay = {
|
||||||
|
if(mediaController.isCommandAvailable(Player.COMMAND_PREPARE)) {
|
||||||
|
mediaController.prepare()
|
||||||
|
}
|
||||||
|
|
||||||
|
if(viewModel.ended) {
|
||||||
|
mediaController.seekTo(0L)
|
||||||
|
}
|
||||||
|
mediaController.playWhenReady = true
|
||||||
|
},
|
||||||
|
onPause = {
|
||||||
|
mediaController.pause()
|
||||||
|
},
|
||||||
|
onStopRecording = {
|
||||||
|
mediaRecorder.stop()
|
||||||
|
val audioUri = recordOutputFile.toUri()
|
||||||
|
mediaController.setMediaItem(
|
||||||
|
MediaItem.Builder()
|
||||||
|
.setUri(audioUri)
|
||||||
|
.setMediaMetadata(MediaMetadata.Builder()
|
||||||
|
.setTitle(getString(R.string.just_recorded_audio_title))
|
||||||
|
.build())
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
viewModel.audioDescriptionToUpload = audioUri
|
||||||
|
viewModel.audioRecorderState.recordIsStopped = true
|
||||||
|
viewModel.audioRecorderState.isRecording = false
|
||||||
|
viewModel.audioRecorderState.recordIsPaused = false
|
||||||
|
viewModel.audioRecorderState.durationMilliseconds = mediaController.duration
|
||||||
|
},
|
||||||
|
onPauseRecording = {
|
||||||
|
mediaRecorder.pause()
|
||||||
|
viewModel.audioRecorderState.recordIsStopped = false
|
||||||
|
viewModel.audioRecorderState.isRecording = true
|
||||||
|
viewModel.audioRecorderState.recordIsPaused = true
|
||||||
|
},
|
||||||
|
onStartRecording = { fromPause ->
|
||||||
|
|
||||||
|
if(fromPause) {
|
||||||
|
mediaRecorder.resume()
|
||||||
|
} else {
|
||||||
|
mediaRecorder.run {
|
||||||
|
setAudioSource(MediaRecorder.AudioSource.MIC)
|
||||||
|
setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP)
|
||||||
|
setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB)
|
||||||
|
if (Build.VERSION.SDK_INT >= 26) {
|
||||||
|
setOutputFile(recordOutputFile)
|
||||||
|
} else {
|
||||||
|
setOutputFile(recordOutputFile.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare()
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.audioRecorderState.recordIsStopped = false
|
||||||
|
viewModel.audioRecorderState.isRecording = true
|
||||||
|
viewModel.audioRecorderState.recordIsPaused = false
|
||||||
|
},
|
||||||
|
state = viewModel.audioRecorderState,
|
||||||
|
onSeek = { ms ->
|
||||||
|
mediaController.seekTo(ms.toLong())
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
AnimatedVisibility(visible = viewModel.audioRecorderState.recordIsStopped) {
|
||||||
|
val uploading by viewModel.uploadingAudio.collectAsState()
|
||||||
|
|
||||||
|
Row(modifier = Modifier.fillMaxWidth().padding(8.dp)) {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = {
|
||||||
|
viewModel.audioRecorderState.clearState()
|
||||||
|
mediaController.clearMediaItems()
|
||||||
|
|
||||||
|
// delete file
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
if(recordOutputFile.exists()) {
|
||||||
|
recordOutputFile.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.reset))
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
enabled = !uploading,
|
||||||
|
onClick = {
|
||||||
|
viewModel.setAudioDescriptionProfile()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
if(uploading) {
|
||||||
|
CircularProgressIndicator(Modifier.size(ButtonDefaults.IconSize))
|
||||||
|
}
|
||||||
|
Text(stringResource(R.string.save))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PullToRefreshBox(
|
PullToRefreshBox(
|
||||||
@ -490,74 +636,57 @@ class ProfileMainFragment : Fragment() {
|
|||||||
onFollowChange = { follow ->
|
onFollowChange = { follow ->
|
||||||
viewModel.followUser()
|
viewModel.followUser()
|
||||||
},
|
},
|
||||||
audioPlayer = {
|
audioPlayer = { descriptionAudio ->
|
||||||
if(mediaControllerIsAvailable) {
|
if(mediaControllerIsAvailable) {
|
||||||
when {
|
AudioPlayer(
|
||||||
settingUpAudio -> {
|
modifier = Modifier.fillMaxWidth(),
|
||||||
AudioRecorder(
|
positionMs = viewModel.audioState.progress,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
isPlaying = viewModel.audioState.isPlaying && (viewModel.audioState.currentAudio == it.descriptionAudio),
|
||||||
onDismiss = {
|
durationMs = viewModel.audioState.duration,
|
||||||
viewModel.settingUpAudio.value = false
|
isLoading = false,
|
||||||
},
|
onPlay = {
|
||||||
onPlay = {
|
playAudio(descriptionAudio)
|
||||||
playRecordedAudio()
|
},
|
||||||
},
|
onPause = {
|
||||||
onPause = {
|
pauseAudio(descriptionAudio)
|
||||||
pauseRecordedAudio()
|
},
|
||||||
},
|
onSeek = { ms ->
|
||||||
onStopRecording = {
|
seekTo(ms)
|
||||||
stopRecording()
|
},
|
||||||
},
|
options = {
|
||||||
onPauseRecording = {
|
if(it.isUserItself) {
|
||||||
pauseRecording()
|
var expanded by remember { mutableStateOf(false) }
|
||||||
},
|
|
||||||
onStartRecording = {
|
|
||||||
startRecording {
|
|
||||||
|
|
||||||
}
|
Box {
|
||||||
},
|
FilledTonalIconButton(
|
||||||
state = viewModel.audioRecorderState,
|
modifier = Modifier.padding(4.dp),
|
||||||
onSeek = {
|
|
||||||
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
it.descriptionAudio != null -> {
|
|
||||||
AudioPlayer(
|
|
||||||
modifier = Modifier.fillMaxWidth().height(120.dp),
|
|
||||||
positionMs = 2000L,
|
|
||||||
isPlaying = false,
|
|
||||||
durationMs = 30_000L,
|
|
||||||
isLoading = false,
|
|
||||||
onPlay = {
|
|
||||||
playAudio(it.descriptionAudio)
|
|
||||||
},
|
|
||||||
onPause = {
|
|
||||||
pauseAudio(it.descriptionAudio)
|
|
||||||
},
|
|
||||||
onSeek = { ms ->
|
|
||||||
seekTo(ms)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
it.isUserItself -> {
|
|
||||||
Card(Modifier.fillMaxWidth().height(120.dp).padding(16.dp)) {
|
|
||||||
Column(modifier = Modifier.fillMaxWidth().padding(8.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Center
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.setup_your_profile_audio))
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.settingUpAudio.value = true
|
expanded = true
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Text("Record audio")
|
Icon(Icons.Default.MoreVert, stringResource(R.string.audio_options))
|
||||||
|
}
|
||||||
|
|
||||||
|
DropdownMenu(expanded, onDismissRequest = { expanded = false }) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringResource(R.string.set_audio_description)) },
|
||||||
|
onClick = {
|
||||||
|
expanded = false
|
||||||
|
onAddAudioDescription()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringResource(R.string.remove)) },
|
||||||
|
onClick = {
|
||||||
|
expanded = false
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
@ -565,9 +694,7 @@ class ProfileMainFragment : Fragment() {
|
|||||||
editProfile.launch(it)
|
editProfile.launch(it)
|
||||||
},
|
},
|
||||||
onAddAudioDescription = {
|
onAddAudioDescription = {
|
||||||
mediaRecorder = if (Build.VERSION.SDK_INT >= 31) MediaRecorder(requireContext()) else MediaRecorder()
|
onAddAudioDescription()
|
||||||
showAudioRecorder = true
|
|
||||||
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -633,6 +760,31 @@ class ProfileMainFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val playerListener: Player.Listener = object: Player.Listener {
|
||||||
|
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||||
|
viewModel.audioRecorderState.isPlaying = isPlaying
|
||||||
|
viewModel.audioState.isPlaying = isPlaying
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||||
|
when(playbackState) {
|
||||||
|
Player.STATE_READY -> {
|
||||||
|
viewModel.ended = false
|
||||||
|
viewModel.audioRecorderState.isLoading = false
|
||||||
|
viewModel.audioRecorderState.durationMilliseconds = this@ProfileMainFragment.mediaController.duration
|
||||||
|
}
|
||||||
|
Player.STATE_ENDED -> {
|
||||||
|
viewModel.ended = true
|
||||||
|
}
|
||||||
|
Player.STATE_BUFFERING -> {
|
||||||
|
viewModel.audioRecorderState.isLoading = true
|
||||||
|
}
|
||||||
|
|
||||||
|
Player.STATE_IDLE -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
@ -641,6 +793,8 @@ class ProfileMainFragment : Fragment() {
|
|||||||
mediaController = mediaControllerFuture.await()
|
mediaController = mediaControllerFuture.await()
|
||||||
viewModel.mediaControllerIsAvailable.value = true
|
viewModel.mediaControllerIsAvailable.value = true
|
||||||
|
|
||||||
|
mediaController.addListener(playerListener)
|
||||||
|
|
||||||
// check audios directory
|
// check audios directory
|
||||||
directory = File(requireContext().filesDir, "recordings")
|
directory = File(requireContext().filesDir, "recordings")
|
||||||
|
|
||||||
|
|||||||
@ -76,6 +76,7 @@ fun ProfileHeader(
|
|||||||
loading: Boolean = false,
|
loading: Boolean = false,
|
||||||
isOwnUser: Boolean,
|
isOwnUser: Boolean,
|
||||||
followingThisUser: Boolean,
|
followingThisUser: Boolean,
|
||||||
|
audioPlayer: @Composable (Audio) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Box(modifier) {
|
Box(modifier) {
|
||||||
@ -133,11 +134,7 @@ fun ProfileHeader(
|
|||||||
OutlinedCard(modifier = Modifier.padding(16.dp).fillMaxWidth()) {
|
OutlinedCard(modifier = Modifier.padding(16.dp).fillMaxWidth()) {
|
||||||
Column(modifier = Modifier.padding(8.dp).fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
|
Column(modifier = Modifier.padding(8.dp).fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
Text(stringResource(R.string.you_have_now_audio))
|
Text(stringResource(R.string.you_have_now_audio))
|
||||||
FilledTonalButton(
|
FilledTonalButton(onClick = onAddAudioDescription) {
|
||||||
onClick = {
|
|
||||||
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.set_audio_description))
|
Text(stringResource(R.string.set_audio_description))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -145,7 +142,7 @@ fun ProfileHeader(
|
|||||||
}
|
}
|
||||||
|
|
||||||
audio != null -> {
|
audio != null -> {
|
||||||
|
audioPlayer(audio)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,6 +188,7 @@ fun ProfileHeaderPreview() {
|
|||||||
onFollowersClick = {},
|
onFollowersClick = {},
|
||||||
onFollowChange = {},
|
onFollowChange = {},
|
||||||
onEditProfileClick = {},
|
onEditProfileClick = {},
|
||||||
onAddAudioDescription = {}
|
onAddAudioDescription = {},
|
||||||
|
audioPlayer = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -235,6 +235,8 @@
|
|||||||
<string name="uploading_photo">Uploading photo</string>
|
<string name="uploading_photo">Uploading photo</string>
|
||||||
<string name="you_have_now_audio">You have no audio description</string>
|
<string name="you_have_now_audio">You have no audio description</string>
|
||||||
<string name="setup_your_profile_audio">Setup your profile audio</string>
|
<string name="setup_your_profile_audio">Setup your profile audio</string>
|
||||||
|
<string name="reset">Reset</string>
|
||||||
|
<string name="error_uploading_audio">An error ocurred while uploading the audio</string>
|
||||||
<string-array name="report_reasons">
|
<string-array name="report_reasons">
|
||||||
<item>Spam</item>
|
<item>Spam</item>
|
||||||
<item>Explicit content</item>
|
<item>Explicit content</item>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user