WIP audios

This commit is contained in:
Erik Everardo 2023-11-25 22:10:35 -06:00
parent a1c8b49607
commit 0322f79579
18 changed files with 435 additions and 32 deletions

View File

@ -0,0 +1,24 @@
package com.isolaatti.audio
import com.isolaatti.audio.common.data.AudiosApi
import com.isolaatti.audio.common.data.AudiosRepositoryImpl
import com.isolaatti.audio.common.domain.AudiosRepository
import com.isolaatti.connectivity.RetrofitClient
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
class Module {
@Provides
fun provideAudiosApi(retrofitClient: RetrofitClient): AudiosApi {
return retrofitClient.client.create(AudiosApi::class.java)
}
@Provides
fun provideAudiosRepository(audiosApi: AudiosApi): AudiosRepository {
return AudiosRepositoryImpl(audiosApi)
}
}

View File

@ -0,0 +1,50 @@
package com.isolaatti.audio.audios_list.presentation
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import coil.load
import com.isolaatti.audio.common.domain.Audio
import com.isolaatti.databinding.AudioListItemBinding
class AudiosAdapter(
private val onClick: ((audio: Audio) -> Unit),
private val onOptionsClick: ((audio: Audio, button: View) -> Boolean)
) : RecyclerView.Adapter<AudiosAdapter.AudiosViewHolder>() {
private var data: List<Audio> = listOf()
fun setData(audios: List<Audio>) {
data = audios
notifyDataSetChanged()
}
inner class AudiosViewHolder(val binding: AudioListItemBinding) : ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AudiosViewHolder {
val binding = AudioListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return AudiosViewHolder(binding)
}
override fun getItemCount(): Int {
return data.size
}
override fun onBindViewHolder(holder: AudiosViewHolder, position: Int) {
val audio = data[position]
holder.binding.audioName.text = audio.name
holder.binding.audioAuthor.text = audio.userName
holder.binding.thumbnail.load(audio.thumbnail)
holder.binding.root.setOnClickListener {
onClick(audio)
}
holder.binding.audioItemOptionsButton.setOnClickListener {
onOptionsClick(audio, it)
}
}
}

View File

@ -0,0 +1,31 @@
package com.isolaatti.audio.audios_list.presentation
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.isolaatti.audio.common.domain.Audio
import com.isolaatti.audio.common.domain.AudiosRepository
import com.isolaatti.utils.Resource
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 AudiosViewModel @Inject constructor(private val audiosRepository: AudiosRepository) : ViewModel() {
val resource: MutableLiveData<Resource<List<Audio>>> = MutableLiveData()
fun loadAudios(userId: Int) {
viewModelScope.launch {
audiosRepository.getAudiosOfUser(userId, null).onEach {
resource.postValue(it)
}.flowOn(Dispatchers.IO).launchIn(this)
}
}
}

View File

@ -0,0 +1,94 @@
package com.isolaatti.audio.audios_list.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager
import com.isolaatti.R
import com.isolaatti.audio.audios_list.presentation.AudiosAdapter
import com.isolaatti.audio.audios_list.presentation.AudiosViewModel
import com.isolaatti.audio.common.domain.Audio
import com.isolaatti.common.ErrorMessageViewModel
import com.isolaatti.databinding.FragmentAudiosBinding
import com.isolaatti.utils.Resource
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class AudiosFragment : Fragment() {
lateinit var viewBinding: FragmentAudiosBinding
private val viewModel: AudiosViewModel by viewModels()
private val errorViewModel: ErrorMessageViewModel by activityViewModels()
private val arguments: AudiosFragmentArgs by navArgs()
private lateinit var adapter: AudiosAdapter
private var loadedFirstTime = false
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
viewBinding = FragmentAudiosBinding.inflate(inflater)
return viewBinding.root
}
private val onOptionsClick: ((audio: Audio, button: View) -> Boolean) = { audio, button ->
val popup = PopupMenu(requireContext(), button)
popup.menuInflater.inflate(R.menu.audio_item_menu, popup.menu)
popup.show()
true
}
private val onAudioClick: ((audio: Audio) -> Unit) = {
// Play audio
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
adapter = AudiosAdapter(onClick = onAudioClick, onOptionsClick = onOptionsClick)
viewBinding.recyclerAudios.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
viewBinding.recyclerAudios.adapter = adapter
setupObservers()
if(arguments.source == SOURCE_PROFILE) {
viewModel.loadAudios(arguments.sourceId.toInt())
}
}
private fun setupObservers() {
viewModel.resource.observe(viewLifecycleOwner) { resource ->
when(resource) {
is Resource.Error -> {
errorViewModel.error.postValue(resource.errorType)
}
is Resource.Loading -> {
if(!loadedFirstTime) {
viewBinding.progressBarLoading.visibility = View.VISIBLE
}
}
is Resource.Success -> {
viewBinding.progressBarLoading.visibility = View.GONE
loadedFirstTime = true
adapter.setData(resource.data!!)
}
}
}
}
companion object {
const val SOURCE_PROFILE = "source_profile"
const val SOURCE_SQUAD = "source_squads"
}
}

View File

@ -0,0 +1,13 @@
package com.isolaatti.audio.common.data
import java.time.ZonedDateTime
data class AudiosDto(val data: List<AudioDto>)
data class AudioDto(
val id: String,
val name: String,
val creationTime: ZonedDateTime,
val userId: Int,
val firestoreObjectPath: String,
val userName: String
)

View File

@ -0,0 +1,11 @@
package com.isolaatti.audio.common.data
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
interface AudiosApi {
@GET("/api/Audios/OfUser/{userId}")
fun getAudiosOfUser(@Path("userId") userId: Int, @Query("lastAudioId") lastAudioId: String?): Call<AudiosDto>
}

View File

@ -0,0 +1,32 @@
package com.isolaatti.audio.common.data
import android.util.Log
import com.isolaatti.audio.common.domain.Audio
import com.isolaatti.audio.common.domain.AudiosRepository
import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import retrofit2.awaitResponse
import javax.inject.Inject
class AudiosRepositoryImpl @Inject constructor(private val audiosApi: AudiosApi) : AudiosRepository {
override fun getAudiosOfUser(userId: Int, lastId: String?): Flow<Resource<List<Audio>>> = flow {
emit(Resource.Loading())
try {
val response = audiosApi.getAudiosOfUser(userId, lastId).awaitResponse()
if(response.isSuccessful) {
val body = response.body()
if(body == null) {
emit(Resource.Error(Resource.Error.ErrorType.ServerError))
return@flow
}
emit(Resource.Success(body.data.map { Audio.fromDto(it) }))
}
} catch(e: Exception) {
Log.e("AudiosRepositoryImpl", e.message.toString())
emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
}
}
}

View File

@ -0,0 +1,37 @@
package com.isolaatti.audio.common.domain
import com.isolaatti.audio.common.data.AudioDto
import com.isolaatti.common.Ownable
import com.isolaatti.connectivity.RetrofitClient.Companion.BASE_URL
import com.isolaatti.utils.UrlGen
import java.time.ZonedDateTime
data class Audio(
val id: String,
val name: String,
val creationTime: ZonedDateTime,
override val userId: Int,
val userName: String
): Ownable {
var playing: Boolean = false
val downloadUrl: String get() {
return "${BASE_URL}/$id"
}
val thumbnail: String get() {
return UrlGen.userProfileImage(userId)
}
companion object {
fun fromDto(audioDto: AudioDto): Audio {
return Audio(
id = audioDto.id,
name = audioDto.name,
creationTime = audioDto.creationTime,
userId = audioDto.userId,
userName = audioDto.userName
)
}
}
}

View File

@ -0,0 +1,8 @@
package com.isolaatti.audio.common.domain
import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow
interface AudiosRepository {
fun getAudiosOfUser(userId: Int, lastId: String?): Flow<Resource<List<Audio>>>
}

View File

@ -1,10 +1,12 @@
package com.isolaatti.connectivity
import com.google.gson.GsonBuilder
import com.isolaatti.BuildConfig
import com.isolaatti.type_adapters.LocalDateTimeAdapter
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.io.IOException
import java.time.ZonedDateTime
import javax.inject.Inject
@ -25,10 +27,14 @@ class RetrofitClient @Inject constructor(private val authenticationInterceptor:
.addInterceptor(authenticationInterceptor)
.build()
private val gson = GsonBuilder()
.registerTypeAdapter(ZonedDateTime::class.java, LocalDateTimeAdapter())
.create()
val client: Retrofit get() = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
}

View File

@ -1,21 +0,0 @@
package com.isolaatti.profile.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.isolaatti.databinding.FragmentAudiosBinding
class AudiosFragment : Fragment() {
lateinit var viewBinding: FragmentAudiosBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewBinding = FragmentAudiosBinding.inflate(inflater)
return viewBinding.root
}
}

View File

