diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 19262f0..a263724 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,13 +15,25 @@ android:supportsRtl="true" android:theme="@style/Theme.Woka" tools:targetApi="31"> + + + android:theme="@style/FullScreenTheme" /> , private var onBookClicked: (ContinueAudioData) -> Unit, - private var onContinueBookChanged: (id: Int) -> Unit): ListAdapter(config) { + private var onContinueBookChanged: (id: Int) -> Unit): ListAdapter(DIFF_CONFIG) { companion object{ private val DIFF_UTIL = object : DiffUtil.ItemCallback(){ @@ -39,10 +38,6 @@ class ContinueAudioAdapter(private val context: Context, .build() } - constructor(context: Context, - onBookClicked: (ContinueAudioData) -> Unit, - onContinueBookChanged: (id: Int) -> Unit): this(context, DIFF_CONFIG, onBookClicked, onContinueBookChanged) - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FavoriteViewHolder { return FavoriteViewHolder( FavViewHolderBinding.inflate( diff --git a/app/src/main/java/com/woka/home/fragments/Home1Fragment.kt b/app/src/main/java/com/woka/home/fragments/Home1Fragment.kt index 6de2ac8..2dacd15 100644 --- a/app/src/main/java/com/woka/home/fragments/Home1Fragment.kt +++ b/app/src/main/java/com/woka/home/fragments/Home1Fragment.kt @@ -25,6 +25,7 @@ import com.woka.home.views.FMActivity import com.woka.home.viewmodels.HomeViewModel import com.woka.home.views.MoreHomeActivity import com.woka.home.models.TimePeriod +import com.woka.karaoke.views.KaraokeActivity import com.woka.userdata.userDataModels.UserDataResponse import com.woka.networking.ApiResult import com.woka.players.views.LiveStreamPlayerActivity @@ -193,6 +194,12 @@ class Home1Fragment : Fragment(), Listener { startActivity(Intent(it, GamesActivity::class.java)) } } + + karaoke.setOnClickListener { + activity?.let { + startActivity(Intent(it, KaraokeActivity::class.java)) + } + } } } diff --git a/app/src/main/java/com/woka/home/fragments/MyListFragment.kt b/app/src/main/java/com/woka/home/fragments/MyListFragment.kt index cd7df64..993b4b1 100644 --- a/app/src/main/java/com/woka/home/fragments/MyListFragment.kt +++ b/app/src/main/java/com/woka/home/fragments/MyListFragment.kt @@ -29,12 +29,15 @@ import com.woka.databinding.FragmentMyListBinding import com.woka.home.mylist.MyListRepository import com.woka.home.mylist.adapters.FavAudioAdapter import com.woka.home.mylist.adapters.FavGamesAdapter -import com.woka.home.mylist.adapters.KaraokeAdapter +import com.woka.home.mylist.adapters.FavKaraokeAdapter import com.woka.home.mylist.adapters.WebSeriesAdapter import com.woka.home.mylist.models.PostType import com.woka.home.mylist.models.BookmarkedShowData import com.woka.home.mylist.models.FavAudioBookData import com.woka.home.mylist.models.FavGameData +import com.woka.home.mylist.models.FavKaraokeData +import com.woka.karaoke.KaraokeRepository +import com.woka.karaoke.models.listing.KaraokeData import com.woka.networking.ApiResult import com.woka.players.models.VideoPlayList import com.woka.players.views.PlayerActivity @@ -59,7 +62,7 @@ class MyListFragment : Fragment() { private lateinit var webSeriesEAdapter: WebSeriesAdapter private lateinit var webSeriesHAdapter: WebSeriesAdapter private lateinit var audioBooksAdapter: FavAudioAdapter - private lateinit var karaokeAdapter: KaraokeAdapter + private lateinit var karaokeAdapter: FavKaraokeAdapter private lateinit var gamesAdapter: FavGamesAdapter private var webShowIntentLauncher: ActivityResultLauncher? = null @@ -77,7 +80,7 @@ class MyListFragment : Fragment() { webSeriesEAdapter = WebSeriesAdapter(requireContext(),"1", ::onListGotEmpty) webSeriesHAdapter = WebSeriesAdapter(requireContext(),"18", ::onListGotEmpty) audioBooksAdapter = FavAudioAdapter(requireContext(), ::onListGotEmpty, ::onAudioBookClicked) - karaokeAdapter = KaraokeAdapter(requireContext(), ::onListGotEmpty) + karaokeAdapter = FavKaraokeAdapter(requireContext(), ::onListGotEmpty, ::onKaraokeClicked) gamesAdapter = FavGamesAdapter(requireContext(), ::onListGotEmpty, ::onGameClicked) // dialogs @@ -606,6 +609,128 @@ class MyListFragment : Fragment() { } } + // karaoke + private fun onKaraokeClicked(karaokeData: FavKaraokeData, position: Int) { + dialogBinding.apply { + karaokeData.content_more_details?.let { moreDetailsList -> + + karaokeData.thumbnail_path?.let { + image.loadImage(it) + } + + fav.isSelected = karaokeData.mark_as_favourite == true + like.isSelected = karaokeData.is_liked == true + likeCount.text = "${karaokeData.likes_count}" + + year.text = try { + karaokeData.release_date?.let { + val cal = Calendar.getInstance() + cal.time = + SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).parse( + karaokeData.release_date + )!! + "${cal.get(Calendar.YEAR)}" + } ?: throw Exception() + } catch (e: Exception) { + "${karaokeData.release_date}" + } + + if (moreDetailsList.isNotEmpty()) { + + if (WokaApp.userPrefs?.appLanguage == "hi" && moreDetailsList.size > 1) { + moreDetailsList[1]?.let { data -> + title.text = data.title + description.text = Html.fromHtml( + data.description?.replace( + "
", + " " + ), Html.FROM_HTML_MODE_LEGACY + ) + } + } else { + moreDetailsList[0]?.let { data -> + title.text = data.title + description.text = Html.fromHtml( + data.description?.replace( + "
", + " " + ), Html.FROM_HTML_MODE_LEGACY + ) + } + } + } else { + title.text = karaokeData.title + description.text = Html.fromHtml( + karaokeData.description?.replace( + "
", + " " + ), Html.FROM_HTML_MODE_LEGACY + ) + } + + activity?.let { + watchCard.backgroundTintList = ColorStateList.valueOf(it.getColor(R.color.game_grad_one)) + } + watchCard.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_mic, 0, 0, 0) + watchCard.text = getString(R.string.sing_now) + watchCard.setOnClickListener { + + } + + like.setOnClickListener { + KaraokeRepository.likeUnLikeSong( + "${karaokeData.id}", + !like.isSelected + ) + + karaokeData.id?.let { + onKaraokeChanged(it) + } + like.isSelected = !like.isSelected + likeCount.text = "${karaokeData.likes_count}" + } + + fav.setOnClickListener { + if (context?.isNetworkConnected() == false){ + toast(getString(R.string.no_internet)) + return@setOnClickListener + } + + KaraokeRepository.updateFavShow( + KaraokeData(karaokeData), + !fav.isSelected + ) + + try { + karaokeAdapter.notifyItemRemoved(position) + } catch (e: Exception) { + // do nothing + } + + if (karaokeAdapter.currentList.isEmpty()){ + onListGotEmpty(PostType.KARAOKE, true) + } + + moduleShowerDialog.dismiss() + } + + close.setOnClickListener { + moduleShowerDialog.dismiss() + } + + moduleShowerDialog.show() + } + } + } + + private fun onKaraokeChanged(id: Int) { + // updating continue book list + val position = karaokeAdapter.currentList.indexOfFirst { it.id == id } + if (position >= 0 && position < karaokeAdapter.currentList.size) { + karaokeAdapter.notifyItemChanged(position) + } + } + companion object { fun newInstance() = MyListFragment() } diff --git a/app/src/main/java/com/woka/home/mylist/adapters/KaraokeAdapter.kt b/app/src/main/java/com/woka/home/mylist/adapters/FavKaraokeAdapter.kt similarity index 56% rename from app/src/main/java/com/woka/home/mylist/adapters/KaraokeAdapter.kt rename to app/src/main/java/com/woka/home/mylist/adapters/FavKaraokeAdapter.kt index a4991c3..abdcad4 100644 --- a/app/src/main/java/com/woka/home/mylist/adapters/KaraokeAdapter.kt +++ b/app/src/main/java/com/woka/home/mylist/adapters/FavKaraokeAdapter.kt @@ -1,34 +1,34 @@ package com.woka.home.mylist.adapters import android.content.Context -import android.util.Log import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter -import com.bumptech.glide.Glide import com.woka.R import com.woka.databinding.FavViewHolderBinding -import com.woka.home.mylist.MyListRepository +import com.woka.home.mylist.models.FavKaraokeData import com.woka.home.mylist.models.PostType -import com.woka.home.mylist.models.SingKaraokeData -import com.woka.utils.TAG +import com.woka.karaoke.KaraokeRepository +import com.woka.karaoke.models.listing.KaraokeData import com.woka.utils.isNetworkConnected import com.woka.utils.toast import java.util.concurrent.Executors -import kotlin.math.max -class KaraokeAdapter(private val context: Context, - config: AsyncDifferConfig, - private val onListEmptyListener: ((postType: PostType, isEng: Boolean) -> Unit)): ListAdapter(config) { +class FavKaraokeAdapter(private val context: Context, + private val onListEmptyListener: ((postType: PostType, isEng: Boolean) -> Unit), + private var onKaraokeClicked: (FavKaraokeData, Int) -> Unit) + : ListAdapter(DIFF_CONFIG) { companion object{ - private val DIFF_UTIL = object : DiffUtil.ItemCallback(){ - override fun areItemsTheSame(oldItem: SingKaraokeData, newItem: SingKaraokeData): Boolean = oldItem.id == newItem.id - override fun areContentsTheSame(oldItem: SingKaraokeData, newItem: SingKaraokeData): Boolean { - return oldItem.title == newItem.title && + private val DIFF_UTIL = object : DiffUtil.ItemCallback(){ + override fun areItemsTheSame(oldItem: FavKaraokeData, newItem: FavKaraokeData): Boolean = oldItem.id == newItem.id + override fun areContentsTheSame(oldItem: FavKaraokeData, newItem: FavKaraokeData): Boolean { + return oldItem.thumbnail_path == newItem.thumbnail_path && + oldItem.title == newItem.title && oldItem.is_liked == newItem.is_liked && + oldItem.mark_as_favourite == newItem.mark_as_favourite && oldItem.likes_count == newItem.likes_count } } @@ -38,8 +38,6 @@ class KaraokeAdapter(private val context: Context, .build() } - constructor(context: Context, onListEmptyListener: ((postType: PostType, isEng: Boolean) -> Unit)): this(context, DIFF_CONFIG, onListEmptyListener) - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FavoriteViewHolder { return FavoriteViewHolder( FavViewHolderBinding.inflate( @@ -74,7 +72,13 @@ class KaraokeAdapter(private val context: Context, return@setOnClickListener } + KaraokeRepository.likeUnLikeSong( + "${karaokeData.id}", + !like.isSelected + ) + like.isSelected = !like.isSelected + likeCount.text = "${karaokeData.likes_count}" } fav.isSelected = true @@ -85,6 +89,20 @@ class KaraokeAdapter(private val context: Context, return@setOnClickListener } + KaraokeRepository.updateFavShow( + KaraokeData(karaokeData), + !fav.isSelected + ) + + notifyItemRemoved(holder.absoluteAdapterPosition) + + if (currentList.isEmpty()){ + onListEmptyListener(PostType.KARAOKE, true) + } + } + + root.setOnClickListener { + onKaraokeClicked(karaokeData, holder.absoluteAdapterPosition) } } } diff --git a/app/src/main/java/com/woka/home/mylist/models/FavKaraokeData.kt b/app/src/main/java/com/woka/home/mylist/models/FavKaraokeData.kt new file mode 100644 index 0000000..7891076 --- /dev/null +++ b/app/src/main/java/com/woka/home/mylist/models/FavKaraokeData.kt @@ -0,0 +1,46 @@ +package com.woka.home.mylist.models + +import com.woka.karaoke.models.listing.ContentMoreDetail +import com.woka.karaoke.models.listing.KaraokeData + +data class FavKaraokeData( + val age_range_master_id: String?, + val bookmark_category_ids: String?, + val bookmark_count: Int?, + val category_master_id: String?, + val content_more_details: List?, + val description: String?, + val duration: String?, + val gender_master_id: String?, + val id: Int?, + var is_liked: Boolean?, + val language_master_id: Int?, + var likes_count: Int?, + val mark_as_favourite: Boolean?, + val release_date: String?, + val thumbnail_path: String?, + val title: String?, + val video_url: String?, + val views_count: Int? +){ + constructor(karaoke: KaraokeData): this( + karaoke.age_range_master_id, + karaoke.bookmark_category_ids, + karaoke.bookmark_count, + karaoke.category_master_id, + karaoke.content_more_details, + karaoke.description, + karaoke.duration, + karaoke.gender_master_id, + karaoke.id, + karaoke.is_liked, + karaoke.language_master_id, + karaoke.likes_count, + karaoke.mark_as_favourite, + karaoke.release_date, + karaoke.thumbnail_path, + karaoke.title, + karaoke.video_url, + karaoke.views_count + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/woka/home/mylist/models/Result.kt b/app/src/main/java/com/woka/home/mylist/models/Result.kt index a2292f8..d44c5a9 100644 --- a/app/src/main/java/com/woka/home/mylist/models/Result.kt +++ b/app/src/main/java/com/woka/home/mylist/models/Result.kt @@ -4,6 +4,6 @@ data class Result( val audio_data: MutableList?, val game_data: MutableList?, val show_data: MutableList?, - val sing_karaoke_data: MutableList?, + val sing_karaoke_data: MutableList?, val video_data: MutableList? ) \ No newline at end of file diff --git a/app/src/main/java/com/woka/karaoke/KaraokeRepository.kt b/app/src/main/java/com/woka/karaoke/KaraokeRepository.kt index b6900f6..b96b5f7 100644 --- a/app/src/main/java/com/woka/karaoke/KaraokeRepository.kt +++ b/app/src/main/java/com/woka/karaoke/KaraokeRepository.kt @@ -3,7 +3,10 @@ package com.woka.karaoke import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.woka.home.mylist.MyListRepository +import com.woka.home.mylist.models.FavKaraokeData import com.woka.home.mylist.models.PostType +import com.woka.karaoke.models.continuesing.ContinueKaraokeResponse +import com.woka.karaoke.models.listing.KaraokeData import com.woka.karaoke.models.listing.KaraokeResponse import com.woka.networking.ApiResult import com.woka.networking.RetrofitHelper @@ -29,7 +32,14 @@ object KaraokeRepository { private var karaokeData: KaraokeResponse? = null - fun loadGames(){ + // continue sing karaoke data + private val _continueKaraokeLiveData = MutableLiveData>() + val continueKaraokeLiveData: LiveData> + get() = _continueKaraokeLiveData + + private var continueKaraokeData: ContinueKaraokeResponse? = null + + fun loadKaraokeSongs(){ if (karaokeData != null){ _karaokeLiveData.postValue(ApiResult.Success(karaokeData)) return @@ -38,7 +48,7 @@ object KaraokeRepository { CoroutineScope(Dispatchers.IO).launch{ _karaokeLiveData.postValue(ApiResult.Loading()) - val response = RetrofitHelper.handleApiCall { + val response = handleApiCall { apiService.karaokeListing( FormBody.Builder() .build() @@ -60,7 +70,36 @@ object KaraokeRepository { } } - fun likeUnLikeGame(postId: String, likeIt: Boolean){ + fun loadContinueKaraoke(){ + if (continueKaraokeData != null){ + _continueKaraokeLiveData.postValue(ApiResult.Success(continueKaraokeData)) + return + } + + CoroutineScope(Dispatchers.IO).launch { + val response = handleApiCall { + userActionApiService.continueKaraokeListing( + FormBody.Builder() + .add("post_type", PostType.KARAOKE.value) + .build() + ) + } + + when (response){ + is ApiResult.Error -> {} + is ApiResult.Loading -> {} + is ApiResult.Success -> { + response.data?.let { + continueKaraokeData = it + } + } + } + + _continueKaraokeLiveData.postValue(response) + } + } + + fun likeUnLikeSong(postId: String, likeIt: Boolean){ CoroutineScope(Dispatchers.IO).launch { handleApiCall { if (likeIt){ @@ -106,6 +145,27 @@ object KaraokeRepository { } } + // continue Karaoke data update + continueKaraokeData?.result?.let { + for (audio in it){ + var found = false + audio?.let {data -> + if ("${data.id}" == id){ + + data.is_liked = isLiked + + data.likes_count?.let { count -> + data.likes_count = if (isLiked) count + 1 + else max(0, count - 1) + } + + found = true + } + } + if (found) break + } + } + // changing in fav list locally MyListRepository.myFavData.result?.sing_karaoke_data?.let { for (audioData in it){ @@ -120,4 +180,59 @@ object KaraokeRepository { } } } + + fun updateFavShow(karaoke: KaraokeData, addToBookmark: Boolean){ + CoroutineScope(Dispatchers.IO).launch { + handleApiCall { + if (addToBookmark){ + userActionApiService.addToFav( + FormBody.Builder() + .add("post_id", "${karaoke.id}") + .add("post_type", PostType.KARAOKE.value) + .build() + ) + }else{ + userActionApiService.removeFromFav( + FormBody.Builder() + .add("id", "${karaoke.id}") + .add("post_type", PostType.KARAOKE.value) + .build() + ) + } + } + } + + MyListRepository.myFavData.result?.sing_karaoke_data?.let {favAudioData -> + karaoke.mark_as_favourite = addToBookmark + if (addToBookmark){ + favAudioData.add(FavKaraokeData(karaoke)) + }else{ + favAudioData.removeIf{it.id == karaoke.id} + } + } + + karaokeData?.karaoke_data?.let { + for (audio in it){ + if (audio?.id == karaoke.id){ + audio?.mark_as_favourite = addToBookmark + break + } + } + } + + continueKaraokeData?.result?.let { + for (audio in it){ + if (audio?.id == karaoke.id){ + audio?.mark_as_favourite = addToBookmark + break + } + } + } + + } + + fun clearData(){ + karaokeData = null + continueKaraokeData = null + } } \ No newline at end of file diff --git a/app/src/main/java/com/woka/karaoke/adapters/ContinueKaraokeAdapter.kt b/app/src/main/java/com/woka/karaoke/adapters/ContinueKaraokeAdapter.kt new file mode 100644 index 0000000..622cf46 --- /dev/null +++ b/app/src/main/java/com/woka/karaoke/adapters/ContinueKaraokeAdapter.kt @@ -0,0 +1,134 @@ +package com.woka.karaoke.adapters + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.AsyncDifferConfig +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import com.woka.R +import com.woka.WokaApp.Companion.userPrefs +import com.woka.databinding.FavViewHolderBinding +import com.woka.karaoke.KaraokeRepository +import com.woka.karaoke.models.continuesing.ContinueKaraokeData +import com.woka.karaoke.models.listing.KaraokeData +import com.woka.utils.isNetworkConnected +import com.woka.utils.show +import com.woka.utils.toast +import java.util.concurrent.Executors + +class ContinueKaraokeAdapter( + private val context: Context, + private var onKaraokeClicked: (KaraokeData) -> Unit, + private var onKaraokeChanged: (id: Int, isFromContinue: Boolean) -> Unit +): ListAdapter(ASYNC_DIFF_UTIL) { + + inner class AudioBookViewHolder(val binding: FavViewHolderBinding): ViewHolder(binding.root) + + companion object{ + val DIFF_UTIL = object : DiffUtil.ItemCallback(){ + override fun areItemsTheSame(oldItem: ContinueKaraokeData, newItem: ContinueKaraokeData): Boolean = oldItem.id == newItem.id + + override fun areContentsTheSame(oldItem: ContinueKaraokeData, newItem: ContinueKaraokeData): Boolean { + return oldItem.thumbnail_path == newItem.thumbnail_path && + oldItem.title == newItem.title && + oldItem.is_liked == newItem.is_liked && + oldItem.mark_as_favourite == newItem.mark_as_favourite && + oldItem.likes_count == newItem.likes_count + } + } + + val ASYNC_DIFF_UTIL = AsyncDifferConfig.Builder(DIFF_UTIL) + .setBackgroundThreadExecutor(Executors.newSingleThreadExecutor()) + .build() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AudioBookViewHolder { + return AudioBookViewHolder( + FavViewHolderBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: AudioBookViewHolder, position: Int) { + val karaokeData = getItem(holder.absoluteAdapterPosition) + + holder.binding.apply { + karaokeData.thumbnail_path?.let { + image.loadImage(it) + } + + karaokeData.content_more_details?.let {moreDetailsList -> + title.text = if (moreDetailsList.isNotEmpty()){ + if (userPrefs?.appLanguage == "hi" && moreDetailsList.size > 1){ + moreDetailsList[1]?.title + }else{ + moreDetailsList[0]?.title + } + }else{ + karaokeData.title + } + } + + like.show() + likeCount.show() + fav.show() + + karaokeData.likes_count?.let { + likeCount.text = "$it" + } + + karaokeData.is_liked?.let { + like.isSelected = it + } + + like.setOnClickListener { + if (!context.isNetworkConnected()){ + context.toast(context.getString(R.string.no_internet)) + return@setOnClickListener + } + + KaraokeRepository.likeUnLikeSong( + "${karaokeData.id}", + !like.isSelected + ) + + karaokeData.likes_count?.let { + likeCount.text = "$it" + } + + karaokeData.is_liked?.let { + like.isSelected = it + } + + karaokeData.id?.let{onKaraokeChanged(it, true)} + } + + fav.isSelected = karaokeData.mark_as_favourite == true + + fav.setOnClickListener { + if (!context.isNetworkConnected()){ + context.toast(context.getString(R.string.no_internet)) + return@setOnClickListener + } + + KaraokeRepository.updateFavShow( + KaraokeData(karaokeData), + !fav.isSelected + ) + + fav.isSelected = karaokeData.mark_as_favourite == true + + karaokeData.id?.let{onKaraokeChanged(it, true)} + } + + root.setOnClickListener { + onKaraokeClicked(KaraokeData(karaokeData)) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/woka/karaoke/adapters/KaraokeAdapter.kt b/app/src/main/java/com/woka/karaoke/adapters/KaraokeAdapter.kt new file mode 100644 index 0000000..9b83b36 --- /dev/null +++ b/app/src/main/java/com/woka/karaoke/adapters/KaraokeAdapter.kt @@ -0,0 +1,133 @@ +package com.woka.karaoke.adapters + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.AsyncDifferConfig +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import com.woka.R +import com.woka.WokaApp.Companion.userPrefs +import com.woka.databinding.ShowViewHolderBinding +import com.woka.karaoke.KaraokeRepository +import com.woka.karaoke.models.listing.KaraokeData +import com.woka.utils.isNetworkConnected +import com.woka.utils.show +import com.woka.utils.toast +import java.util.concurrent.Executors + +class KaraokeAdapter( + private val context: Context, + private var onKaraokeClicked: (KaraokeData) -> Unit, + private var onKaraokeChanged: (id: Int, isContinue: Boolean) -> Unit +): ListAdapter(ASYNC_DIFF_UTIL) { + + inner class AudioBookViewHolder(val binding: ShowViewHolderBinding): ViewHolder(binding.root) + + companion object{ + val DIFF_UTIL = object : DiffUtil.ItemCallback(){ + override fun areItemsTheSame(oldItem: KaraokeData, newItem: KaraokeData): Boolean = oldItem.id == newItem.id + + override fun areContentsTheSame(oldItem: KaraokeData, newItem: KaraokeData): Boolean { + return oldItem.thumbnail_path == newItem.thumbnail_path && + oldItem.title == newItem.title && + oldItem.is_liked == newItem.is_liked && + oldItem.mark_as_favourite == newItem.mark_as_favourite && + oldItem.likes_count == newItem.likes_count + } + } + + val ASYNC_DIFF_UTIL = AsyncDifferConfig.Builder(DIFF_UTIL) + .setBackgroundThreadExecutor(Executors.newSingleThreadExecutor()) + .build() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AudioBookViewHolder { + return AudioBookViewHolder( + ShowViewHolderBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: AudioBookViewHolder, position: Int) { + val karaokeData = getItem(holder.absoluteAdapterPosition) + + holder.binding.apply { + karaokeData.thumbnail_path?.let { + image.loadImage(it) + } + + karaokeData.content_more_details?.let {moreDetailsList -> + title.text = if (moreDetailsList.isNotEmpty()){ + if (userPrefs?.appLanguage == "hi" && moreDetailsList.size > 1){ + moreDetailsList[1]?.title + }else{ + moreDetailsList[0]?.title + } + }else{ + karaokeData.title + } + } + + like.show() + likeCount.show() + fav.show() + + karaokeData.likes_count?.let { + likeCount.text = "$it" + } + + karaokeData.is_liked?.let { + like.isSelected = it + } + + like.setOnClickListener { + if (!context.isNetworkConnected()){ + context.toast(context.getString(R.string.no_internet)) + return@setOnClickListener + } + + KaraokeRepository.likeUnLikeSong( + "${karaokeData.id}", + !like.isSelected + ) + + karaokeData.likes_count?.let { + likeCount.text = "$it" + } + + karaokeData.is_liked?.let { + like.isSelected = it + } + + karaokeData.id?.let{onKaraokeChanged(it, false)} + } + + fav.isSelected = karaokeData.mark_as_favourite == true + + fav.setOnClickListener { + if (!context.isNetworkConnected()){ + context.toast(context.getString(R.string.no_internet)) + return@setOnClickListener + } + + KaraokeRepository.updateFavShow( + karaokeData, + !fav.isSelected + ) + + fav.isSelected = karaokeData.mark_as_favourite == true + + karaokeData.id?.let{onKaraokeChanged(it, false)} + } + + root.setOnClickListener { + onKaraokeClicked(karaokeData) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/woka/home/mylist/models/SingKaraokeData.kt b/app/src/main/java/com/woka/karaoke/models/continuesing/ContinueKaraokeData.kt similarity index 59% rename from app/src/main/java/com/woka/home/mylist/models/SingKaraokeData.kt rename to app/src/main/java/com/woka/karaoke/models/continuesing/ContinueKaraokeData.kt index 0eb649c..2f547c2 100644 --- a/app/src/main/java/com/woka/home/mylist/models/SingKaraokeData.kt +++ b/app/src/main/java/com/woka/karaoke/models/continuesing/ContinueKaraokeData.kt @@ -1,11 +1,12 @@ -package com.woka.home.mylist.models +package com.woka.karaoke.models.continuesing -data class SingKaraokeData( +import com.woka.karaoke.models.listing.ContentMoreDetail + +data class ContinueKaraokeData( val age_range_master_id: String?, - val bookmark_category_ids: String?, val bookmark_count: Int?, val category_master_id: String?, -// val content_more_details: List?, + val content_more_details: List?, val description: String?, val duration: String?, val gender_master_id: String?, @@ -13,10 +14,12 @@ data class SingKaraokeData( var is_liked: Boolean?, val language_master_id: Int?, var likes_count: Int?, - val mark_as_favourite: Boolean?, + var mark_as_favourite: Boolean?, val release_date: String?, + val show_data_count: Any?, val thumbnail_path: String?, val title: String?, + val user_video_view: List?, val video_url: String?, val views_count: Int? ) \ No newline at end of file diff --git a/app/src/main/java/com/woka/karaoke/models/continuesing/ContinueKaraokeResponse.kt b/app/src/main/java/com/woka/karaoke/models/continuesing/ContinueKaraokeResponse.kt new file mode 100644 index 0000000..16956a9 --- /dev/null +++ b/app/src/main/java/com/woka/karaoke/models/continuesing/ContinueKaraokeResponse.kt @@ -0,0 +1,6 @@ +package com.woka.karaoke.models.continuesing + +data class ContinueKaraokeResponse( + val result: List?, + val total_records: Int? +) \ No newline at end of file diff --git a/app/src/main/java/com/woka/karaoke/models/listing/KaraokeData.kt b/app/src/main/java/com/woka/karaoke/models/listing/KaraokeData.kt index 402dc49..8100e6c 100644 --- a/app/src/main/java/com/woka/karaoke/models/listing/KaraokeData.kt +++ b/app/src/main/java/com/woka/karaoke/models/listing/KaraokeData.kt @@ -1,24 +1,82 @@ package com.woka.karaoke.models.listing +import com.woka.home.mylist.models.FavKaraokeData +import com.woka.karaoke.models.continuesing.ContinueKaraokeData + data class KaraokeData( - val age_range_data: List?, val age_range_master_id: String?, + val bookmark_category_ids: String?, val bookmark_count: Int?, - val category_data: List?, val category_master_id: String?, val content_more_details: List?, val description: String?, val duration: String?, - val gender_data: List?, val gender_master_id: String?, val id: Int?, var is_liked: Boolean?, val language_master_id: Int?, var likes_count: Int?, - val mark_as_favourite: Boolean?, + var mark_as_favourite: Boolean?, val release_date: String?, + val show_data_count: Any?, val thumbnail_path: String?, val title: String?, + val user_video_view: List?, val video_url: String?, - val views_count: Int? -) \ No newline at end of file + val views_count: Int?, + val age_range_data: List?, + val category_data: List?, + val gender_data: List? +){ + constructor(favKaraoke: FavKaraokeData): this( + favKaraoke.age_range_master_id, + favKaraoke.bookmark_category_ids, + favKaraoke.bookmark_count, + favKaraoke.category_master_id, + favKaraoke.content_more_details, + favKaraoke.description, + favKaraoke.duration, + favKaraoke.gender_master_id, + favKaraoke.id, + favKaraoke.is_liked, + favKaraoke.language_master_id, + favKaraoke.likes_count, + favKaraoke.mark_as_favourite, + favKaraoke.release_date, + null, + favKaraoke.thumbnail_path, + favKaraoke.title, + null, + favKaraoke.video_url, + favKaraoke.views_count, + null, + null, + null + ) + + constructor(continueKaraoke: ContinueKaraokeData): this( + continueKaraoke.age_range_master_id, + null, + continueKaraoke.bookmark_count, + continueKaraoke.category_master_id, + continueKaraoke.content_more_details, + continueKaraoke.description, + continueKaraoke.duration, + continueKaraoke.gender_master_id, + continueKaraoke.id, + continueKaraoke.is_liked, + continueKaraoke.language_master_id, + continueKaraoke.likes_count, + continueKaraoke.mark_as_favourite, + continueKaraoke.release_date, + continueKaraoke.show_data_count, + continueKaraoke.thumbnail_path, + continueKaraoke.title, + continueKaraoke.user_video_view, + continueKaraoke.video_url, + continueKaraoke.views_count, + null, + null, + null + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/woka/karaoke/player/KaraokePlayerActivity.kt b/app/src/main/java/com/woka/karaoke/player/KaraokePlayerActivity.kt new file mode 100644 index 0000000..1357693 --- /dev/null +++ b/app/src/main/java/com/woka/karaoke/player/KaraokePlayerActivity.kt @@ -0,0 +1,84 @@ +package com.woka.karaoke.player + +import android.os.Bundle +import android.widget.ImageView +import android.widget.TextView +import androidx.activity.enableEdgeToEdge +import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.media3.common.MediaItem +import androidx.media3.exoplayer.ExoPlayer +import com.woka.R +import com.woka.databinding.ActivityKaraokePlayerrBinding +import com.woka.utils.WokaBaseActivity + +class KaraokePlayerActivity : WokaBaseActivity() { + + companion object { + const val EXTRA_KARAOKE_DATA = "extra_karaoke_data" + } + + private lateinit var binding: ActivityKaraokePlayerrBinding + + private var karaokePlayerData: KaraokePlayerData? = null + + private var player: ExoPlayer? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + binding = ActivityKaraokePlayerrBinding.inflate(layoutInflater) + setContentView(binding.root) + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) + insets + } + + val windowInsetsController = + WindowCompat.getInsetsController(window, window.decorView) + windowInsetsController.hide(WindowInsetsCompat.Type.systemBars()) + + karaokePlayerData = intent.getParcelableExtra(EXTRA_KARAOKE_DATA) + + player = ExoPlayer.Builder(this).build() + binding.playerView.player = player + + playVideo() + + initViews() + + clickEvents() + + } + + override fun onDestroy() { + super.onDestroy() + player?.stop() + player?.release() + } + + private fun initViews(){ + binding.apply { + playerView.findViewById(R.id.player_controller_title).text = karaokePlayerData?.title + } + } + + private fun clickEvents(){ + binding.apply { + playerView.findViewById(R.id.player_controller_close_btn).setOnClickListener { + onBackPressedDispatcher.onBackPressed() + } + } + } + + private fun playVideo() { + if (karaokePlayerData?.karaokeVideoUrl == null) return + + player?.setMediaItem(MediaItem.fromUri(karaokePlayerData?.karaokeVideoUrl!!)) + player?.prepare() + player?.play() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/woka/karaoke/player/KaraokePlayerData.kt b/app/src/main/java/com/woka/karaoke/player/KaraokePlayerData.kt new file mode 100644 index 0000000..523685c --- /dev/null +++ b/app/src/main/java/com/woka/karaoke/player/KaraokePlayerData.kt @@ -0,0 +1,10 @@ +package com.woka.karaoke.player + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class KaraokePlayerData( + val karaokeVideoUrl: String, + val title: String? +): Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/woka/karaoke/views/KaraokeActivity.kt b/app/src/main/java/com/woka/karaoke/views/KaraokeActivity.kt new file mode 100644 index 0000000..96ceb4f --- /dev/null +++ b/app/src/main/java/com/woka/karaoke/views/KaraokeActivity.kt @@ -0,0 +1,361 @@ +package com.woka.karaoke.views + +import android.app.Dialog +import android.content.Intent +import android.content.res.ColorStateList +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.InsetDrawable +import android.os.Bundle +import android.text.Html +import android.view.WindowManager +import androidx.activity.enableEdgeToEdge +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.recyclerview.widget.SimpleItemAnimator +import com.google.android.material.appbar.CollapsingToolbarLayout +import com.woka.R +import com.woka.WokaApp +import com.woka.databinding.ActivityKaraokeBinding +import com.woka.databinding.DialogModuleShowerBinding +import com.woka.karaoke.KaraokeRepository +import com.woka.karaoke.adapters.ContinueKaraokeAdapter +import com.woka.karaoke.adapters.KaraokeAdapter +import com.woka.karaoke.models.listing.KaraokeData +import com.woka.karaoke.player.KaraokePlayerActivity +import com.woka.karaoke.player.KaraokePlayerActivity.Companion.EXTRA_KARAOKE_DATA +import com.woka.karaoke.player.KaraokePlayerData +import com.woka.networking.ApiResult +import com.woka.utils.WokaBaseActivity +import com.woka.utils.hide +import com.woka.utils.show +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Locale + +class KaraokeActivity : WokaBaseActivity() { + + private lateinit var binding: ActivityKaraokeBinding + + // adapters + private lateinit var karaokeAdapter: KaraokeAdapter + private lateinit var continueKaraokeAdapter: ContinueKaraokeAdapter + + private lateinit var dialogBinding: DialogModuleShowerBinding + private lateinit var karaokeDialog: Dialog + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityKaraokeBinding.inflate(layoutInflater) + enableEdgeToEdge() + setContentView(binding.root) + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) + insets + } + + window.apply { + navigationBarColor = getColor(R.color.color_primary_dark) + } + + karaokeAdapter = KaraokeAdapter(this, ::onKaraokeClicked, ::onKaraokeChanged) + continueKaraokeAdapter = ContinueKaraokeAdapter(this, ::onKaraokeClicked, ::onKaraokeChanged) + + dialogBinding = DialogModuleShowerBinding.inflate(layoutInflater) + karaokeDialog = Dialog(this) + karaokeDialog.setContentView(dialogBinding.root) + + initViews() + + initKaraokeDialog() + + clickEvents() + + setObservers() + + KaraokeRepository.loadKaraokeSongs() + } + + private fun initViews() { + binding.apply { + adjustTrailerImage() + + toolbar.title.text = getString(R.string.karaoke) + + rvKaraoke.adapter = karaokeAdapter + (rvKaraoke.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false + + rvContinueSing.adapter = continueKaraokeAdapter + (rvContinueSing.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false + } + } + + private fun initKaraokeDialog() { + try { + val back = ColorDrawable(Color.TRANSPARENT) + val inset = InsetDrawable(back, 50) + karaokeDialog.window!!.setBackgroundDrawable(inset) + } catch (e: Exception) { + // do nothing + } + + try { + val layoutParams = karaokeDialog.window!!.attributes + layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT + layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT + karaokeDialog.window!!.setAttributes(layoutParams) + } catch (e: Exception) { + // do nothing + } + + dialogBinding.close.setOnClickListener { karaokeDialog.dismiss() } + + dialogBinding.watchCard.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_mic, 0, 0, 0) + dialogBinding.watchCard.backgroundTintList = ColorStateList.valueOf(getColor(R.color.game_grad_one)) + dialogBinding.watchCard.text = getString(R.string.sing_now) + } + + private fun clickEvents() { + binding.apply { + toolbar.backBtn.setOnClickListener { + onBackPressedDispatcher.onBackPressed() + } + + retryBtn.setOnClickListener { + KaraokeRepository.loadKaraokeSongs() + } + } + } + + private fun loadTrailerData(karaokeData: KaraokeData) { + binding.apply { + trailerView.show() + adjustTrailerImage() + + karaokeData.thumbnail_path?.let { + trailerImage.loadImage(it) + } + + karaokeData.content_more_details?.let { moreDetailsList -> + trailerName.text = if (moreDetailsList.isNotEmpty()) { + if (WokaApp.userPrefs?.appLanguage == "hi" && moreDetailsList.size > 1) { + moreDetailsList[1]?.title + } else { + moreDetailsList[0]?.title + } + } else { + karaokeData.title + } + } + + trailerBtn.setOnClickListener { + onSingClicked(karaokeData) + } + } + } + + private fun onSingClicked(karaokeData: KaraokeData){ + karaokeData.video_url?.let { + startActivity(Intent(this@KaraokeActivity, KaraokePlayerActivity::class.java).apply { + putExtra(EXTRA_KARAOKE_DATA, KaraokePlayerData(it, karaokeData.title)) + }) + } + } + + private fun adjustTrailerImage() { + // making space for Trailer image + binding.apply { + topPinnedView.post { + val imgParams = trailerImage.layoutParams as CollapsingToolbarLayout.LayoutParams + imgParams.setMargins(0, 0, 0, topPinnedView.height) + trailerImage.layoutParams = imgParams + } + } + } + + private fun showKaraokeDialog(karaokeData: KaraokeData) { + dialogBinding.apply { + karaokeData.content_more_details?.let { moreDetailsList -> + + karaokeData.thumbnail_path?.let { + image.loadImage(it) + } + + fav.isSelected = karaokeData.mark_as_favourite == true + like.isSelected = karaokeData.is_liked == true + likeCount.text = "${karaokeData.likes_count}" + + year.text = try { + karaokeData.release_date?.let { + val cal = Calendar.getInstance() + cal.time = + SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).parse( + karaokeData.release_date + )!! + "${cal.get(Calendar.YEAR)}" + } ?: throw Exception() + } catch (e: Exception) { + "${karaokeData.release_date}" + } + + if (moreDetailsList.isNotEmpty()) { + + if (WokaApp.userPrefs?.appLanguage == "hi" && moreDetailsList.size > 1) { + moreDetailsList[1]?.let { data -> + title.text = data.title + description.text = Html.fromHtml( + data.description?.replace( + "
", + " " + ), Html.FROM_HTML_MODE_LEGACY + ) + } + } else { + moreDetailsList[0]?.let { data -> + title.text = data.title + description.text = Html.fromHtml( + data.description?.replace( + "
", + " " + ), Html.FROM_HTML_MODE_LEGACY + ) + } + } + } else { + title.text = karaokeData.title + description.text = Html.fromHtml( + karaokeData.description?.replace( + "
", + " " + ), Html.FROM_HTML_MODE_LEGACY + ) + } + + watchCard.setOnClickListener { + onSingClicked(karaokeData) + } + + like.setOnClickListener { + KaraokeRepository.likeUnLikeSong( + "${karaokeData.id}", + !like.isSelected + ) + + karaokeData.id?.let { + onKaraokeChanged(it, false) + onKaraokeChanged(it, true) + } + like.isSelected = !like.isSelected + likeCount.text = "${karaokeData.likes_count}" + } + + fav.setOnClickListener { + KaraokeRepository.updateFavShow( + karaokeData, + !fav.isSelected + ) + + karaokeData.id?.let { + onKaraokeChanged(it, false) + onKaraokeChanged(it, true) + } + + fav.isSelected = !fav.isSelected + } + + close.setOnClickListener { + karaokeDialog.dismiss() + } + + karaokeDialog.show() + } + } + } + + private fun onKaraokeChanged(id: Int, isFromContinue: Boolean) { + if (isFromContinue){ + // updating karaoke list + val position = karaokeAdapter.currentList.indexOfFirst { it.id == id } + if (position >= 0 && position < karaokeAdapter.currentList.size) { + karaokeAdapter.notifyItemChanged(position) + } + }else{ + // updating continue karaoke list + val continuePos = continueKaraokeAdapter.currentList.indexOfFirst { it.id == id } + if (continuePos >= 0 && continuePos < continueKaraokeAdapter.currentList.size) { + continueKaraokeAdapter.notifyItemChanged(continuePos) + } + } + } + + private fun onKaraokeClicked(karaokeData: KaraokeData) { + loadTrailerData(karaokeData) + showKaraokeDialog(karaokeData) + } + + private fun setObservers() { + KaraokeRepository.karaokeLiveData.observe(this) { + when (it) { + is ApiResult.Error -> { + binding.shimmer.hide() + binding.rvKaraoke.hide() + binding.singTxt.hide() + binding.trailerView.hide() + + binding.errorView.show() + } + + is ApiResult.Loading -> { + binding.shimmer.show() + + binding.rvKaraoke.hide() + binding.singTxt.hide() + binding.trailerView.hide() + binding.errorView.hide() + } + + is ApiResult.Success -> { + it.data?.karaoke_data?.filterNotNull()?.let { audioBookData -> + if (audioBookData.isNotEmpty()) { + binding.shimmer.hide() + binding.errorView.hide() + + KaraokeRepository.loadContinueKaraoke() + + loadTrailerData(audioBookData[0]) + binding.rvKaraoke.show() + binding.singTxt.show() + + karaokeAdapter.submitList(audioBookData) + } + } + } + } + } + + KaraokeRepository.continueKaraokeLiveData.observe(this) { + when (it) { + is ApiResult.Error -> { + binding.continueSingTxt.hide() + binding.rvContinueSing.hide() + } + + is ApiResult.Loading -> {} + is ApiResult.Success -> { + it.data?.result?.filterNotNull()?.let {continueData -> + if (continueData.isNotEmpty()) { + binding.continueSingTxt.show() + binding.rvContinueSing.show() + + continueKaraokeAdapter.submitList(continueData) + } else { + binding.continueSingTxt.hide() + binding.rvContinueSing.hide() + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/woka/userPreference/UserPreference.kt b/app/src/main/java/com/woka/userPreference/UserPreference.kt index b31717a..fd714a9 100644 --- a/app/src/main/java/com/woka/userPreference/UserPreference.kt +++ b/app/src/main/java/com/woka/userPreference/UserPreference.kt @@ -11,6 +11,7 @@ import androidx.lifecycle.MutableLiveData import com.woka.audiobooks.AudioBookRepository import com.woka.home.models.Theme import com.woka.home.mylist.MyListRepository +import com.woka.karaoke.KaraokeRepository import com.woka.networking.ApiResult import com.woka.onboard.views.OnboardActivity import com.woka.userdata.UserRepository @@ -117,5 +118,6 @@ class UserPreference(val context: Context) { WebSeriesRepository.clearData() MyListRepository.clearData() AudioBookRepository.clearData() + KaraokeRepository.clearData() } } \ No newline at end of file diff --git a/app/src/main/java/com/woka/userdata/UserActionApiService.kt b/app/src/main/java/com/woka/userdata/UserActionApiService.kt index 0ff1fcf..f2674bf 100644 --- a/app/src/main/java/com/woka/userdata/UserActionApiService.kt +++ b/app/src/main/java/com/woka/userdata/UserActionApiService.kt @@ -1,6 +1,7 @@ package com.woka.userdata import com.woka.audiobooks.models.continuedata.ContinueAudioResponse +import com.woka.karaoke.models.continuesing.ContinueKaraokeResponse import com.woka.networking.ApiResponse import com.woka.webseries.models.ContinueEpisodeResponse import okhttp3.FormBody @@ -27,4 +28,7 @@ interface UserActionApiService { @POST("continue_watching") suspend fun continueAudioBookListing(@Body body: FormBody): Response> + + @POST("continue_watching") + suspend fun continueKaraokeListing(@Body body: FormBody): Response> } \ No newline at end of file diff --git a/app/src/main/res/drawable/gradient_karaoke.xml b/app/src/main/res/drawable/gradient_karaoke.xml new file mode 100644 index 0000000..ee489cf --- /dev/null +++ b/app/src/main/res/drawable/gradient_karaoke.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_mic.xml b/app/src/main/res/drawable/ic_mic.xml new file mode 100644 index 0000000..bd90ce0 --- /dev/null +++ b/app/src/main/res/drawable/ic_mic.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/layout/activity_karaoke.xml b/app/src/main/res/layout/activity_karaoke.xml new file mode 100644 index 0000000..1f8ff9f --- /dev/null +++ b/app/src/main/res/layout/activity_karaoke.xml @@ -0,0 +1,283 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +