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(), 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 = 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 { 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(R.id.dialog_ok_btn) val cancelBtn = dialogView.findViewById(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(R.id.newPlayListBtn) val rv = view.findViewById(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(R.id.playlistEt) val confirmBtn = view.findViewById(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() } }