@ -19,26 +19,26 @@ import androidx.recyclerview.widget.LinearLayoutManager
import coil.load
import com.isolaatti.BuildConfig
import com.isolaatti.R
import com.isolaatti.audio.audios_list.ui.AudiosFragment
import com.isolaatti.common.CoilImageLoader.imageLoader
import com.isolaatti.common.Dialogs
import com.isolaatti.common.ErrorMessageViewModel
import com.isolaatti.databinding.FragmentDiscussionsBinding
import com.isolaatti.followers.domain.FollowingState
import com.isolaatti.posting.posts.viewer.ui.PostViewerActivity
import com.isolaatti.posting.comments.ui.BottomSheetPostComments
import com.isolaatti.common.Ownable
import com.isolaatti.common.options_bottom_sheet.domain.OptionClicked
import com.isolaatti.common.options_bottom_sheet.domain.Options
import com.isolaatti.common.options_bottom_sheet.presentation.BottomSheetPostOptionsViewModel
import com.isolaatti.common.options_bottom_sheet.ui.BottomSheetPostOptionsFragment
import com.isolaatti.databinding.FragmentDiscussionsBinding
import com.isolaatti.followers.domain.FollowingState
import com.isolaatti.images.image_list.ui.ImagesFragment
import com.isolaatti.images.picture_viewer.ui.PictureViewerActivity
import com.isolaatti.posting.comments.ui.BottomSheetPostComments
import com.isolaatti.posting.posts.domain.entity.Post
import com.isolaatti.posting.posts.presentation.CreatePostContract
import com.isolaatti.posting.posts.presentation.EditPostContract
import com.isolaatti.posting.posts.presentation.PostListingRecyclerViewAdapterWiring
import com.isolaatti.posting.posts.presentation.PostsRecyclerViewAdapter
import com.isolaatti.posting.posts.presentation.UpdateEvent
import com.isolaatti.posting.posts.viewer.ui.PostViewerActivity
import com.isolaatti.profile.domain.entity.UserProfile
import com.isolaatti.profile.presentation.ProfileViewModel
import com.isolaatti.utils.UrlGen
@ -214,7 +214,7 @@ class ProfileMainFragment : Fragment() {
viewBinding.bottomAppBar.setOnMenuItemClickListener {
when(it.itemId) {
R.id.audios_menu_item -> {
findNavController().navigate(ProfileMainFragmentDirections.actionDiscussionsFragmentToAudiosFragment())
findNavController().navigate(ProfileMainFragmentDirections.actionDiscussionsFragmentToAudiosFragment(AudiosFragment.SOURCE_PROFILE, userId.toString()))
true
}
R.id.images_menu_item -> {

View File

@ -0,0 +1,18 @@
package com.isolaatti.type_adapters
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import java.lang.reflect.Type
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
class LocalDateTimeAdapter : JsonDeserializer<ZonedDateTime> {
override fun deserialize(
json: JsonElement?,
typeOfT: Type?,
context: JsonDeserializationContext?
): ZonedDateTime {
return ZonedDateTime.parse(json?.asString, DateTimeFormatter.ISO_DATE_TIME)
}
}

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="70dp"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="4dp"
style="?attr/materialCardViewFilledStyle"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/thumbnail"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_margin="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
tools:src="@drawable/baseline_image_24" />
<TextView
android:id="@+id/audio_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:lines="1"
android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@+id/audio_author"
app:layout_constraintEnd_toStartOf="@+id/audio_item_options_button"
app:layout_constraintStart_toEndOf="@id/thumbnail"
app:layout_constraintTop_toTopOf="parent"
tools:text="Hello" />
<TextView
android:id="@+id/audio_author"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/audio_item_options_button"
app:layout_constraintStart_toEndOf="@id/thumbnail"
tools:text="Erik" />
<Button
android:id="@+id/audio_item_options_button"
style="?attr/materialIconButtonFilledTonalStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
app:icon="@drawable/baseline_more_horiz_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:contentDescription="@string/audio_options"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -2,7 +2,8 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout 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:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
@ -15,4 +16,17 @@
app:navigationIconTint="@color/on_surface"
app:titleCentered="true"/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_audios"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<ProgressBar
android:id="@+id/progress_bar_loading"
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
tools:visibility="visible" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/delete_item"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:title="@string/delete" />
</menu>

View File

@ -31,8 +31,14 @@
</fragment>
<fragment
android:id="@+id/audiosFragment"
android:name="com.isolaatti.profile.ui.AudiosFragment"
android:label="AudiosFragment" />
android:name="com.isolaatti.audio.audios_list.ui.AudiosFragment"
android:label="AudiosFragment" >
<argument
android:name="source"
app:argType="string" />
<argument android:name="sourceId"
app:argType="string" />
</fragment>
<fragment
android:id="@+id/imagesFragment"
android:name="com.isolaatti.images.image_list.ui.ImagesFragment"
@ -64,4 +70,7 @@
android:name="userId"
app:argType="integer" />
</fragment>
<argument
android:name="userId"
app:argType="integer" />
</navigation>

View File

@ -121,4 +121,5 @@
<string name="delete_images_dialog_message">Remove %d images?</string>
<string name="selected_images_count">Images selected: %d</string>
<string name="picture_name">Picture name</string>
<string name="audio_options">Audio options</string>
</resources>