Musicoo/app/src/main/java/melody/offline/music/fragment/MoHomeFragment.kt
2024-07-22 16:00:06 +08:00

644 lines
29 KiB
Kotlin

package melody.offline.music.fragment
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.Download
import androidx.media3.exoplayer.offline.DownloadRequest
import androidx.media3.exoplayer.offline.DownloadService
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.gyf.immersionbar.ktx.immersionBar
import kotlinx.coroutines.Dispatchers
import melody.offline.music.databinding.FragmentMoHomeBinding
import melody.offline.music.innertube.Innertube
import melody.offline.music.innertube.models.MusicCarouselShelfRenderer
import melody.offline.music.innertube.requests.homePage
import melody.offline.music.innertube.requests.homePageMore
import melody.offline.music.util.LogTag.LogD
import melody.offline.music.view.MusicResponsiveListView
import melody.offline.music.view.MusicTowRowListView
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.withContext
import melody.offline.music.App
import melody.offline.music.R
import melody.offline.music.activity.MoListDetailsActivity
import melody.offline.music.adapter.NewPlayListAdapter
import melody.offline.music.ads.AdPlacement
import melody.offline.music.ads.AnalysisAdState
import melody.offline.music.ads.LolAdWrapper
import melody.offline.music.bean.FavoriteBean
import melody.offline.music.bean.OfflineBean
import melody.offline.music.bean.Playlist
import melody.offline.music.bean.PlaylistItem
import melody.offline.music.http.MyHttpUtil
import melody.offline.music.service.MyDownloadService
import melody.offline.music.util.AnalysisUtil
import melody.offline.music.util.DownloadUtil
import melody.offline.music.util.FileSizeConverter
import melody.offline.music.util.asPlaylistItem
import melody.offline.music.view.ListMoreBottomSheetDialog
import org.json.JSONObject
@OptIn(UnstableApi::class)
class MoHomeFragment : MoBaseFragment<FragmentMoHomeBinding>(),
MusicResponsiveListView.OnMoreClickListener, ListMoreBottomSheetDialog.ListMoreViewListener,
ListMoreBottomSheetDialog.UpdateAdapterListener {
interface MoHomeFragmentToSearchClickListener {
fun onToSearchClick()
}
fun setToSearchClickListener(listener: MoHomeFragmentToSearchClickListener) {
this.toSearchClickListener = listener
}
private var toSearchClickListener: MoHomeFragmentToSearchClickListener? = null
private var moreDialog: ListMoreBottomSheetDialog? = null
private val requests: Channel<Request> = Channel(Channel.UNLIMITED)
sealed class Request {
data object TryAgain : Request()
data class ShowDialog(val bean: MusicCarouselShelfRenderer.Content) : Request()
data class UpdateFavorite(val bean: PlaylistItem) : Request()
data class OnFavorites(val bean: PlaylistItem) : Request()
data class OnDownload(val bean: PlaylistItem) : Request()
data class OnDownloadRemove(val bean: PlaylistItem) : Request()
data class OnUpdateDownloadUi(val bean: PlaylistItem) : Request()
data class OnAddPlaylist(val bean: PlaylistItem) : Request()
}
override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentMoHomeBinding
get() = FragmentMoHomeBinding::inflate
override suspend fun onViewCreated() {
initView()
initData()
onReceive()
}
private fun initImmersionBar() {
immersionBar {
statusBarDarkFont(false)
statusBarView(binding.view)
}
}
private suspend fun onReceive() {
while (isActive) {
select<Unit> {
requests.onReceive {
when (it) {
Request.TryAgain -> {
initData()
}
is Request.ShowDialog -> {
moreDialog = ListMoreBottomSheetDialog(
requireActivity(),
initMoreDialogData(it.bean),
requireActivity(),
this@MoHomeFragment
)
moreDialog?.setListMoreViewListener(this@MoHomeFragment)
moreDialog?.show()
}
is Request.UpdateFavorite -> {
val currentFavoriteBean =
App.appFavoriteDBManager.getFavoriteBeanByID(it.bean.videoId)
if (currentFavoriteBean != null) {
updateFavoriteUi(currentFavoriteBean.isFavorite)
} else {
updateFavoriteUi(false)
}
}
is Request.OnFavorites -> {
val jsonObject = JSONObject()
jsonObject.put(
"song_title", it.bean.title
)
val songMap = mutableMapOf(
Pair(
AnalysisUtil.PARAM_VALUE, jsonObject.toString()
)
)
val currentFavoriteBean =
App.appFavoriteDBManager.getFavoriteBeanByID(it.bean.videoId)
if (currentFavoriteBean != null) {
currentFavoriteBean.isFavorite = !currentFavoriteBean.isFavorite
App.appFavoriteDBManager.updateFavoriteBean(currentFavoriteBean)
if (currentFavoriteBean.isFavorite) {
AnalysisUtil.logEvent(AnalysisUtil.PLAYER_B_LOVE_CLICK, songMap)
} else {
AnalysisUtil.logEvent(
AnalysisUtil.PLAYER_B_UN_LOVE_CLICK, songMap
)
}
} else {
val b = FavoriteBean(
videoId = it.bean.videoId,
title = it.bean.title,
name = it.bean.name,
thumbnail = it.bean.thumbnail,
isFavorite = true
)
App.appFavoriteDBManager.insertFavoriteBean(b)
AnalysisUtil.logEvent(AnalysisUtil.PLAYER_B_LOVE_CLICK, songMap)
}
requests.trySend(Request.UpdateFavorite(it.bean))
}
is Request.OnDownload -> {
val id = it.bean.videoId
val offBean =
App.appOfflineDBManager.getOfflineBeanByID(id)//得到当前ID的本地数据
if (offBean != null && offBean.bytesDownloaded?.let { bytes -> bytes > 0 } == true) {//判断当前数据库是否有这条数据。
showRemoveDownloadDialogHint(it.bean)
} else {
val isFavorite =
App.appFavoriteDBManager.getFavoriteBeanByID(it.bean.videoId)
//判断是否已经下载了这条数据,已经下载,就直接进行数据库数据存储,反之走下载流程。
if (DownloadUtil.downloadResourceExist(id)) {
val favoriteBean = FavoriteBean(
id,
it.bean.title,
it.bean.name,
it.bean.thumbnail,
isFavorite?.isFavorite ?: false
)
insertOfflineData(favoriteBean)
it.bean.isOffline = true//更改状态
requests.trySend(Request.OnUpdateDownloadUi(it.bean))
} else {
val downloadRequest = DownloadRequest.Builder(id, id.toUri())
.setCustomCacheKey(id).build()
val downloadCount = DownloadUtil.getCurrentDownloads()
if (downloadCount >= 3) {
Toast.makeText(
requireActivity(),
getString(R.string.download_tips),
Toast.LENGTH_LONG
).show()
} else {
DownloadService.sendAddDownload(
requireActivity(),
MyDownloadService::class.java,
downloadRequest,
false
)
LolAdWrapper.shared.showAdTiming(
requireActivity(), AdPlacement.INST_DOWNLOAD
)
val favoriteBean = FavoriteBean(
id,
it.bean.title,
it.bean.name,
it.bean.thumbnail,
isFavorite?.isFavorite ?: false
)
insertOfflineData(favoriteBean)
val jsonObject = JSONObject()
jsonObject.put(
"download_id", favoriteBean.videoId
)
val songMap = mutableMapOf(
Pair(
AnalysisUtil.PARAM_VALUE, jsonObject.toString()
)
)
AnalysisUtil.logEvent(
AnalysisUtil.PLAYER_B_DOWNLOAD_CLICK, songMap
)
LolAdWrapper.shared.loadAdIfNotCached(
requireActivity(), AdPlacement.INST_DOWNLOAD
)
}
}
}
}
is Request.OnDownloadRemove -> {
val currentOfflineBean =
App.appOfflineDBManager.getOfflineBeanByID(it.bean.videoId)
if (currentOfflineBean != null) {
App.appOfflineDBManager.deleteOfflineBean(currentOfflineBean)
it.bean.isOffline = false
}
requests.trySend(Request.OnUpdateDownloadUi(it.bean))
}
is Request.OnUpdateDownloadUi -> {
moreDialog?.updateDownloadBtnUi(it.bean.isOffline)//更新对话框的ui
}
is Request.OnAddPlaylist -> {
val isFavorite =
App.appFavoriteDBManager.getFavoriteBeanByID(it.bean.videoId) != null
showAddPlaylistBottomDialog(
FavoriteBean(
videoId = it.bean.videoId,
title = it.bean.title,
name = it.bean.name,
thumbnail = it.bean.thumbnail,
isFavorite
)
)
}
}
}
events.onReceive {
when (it) {
Event.FragmentOnResume -> {
fragmentOnResume()
}
}
}
}
}
}
private fun initView() {
binding.tryAgainBtn.setOnClickListener {
AnalysisUtil.logEvent(AnalysisUtil.HOME_B_MODULE_TRY_AGAIN_ACTION)
requests.trySend(Request.TryAgain)
}
binding.topSearchBtn.setOnClickListener {
if (toSearchClickListener != null) {
toSearchClickListener?.onToSearchClick()
}
}
}
private suspend fun initData() {
showLoadingUi()
Innertube.homePage(appStore.myVisitorData)?.onSuccess {
showDataUi()
if (it.homePage.isNotEmpty()) {
AnalysisUtil.logEvent(AnalysisUtil.HOME_B_MODULE_SHOW_SUCCESS_ACTION)
for (home: Innertube.HomePage in it.homePage) {
for (content: MusicCarouselShelfRenderer.Content in home.contents) {
if (content.musicResponsiveListItemRenderer != null) {
val musicResponsiveListView =
MusicResponsiveListView(requireActivity(), home)
musicResponsiveListView.setOnItemMoreClickListener(this)
binding.contentLayout.addView(musicResponsiveListView)
break
}
if (content.musicTwoRowItemRenderer != null) {
binding.contentLayout.addView(
MusicTowRowListView(
requireActivity(), home
)
)
break
}
}
}
initHomeDataMore(it)
} else {
LogD(TAG, "homePage size 0")
showNoContentUi("homePage size 0")
}
}?.onFailure {
LogD(TAG, "homePage onFailure->${it}")
showNoContentUi(it.message.toString())
}
}
private suspend fun initHomeDataMore(baseHomePage: Innertube.BaseHomePage) {
if (baseHomePage.cToken?.isNotEmpty() == true) {
Innertube.homePageMore(baseHomePage)?.onSuccess {
for (home: Innertube.HomePage in it.homePage) {
for (content: MusicCarouselShelfRenderer.Content in home.contents) {
if (content.musicResponsiveListItemRenderer != null) {
val musicResponsiveListView =
MusicResponsiveListView(requireActivity(), home)
musicResponsiveListView.setOnItemMoreClickListener(this)
binding.contentLayout.addView(musicResponsiveListView)
break
}
if (content.musicTwoRowItemRenderer != null) {
binding.contentLayout.addView(
MusicTowRowListView(
requireActivity(), home
)
)
break
}
}
}
initHomeDataMore(it)
}?.onFailure {
LogD(TAG, "initHomeDataMore onFailure ->${it}")
}
}
}
private fun fragmentOnResume() {
refreshAdapters()
}
private fun refreshAdapters() {//刷新home的单曲ui
for (i in 0 until binding.contentLayout.childCount) {
val child = binding.contentLayout.getChildAt(i)
if (child is MusicResponsiveListView) {
child.updateAdapter()
}
}
}
override fun onResume() {
super.onResume()
initImmersionBar()
AnalysisUtil.logEvent(AnalysisUtil.HOME_B_PV)
}
override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden)
if (!hidden) {
initImmersionBar()
}
}
override fun onDestroy() {
super.onDestroy()
}
private fun showDataUi() {
binding.loadingLayout.visibility = View.GONE
binding.noContentLayout.visibility = View.GONE
}
private fun showLoadingUi() {
binding.loadingLayout.visibility = View.VISIBLE
binding.noContentLayout.visibility = View.GONE
}
private fun showNoContentUi(string: String) {
val networkType = MyHttpUtil.mInstance.getNetworkType(requireActivity())
val isVpnConnected = MyHttpUtil.mInstance.isVpnConnected(requireActivity())
val jsonObject = JSONObject()
jsonObject.put("fail", string)
jsonObject.put("networkType", networkType)
jsonObject.put("isVpnConnected", isVpnConnected)
val map = mutableMapOf(Pair(AnalysisUtil.PARAM_VALUE, jsonObject.toString()))
AnalysisUtil.logEvent(AnalysisUtil.HOME_B_MODULE_SHOW_FAIL_ACTION, map)
binding.loadingLayout.visibility = View.GONE
binding.noContentLayout.visibility = View.VISIBLE
}
override fun onMoreClick(bean: MusicCarouselShelfRenderer.Content) {
requests.trySend(Request.ShowDialog(bean))
}
private suspend fun initMoreDialogData(bean: MusicCarouselShelfRenderer.Content): PlaylistItem {
val watchEndpoint =
bean.musicResponsiveListItemRenderer?.flexColumns?.firstOrNull()?.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.firstOrNull()?.navigationEndpoint?.watchEndpoint
val thumbnailUrl =
bean.musicResponsiveListItemRenderer?.thumbnail?.musicThumbnailRenderer?.thumbnail?.thumbnails?.let {
it.getOrNull(1) ?: it.getOrNull(0)
}?.url
val title =
bean.musicResponsiveListItemRenderer?.flexColumns?.get(0)?.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.firstOrNull()?.text
val name =
bean.musicResponsiveListItemRenderer?.flexColumns?.get(1)?.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.firstOrNull()?.text
val videoId = watchEndpoint?.videoId
LogD(TAG, "title->$title videoId->$videoId")
val offlineBean = App.appOfflineDBManager.getOfflineBeanByID(videoId ?: "")
val favoriteBean = App.appFavoriteDBManager.getFavoriteBeanByID(videoId ?: "")
return PlaylistItem(
videoId = videoId ?: "",
title = title ?: "",
name = name ?: "",
thumbnail = thumbnailUrl,
bytesDownloaded = offlineBean?.bytesDownloaded ?: 0L,
size = offlineBean?.size,
isOffline = offlineBean?.isOffline ?: false,
isFavorite = favoriteBean?.isFavorite ?: false
)
}
override fun onUpdateAdapterListener(download: Download, playlistItem: PlaylistItem) {
}
override fun onFavoritesClicked(playlistItem: PlaylistItem) {
requests.trySend(Request.OnFavorites(playlistItem))
}
override fun onDownloadClicked(playlistItem: PlaylistItem) {
requests.trySend(Request.OnDownload(playlistItem))
}
override fun onAddToPlaylistClicked(playlistItem: PlaylistItem) {
requests.trySend(Request.OnAddPlaylist(playlistItem))
}
private fun updateFavoriteUi(b: Boolean) {
if (moreDialog != null) {
moreDialog?.updateFavoriteUi(b)
}
}
private fun showRemoveDownloadDialogHint(playlistItem: PlaylistItem) {
val inflater = LayoutInflater.from(requireActivity())
val dialogView = inflater.inflate(R.layout.dialog_hint, null)
val okBtn = dialogView.findViewById<TextView>(R.id.dialog_ok_btn)
val cancelBtn = dialogView.findViewById<TextView>(R.id.dialog_cancel_btn)
val dialogBuilder = AlertDialog.Builder(requireActivity()).setView(dialogView)
val dialog = dialogBuilder.create()
dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
dialog.show()
okBtn.setOnClickListener {
dialog.dismiss()
requests.trySend(Request.OnDownloadRemove(playlistItem))
}
cancelBtn.setOnClickListener {
dialog.dismiss()
}
}
private suspend fun insertOfflineData(favoriteBean: FavoriteBean) {
val currentDownload = DownloadUtil.getCurrentIdDownload(favoriteBean.videoId)
if (currentDownload != null) {
val bytesDownloaded = currentDownload.bytesDownloaded
val size = FileSizeConverter(currentDownload.bytesDownloaded).formattedSize()
val bean = OfflineBean(
videoId = favoriteBean.videoId,
title = favoriteBean.title,
name = favoriteBean.name,
thumbnail = favoriteBean.thumbnail,
isOffline = true,
isFavorite = favoriteBean.isFavorite,
bytesDownloaded = bytesDownloaded,
size = size
)
App.appOfflineDBManager.insertOfflineBean(bean)
} else {
val bean = OfflineBean(
videoId = favoriteBean.videoId,
title = favoriteBean.title,
name = favoriteBean.name,
thumbnail = favoriteBean.thumbnail,
isOffline = true,
isFavorite = favoriteBean.isFavorite,
)
App.appOfflineDBManager.insertOfflineBean(bean)
}
}
suspend fun showAddPlaylistBottomDialog(favoriteBean: FavoriteBean) {
val bottomAddPlaylistSheetDialog = BottomSheetDialog(requireActivity())
val view = layoutInflater.inflate(R.layout.add_playlist_layout, null)
bottomAddPlaylistSheetDialog.setContentView(view)
val newPlayListBtn = view.findViewById<LinearLayout>(R.id.newPlayListBtn)
val rv = view.findViewById<RecyclerView>(R.id.newPlayListRv)
newPlayListBtn.setOnClickListener {
bottomAddPlaylistSheetDialog.dismiss()
showNewPlaylistBottomDialog(favoriteBean)
}
// 设置对话框背景为透明以显示圆角
bottomAddPlaylistSheetDialog.window?.setBackgroundDrawableResource(android.R.color.transparent)
bottomAddPlaylistSheetDialog.window?.navigationBarColor =
ContextCompat.getColor(requireActivity(), R.color.main_bg_color)
bottomAddPlaylistSheetDialog.show()
val playlist = (App.appPlaylistDBManager.getAllPlaylists())
val adapter = NewPlayListAdapter(requireActivity(), playlist)
adapter.setOnItemClickListener(object : NewPlayListAdapter.OnItemClickListener {
override fun onItemClick(position: Int) {
launch {
val playlistItem =
App.appPlaylistDBManager.getPlaylistItems(playlist[position].id)
val isAny = playlistItem.any { it.title == favoriteBean.title }
if (isAny) {//如何这首歌曲已经存在歌单则不添加
withContext(Dispatchers.Main) {
Toast.makeText(
requireActivity(),
getString(R.string.song_exists_playlist_hint),
Toast.LENGTH_LONG
).show()
}
} else {
val isOffline =
App.appOfflineDBManager.getOfflineBeanByID(favoriteBean.videoId) != null
val isFavorite =
App.appFavoriteDBManager.getFavoriteBeanByID(favoriteBean.videoId) != null
App.appPlaylistDBManager.insertOrUpdatePlaylistItem(
PlaylistItem(
playlistId = playlist[position].id,
videoId = favoriteBean.videoId,
title = favoriteBean.title,
name = favoriteBean.name,
thumbnail = favoriteBean.thumbnail,
isOffline = isOffline,
isFavorite = isFavorite
)
)
withContext(Dispatchers.Main) {
bottomAddPlaylistSheetDialog.dismiss()
Toast.makeText(
requireActivity(),
getString(R.string.added_playlist_success_Hint),
Toast.LENGTH_LONG
).show()
}
}
}
}
})
rv.layoutManager =
LinearLayoutManager(requireActivity(), LinearLayoutManager.VERTICAL, false)
rv.adapter = adapter
}
private var bottomSheetDialog: BottomSheetDialog? = null
private fun showNewPlaylistBottomDialog(favoriteBean: FavoriteBean) {
bottomSheetDialog = BottomSheetDialog(requireActivity())
val view = layoutInflater.inflate(R.layout.new_playlist_layout, null)
bottomSheetDialog?.setContentView(view)
val edit = view.findViewById<EditText>(R.id.playlistEt)
val confirmBtn = view.findViewById<TextView>(R.id.confirmBtn)
confirmBtn.setOnClickListener {
val text = edit.text.toString().trim()
if (text.isNotEmpty()) {
launch {
val playlist = App.appPlaylistDBManager.getPlaylistByTitle(text)
if (playlist != null) {
withContext(Dispatchers.Main) {
Toast.makeText(
requireActivity(),
getString(R.string.new_playlist_duplicate_name_hint),
Toast.LENGTH_LONG
).show()
}
} else {
val newPlaylist = Playlist(title = text)
App.appPlaylistDBManager.insertOrUpdatePlaylist(newPlaylist)
withContext(Dispatchers.Main) {
if (bottomSheetDialog != null) {
bottomSheetDialog?.dismiss()
}
Toast.makeText(
requireActivity(),
getString(R.string.created_successfully),
Toast.LENGTH_LONG
).show()
}
val currentPlaylist = App.appPlaylistDBManager.getPlaylistByTitle(text)
if (currentPlaylist != null) {
val isOffline =
App.appOfflineDBManager.getOfflineBeanByID(favoriteBean.videoId) != null//返回非null则为true
val isFavorite =
App.appFavoriteDBManager.getFavoriteBeanByID(favoriteBean.videoId) != null
val playlistItem = PlaylistItem(
playlistId = currentPlaylist.id,
videoId = favoriteBean.videoId,
title = favoriteBean.title,
name = favoriteBean.name,
thumbnail = favoriteBean.thumbnail,
isOffline = isOffline,
isFavorite = isFavorite
)
App.appPlaylistDBManager.insertOrUpdatePlaylistItem(playlistItem)
}
}
}
}
}
// 设置对话框背景为透明以显示圆角
bottomSheetDialog?.window?.setBackgroundDrawableResource(android.R.color.transparent)
bottomSheetDialog?.window?.navigationBarColor =
ContextCompat.getColor(requireActivity(), R.color.main_bg_color)
bottomSheetDialog?.show()
}
}