add playlist

This commit is contained in:
ocean 2024-06-26 10:05:52 +08:00
parent 641e789e2c
commit 87f3fdd933
20 changed files with 858 additions and 13 deletions

View File

@ -24,6 +24,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import melody.offline.music.database.AppFavoriteDBManager import melody.offline.music.database.AppFavoriteDBManager
import melody.offline.music.database.AppPlaylistDBManager
import melody.offline.music.firebase.RemoteConfig import melody.offline.music.firebase.RemoteConfig
import melody.offline.music.http.CommonIpInfoUtil import melody.offline.music.http.CommonIpInfoUtil
import melody.offline.music.http.UploadEventName import melody.offline.music.http.UploadEventName
@ -40,6 +41,8 @@ class App : Application() {
companion object { companion object {
lateinit var app: App lateinit var app: App
private set private set
lateinit var appPlaylistDBManager: AppPlaylistDBManager
private set
lateinit var appFavoriteDBManager: AppFavoriteDBManager lateinit var appFavoriteDBManager: AppFavoriteDBManager
private set private set
lateinit var appOfflineDBManager: AppOfflineDBManager lateinit var appOfflineDBManager: AppOfflineDBManager
@ -137,6 +140,7 @@ class App : Application() {
appFavoriteDBManager = AppFavoriteDBManager.getInstance(this) appFavoriteDBManager = AppFavoriteDBManager.getInstance(this)
currentAudioManager = CurrentAudioManager.getInstance(this) currentAudioManager = CurrentAudioManager.getInstance(this)
databaseManager = DatabaseManager.getInstance(this) databaseManager = DatabaseManager.getInstance(this)
appPlaylistDBManager = AppPlaylistDBManager.getInstance(this)
initCurrentPlayingAudio() initCurrentPlayingAudio()
initImportAudio() initImportAudio()
CacheManager.initializeCaches(this) CacheManager.initializeCaches(this)

View File

@ -11,35 +11,47 @@ import android.renderscript.RenderScript
import android.renderscript.ScriptIntrinsicBlur import android.renderscript.ScriptIntrinsicBlur
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.EditText
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.RelativeLayout import android.widget.RelativeLayout
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetDialog
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.json.JSONObject
import melody.offline.music.App import melody.offline.music.App
import melody.offline.music.R import melody.offline.music.R
import melody.offline.music.adapter.NewPlayListAdapter
import melody.offline.music.bean.FavoriteBean import melody.offline.music.bean.FavoriteBean
import melody.offline.music.bean.OfflineBean import melody.offline.music.bean.OfflineBean
import melody.offline.music.bean.Playlist
import melody.offline.music.bean.PlaylistItem
import melody.offline.music.fragment.MoMeFragment
import melody.offline.music.http.getAppVersionCode import melody.offline.music.http.getAppVersionCode
import melody.offline.music.http.getCountryCode
import melody.offline.music.media.MediaControllerManager import melody.offline.music.media.MediaControllerManager
import melody.offline.music.sp.AppStore import melody.offline.music.sp.AppStore
import melody.offline.music.util.LogTag
import melody.offline.music.http.getCountryCode
import melody.offline.music.util.DownloadUtil import melody.offline.music.util.DownloadUtil
import melody.offline.music.util.FileSizeConverter import melody.offline.music.util.FileSizeConverter
import melody.offline.music.util.LogTag
import melody.offline.music.view.MusicPlayerView import melody.offline.music.view.MusicPlayerView
import org.json.JSONObject
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
abstract class MoBaseActivity : AppCompatActivity(), CoroutineScope by MainScope(), LifecycleOwner { abstract class MoBaseActivity : AppCompatActivity(), CoroutineScope by MainScope(), LifecycleOwner {
@ -57,6 +69,7 @@ abstract class MoBaseActivity : AppCompatActivity(), CoroutineScope by MainScope
private var defer: suspend () -> Unit = {} private var defer: suspend () -> Unit = {}
private var deferRunning = false private var deferRunning = false
private lateinit var musicPlayerView: MusicPlayerView private lateinit var musicPlayerView: MusicPlayerView
private lateinit var bottomAddPlaylistSheetDialog: BottomSheetDialog
fun defer(operation: suspend () -> Unit) { fun defer(operation: suspend () -> Unit) {
this.defer = operation this.defer = operation
@ -350,4 +363,123 @@ abstract class MoBaseActivity : AppCompatActivity(), CoroutineScope by MainScope
LogTag.LogD(TAG, "withPermission currentCountryCode->${currentCountryCode}") LogTag.LogD(TAG, "withPermission currentCountryCode->${currentCountryCode}")
return currentCountryCode !in restrictedCountryCodes return currentCountryCode !in restrictedCountryCodes
} }
suspend fun showAddPlaylistBottomDialog(favoriteBean: FavoriteBean) {
bottomAddPlaylistSheetDialog = BottomSheetDialog(this)
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(this, R.color.main_bg_color)
bottomAddPlaylistSheetDialog.show()
val playlist = (App.appPlaylistDBManager.getAllPlaylists())
val adapter = NewPlayListAdapter(this, 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(
this@MoBaseActivity,
getString(R.string.song_exists_playlist_hint),
Toast.LENGTH_LONG
).show()
}
} else {
App.appPlaylistDBManager.insertOrUpdatePlaylistItem(
PlaylistItem(
playlistId = playlist[position].id,
videoId = favoriteBean.videoId,
title = favoriteBean.title,
name = favoriteBean.name,
thumbnail = favoriteBean.thumbnail,
isOffline = false,
isFavorite = favoriteBean.isFavorite
)
)
withContext(Dispatchers.Main) {
bottomAddPlaylistSheetDialog.dismiss()
Toast.makeText(
this@MoBaseActivity,
getString(R.string.added_playlist_success_Hint),
Toast.LENGTH_LONG
).show()
}
}
}
}
})
rv.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
rv.adapter = adapter
}
private var bottomSheetDialog: BottomSheetDialog? = null
private fun showNewPlaylistBottomDialog(favoriteBean: FavoriteBean) {
bottomSheetDialog = BottomSheetDialog(this)
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(
this@MoBaseActivity,
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(
this@MoBaseActivity,
getString(R.string.created_successfully),
Toast.LENGTH_LONG
).show()
}
val currentPlaylist = App.appPlaylistDBManager.getPlaylistByTitle(text)
if (currentPlaylist != null) {
val playlistItem = PlaylistItem(
playlistId = currentPlaylist.id,
videoId = favoriteBean.videoId,
title = favoriteBean.title,
name = favoriteBean.name,
thumbnail = favoriteBean.thumbnail,
isOffline = false,
isFavorite = favoriteBean.isFavorite
)
App.appPlaylistDBManager.insertOrUpdatePlaylistItem(playlistItem)
}
}
}
}
}
// 设置对话框背景为透明以显示圆角
bottomSheetDialog?.window?.setBackgroundDrawableResource(android.R.color.transparent)
bottomSheetDialog?.window?.navigationBarColor =
ContextCompat.getColor(this, R.color.main_bg_color)
bottomSheetDialog?.show()
}
} }

View File

@ -6,10 +6,12 @@ import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.animation.AnimationUtils import android.view.animation.AnimationUtils
import android.widget.EditText
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.Download import androidx.media3.exoplayer.offline.Download
@ -17,6 +19,7 @@ import androidx.media3.exoplayer.offline.DownloadRequest
import androidx.media3.exoplayer.offline.DownloadService import androidx.media3.exoplayer.offline.DownloadService
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.gyf.immersionbar.ktx.immersionBar import com.gyf.immersionbar.ktx.immersionBar
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
@ -28,6 +31,7 @@ import melody.offline.music.ads.AdPlacement
import melody.offline.music.ads.LolAdWrapper import melody.offline.music.ads.LolAdWrapper
import melody.offline.music.bean.FavoriteBean import melody.offline.music.bean.FavoriteBean
import melody.offline.music.databinding.ActivityLikedSongsBinding import melody.offline.music.databinding.ActivityLikedSongsBinding
import melody.offline.music.fragment.MoMeFragment
import melody.offline.music.service.MyDownloadService import melody.offline.music.service.MyDownloadService
import melody.offline.music.service.ViewModelMain import melody.offline.music.service.ViewModelMain
import melody.offline.music.util.AnalysisUtil import melody.offline.music.util.AnalysisUtil
@ -48,6 +52,7 @@ class MoLikedSongsActivity : MoBaseActivity(), LikedSongsAdapter.OnItemFavorites
data class OnDownload(val favoriteBean: FavoriteBean) : Request() data class OnDownload(val favoriteBean: FavoriteBean) : Request()
data class OnDownloadRemove(val id: String) : Request() data class OnDownloadRemove(val id: String) : Request()
data class OnUpdateDownloadUi(val id: String) : Request() data class OnUpdateDownloadUi(val id: String) : Request()
data class OnAddPlaylist(val bean: FavoriteBean) : Request()
} }
private lateinit var binding: ActivityLikedSongsBinding private lateinit var binding: ActivityLikedSongsBinding
@ -110,7 +115,7 @@ class MoLikedSongsActivity : MoBaseActivity(), LikedSongsAdapter.OnItemFavorites
is Request.OnDownload -> { is Request.OnDownload -> {
val id = it.favoriteBean.videoId val id = it.favoriteBean.videoId
LogTag.LogD(TAG,"OnDownload id->${id}") LogTag.LogD(TAG, "OnDownload id->${id}")
val currentOfflineBean = App.appOfflineDBManager.getOfflineBeanByID(id) val currentOfflineBean = App.appOfflineDBManager.getOfflineBeanByID(id)
if (currentOfflineBean != null) {//判断当前数据库是否有这条数据。 if (currentOfflineBean != null) {//判断当前数据库是否有这条数据。
if (currentPosition >= 0) { if (currentPosition >= 0) {
@ -169,7 +174,7 @@ class MoLikedSongsActivity : MoBaseActivity(), LikedSongsAdapter.OnItemFavorites
} }
is Request.OnDownloadRemove -> { is Request.OnDownloadRemove -> {
LogTag.LogD(TAG,"OnDownloadRemove id->${it.id}") LogTag.LogD(TAG, "OnDownloadRemove id->${it.id}")
val currentOfflineBean = val currentOfflineBean =
App.appOfflineDBManager.getOfflineBeanByID(it.id) App.appOfflineDBManager.getOfflineBeanByID(it.id)
if (currentOfflineBean != null) { if (currentOfflineBean != null) {
@ -189,6 +194,10 @@ class MoLikedSongsActivity : MoBaseActivity(), LikedSongsAdapter.OnItemFavorites
binding.downloadTv.text = getString(R.string.download_save_offline) binding.downloadTv.text = getString(R.string.download_save_offline)
} }
} }
is Request.OnAddPlaylist -> {
showAddPlaylistBottomDialog(it.bean)
}
} }
} }
events.onReceive { events.onReceive {
@ -251,6 +260,12 @@ class MoLikedSongsActivity : MoBaseActivity(), LikedSongsAdapter.OnItemFavorites
requests.trySend(Request.OnDownload(favoriteBeans[currentPosition])) requests.trySend(Request.OnDownload(favoriteBeans[currentPosition]))
} }
} }
binding.moreAddPlaylistBtn.setOnClickListener {
if (currentPosition >= 0) {
requests.trySend(Request.OnAddPlaylist(favoriteBeans[currentPosition]))
hideBottomLayout()
}
}
} }
private fun showRemoveDownloadDialogHint(id: String) { private fun showRemoveDownloadDialogHint(id: String) {
@ -348,7 +363,7 @@ class MoLikedSongsActivity : MoBaseActivity(), LikedSongsAdapter.OnItemFavorites
else -> { else -> {
binding.downloadLoading.visibility = View.GONE binding.downloadLoading.visibility = View.GONE
binding.downloadImg.setImageResource(R.drawable.download_icon) binding.downloadImg.setImageResource(R.drawable.more_download_icon)
binding.downloadImg.visibility = View.VISIBLE binding.downloadImg.visibility = View.VISIBLE
binding.downloadTv.text = getString(R.string.download_save_offline) binding.downloadTv.text = getString(R.string.download_save_offline)

View File

@ -0,0 +1,79 @@
package melody.offline.music.adapter
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import melody.offline.music.App
import melody.offline.music.R
import melody.offline.music.bean.Playlist
import melody.offline.music.databinding.NewPlayListItemBinding
import melody.offline.music.databinding.PlayListItemBinding
import melody.offline.music.media.MediaControllerManager
class NewPlayListAdapter(
private val context: Context,
private val list: List<Playlist>,
) : RecyclerView.Adapter<NewPlayListAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = NewPlayListItemBinding.inflate(LayoutInflater.from(context), parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val bean = list[position]
holder.bind(bean)
holder.itemView.setOnClickListener {
if (itemClickListener != null) {
itemClickListener?.onItemClick(position)
}
}
}
override fun getItemCount(): Int = list.size
inner class ViewHolder(private val binding: NewPlayListItemBinding) :
RecyclerView.ViewHolder(binding.root) {
@OptIn(DelicateCoroutinesApi::class)
@SuppressLint("SetTextI18n")
fun bind(bean: Playlist) {
binding.apply {
title.text = bean.title
GlobalScope.launch {
val items = App.appPlaylistDBManager.getPlaylistItems(bean.id)
withContext(Dispatchers.Main) {
name.text = "${items.size} " + context.getString(R.string.songs)
if (items.isNotEmpty()) {
Glide.with(context).load(items[0].thumbnail).into(image)
}
}
}
}
}
}
private var itemClickListener: OnItemClickListener? = null
fun setOnItemClickListener(listener: OnItemClickListener) {
itemClickListener = listener
}
interface OnItemClickListener {
fun onItemClick(position: Int)
}
}

View File

@ -0,0 +1,38 @@
package melody.offline.music.bean
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.ForeignKey
import androidx.room.ColumnInfo
import androidx.room.Index
@Entity(tableName = "playlists")
data class Playlist(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "title") val title: String
)
@Entity(
tableName = "playlist_items",
foreignKeys = [
ForeignKey(
entity = Playlist::class,
parentColumns = ["id"],
childColumns = ["playlist_id"],
onDelete = ForeignKey.CASCADE
)
],
indices = [Index(value = ["playlist_id"])] // 添加索引
)
data class PlaylistItem(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "playlist_id") val playlistId: Int,
@ColumnInfo(name = "videoId") var videoId: String,
@ColumnInfo(name = "title") var title: String,
@ColumnInfo(name = "name") var name: String,
@ColumnInfo(name = "thumbnail") var thumbnail: String? = null,
@ColumnInfo(name = "bytesDownloaded") var bytesDownloaded: Long? = null,
@ColumnInfo(name = "size") var size: String? = null,
@ColumnInfo(name = "isOffline") var isOffline: Boolean,
@ColumnInfo(name = "isFavorite") var isFavorite: Boolean
)

View File

@ -1,4 +1,3 @@
package melody.offline.music.database package melody.offline.music.database
import androidx.room.Database import androidx.room.Database

View File

@ -0,0 +1,83 @@
package melody.offline.music.database
import android.content.Context
import androidx.room.Room
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import melody.offline.music.bean.Playlist
import melody.offline.music.bean.PlaylistItem
class AppPlaylistDBManager private constructor(context: Context) {
companion object {
@Volatile
private var instance: AppPlaylistDBManager? = null
fun getInstance(context: Context): AppPlaylistDBManager {
return instance ?: synchronized(this) {
instance ?: AppPlaylistDBManager(context).also { instance = it }
}
}
}
private val database = Room.databaseBuilder(
context.applicationContext, AppPlaylistDatabase::class.java, "app_playlist_data_base"
).build()
private val playlistDao = database.playlistDao()
suspend fun insertOrUpdatePlaylist(playlist: Playlist) {
return withContext(Dispatchers.IO) {
val existingPlaylist = playlistDao.getPlaylistById(playlist.id)
if (existingPlaylist == null) {
playlistDao.insertPlaylist(playlist)
} else {
playlistDao.updatePlaylist(playlist)
}
}
}
suspend fun insertOrUpdatePlaylistItem(playlistItem: PlaylistItem) {
return withContext(Dispatchers.IO) {
val existingItem = playlistDao.getPlaylistItemById(playlistItem.id)
if (existingItem == null) {
playlistDao.insertPlaylistItem(playlistItem)
} else {
playlistDao.updatePlaylistItem(playlistItem)
}
}
}
suspend fun getPlaylistByTitle(title: String): Playlist? {
return withContext(Dispatchers.IO) {
playlistDao.getPlaylistByTitle(title)
}
}
suspend fun getAllPlaylists(): List<Playlist> {
return withContext(Dispatchers.IO) {
playlistDao.getAllPlaylists()
}
}
suspend fun getPlaylistItems(playlistId: Int): List<PlaylistItem> {
return withContext(Dispatchers.IO) {
playlistDao.getPlaylistItems(playlistId)
}
}
suspend fun deletePlaylist(playlist: Playlist) {
withContext(Dispatchers.IO) {
playlistDao.deletePlaylist(playlist)
}
}
suspend fun deletePlaylistItem(playlistItem: PlaylistItem) {
withContext(Dispatchers.IO) {
playlistDao.deletePlaylistItem(playlistItem)
}
}
}

View File

@ -0,0 +1,12 @@
package melody.offline.music.database
import androidx.room.Database
import androidx.room.RoomDatabase
import melody.offline.music.bean.Playlist
import melody.offline.music.bean.PlaylistItem
@Database(entities = [Playlist::class, PlaylistItem::class], version = 1, exportSchema = false)
abstract class AppPlaylistDatabase : RoomDatabase() {
abstract fun playlistDao(): PlaylistDao
}

View File

@ -0,0 +1,41 @@
package melody.offline.music.database
import androidx.room.*
import melody.offline.music.bean.Playlist
import melody.offline.music.bean.PlaylistItem
@Dao
interface PlaylistDao {
@Insert
suspend fun insertPlaylist(playlist: Playlist): Long
@Insert
suspend fun insertPlaylistItem(playlistItem: PlaylistItem): Long
@Query("SELECT * FROM playlists WHERE id = :id")
suspend fun getPlaylistById(id: Int): Playlist?
@Query("SELECT * FROM playlists WHERE title = :title")
suspend fun getPlaylistByTitle(title: String): Playlist?
@Query("SELECT * FROM playlist_items WHERE id = :id")
suspend fun getPlaylistItemById(id: Int): PlaylistItem?
@Query("SELECT * FROM playlists")
fun getAllPlaylists(): List<Playlist>
@Query("SELECT * FROM playlist_items WHERE playlist_id = :playlistId")
fun getPlaylistItems(playlistId: Int): List<PlaylistItem>
@Update
suspend fun updatePlaylist(playlist: Playlist)
@Update
suspend fun updatePlaylistItem(playlistItem: PlaylistItem)
@Delete
suspend fun deletePlaylist(playlist: Playlist)
@Delete
suspend fun deletePlaylistItem(playlistItem: PlaylistItem)
}

View File

@ -1,9 +1,27 @@
package melody.offline.music.fragment package melody.offline.music.fragment
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.Intent import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.Window
import android.view.WindowManager
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import android.widget.EditText
import android.widget.RelativeLayout
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import com.bumptech.glide.Glide
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.gyf.immersionbar.ktx.immersionBar import com.gyf.immersionbar.ktx.immersionBar
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
@ -13,17 +31,23 @@ import melody.offline.music.R
import melody.offline.music.activity.MoLikedSongsActivity import melody.offline.music.activity.MoLikedSongsActivity
import melody.offline.music.activity.MoOfflineSongsActivity import melody.offline.music.activity.MoOfflineSongsActivity
import melody.offline.music.activity.SettingsActivity import melody.offline.music.activity.SettingsActivity
import melody.offline.music.adapter.LikedSongsAdapter
import melody.offline.music.adapter.NewPlayListAdapter
import melody.offline.music.ads.AdPlacement import melody.offline.music.ads.AdPlacement
import melody.offline.music.ads.LolAdWrapper import melody.offline.music.ads.LolAdWrapper
import melody.offline.music.bean.FavoriteBean
import melody.offline.music.bean.Playlist
import melody.offline.music.databinding.FragmentMoMeBinding import melody.offline.music.databinding.FragmentMoMeBinding
import melody.offline.music.util.AnalysisUtil import melody.offline.music.util.AnalysisUtil
class MoMeFragment : MoBaseFragment<FragmentMoMeBinding>() { class MoMeFragment : MoBaseFragment<FragmentMoMeBinding>(), NewPlayListAdapter.OnItemClickListener {
private val requests: Channel<Request> = Channel(Channel.UNLIMITED) private val requests: Channel<Request> = Channel(Channel.UNLIMITED)
private var adapter: NewPlayListAdapter? = null
private var playlist: MutableList<Playlist> = mutableListOf()
enum class Request { sealed class Request {
data class AddPlaylist(val text: String) : Request()
} }
override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentMoMeBinding override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentMoMeBinding
@ -32,6 +56,7 @@ class MoMeFragment : MoBaseFragment<FragmentMoMeBinding>() {
override suspend fun onViewCreated() { override suspend fun onViewCreated() {
LolAdWrapper.shared.loadAdIfNotCached(requireActivity(), AdPlacement.INST_ME_PAGE_LIST) LolAdWrapper.shared.loadAdIfNotCached(requireActivity(), AdPlacement.INST_ME_PAGE_LIST)
initView() initView()
initAdapter()
onReceive() onReceive()
} }
@ -46,7 +71,25 @@ class MoMeFragment : MoBaseFragment<FragmentMoMeBinding>() {
while (isActive) { while (isActive) {
select<Unit> { select<Unit> {
requests.onReceive { requests.onReceive {
when (it) {
is Request.AddPlaylist -> {
val playlist = App.appPlaylistDBManager.getPlaylistByTitle(it.text)
if (playlist != null) {
Toast.makeText(
requireActivity(),
getString(R.string.new_playlist_duplicate_name_hint),
Toast.LENGTH_LONG
).show()
} else {
val newPlaylist = Playlist(title = it.text)
App.appPlaylistDBManager.insertOrUpdatePlaylist(newPlaylist)
if (bottomSheetDialog != null) {
bottomSheetDialog?.dismiss()
}
getPlaylistData()
}
}
}
} }
events.onReceive { events.onReceive {
@ -88,6 +131,9 @@ class MoMeFragment : MoBaseFragment<FragmentMoMeBinding>() {
).show() ).show()
} }
} }
binding.newPlayListBtn.setOnClickListener {
showBottomDialog()
}
} }
private suspend fun fragmentOnResume() { private suspend fun fragmentOnResume() {
@ -100,6 +146,8 @@ class MoMeFragment : MoBaseFragment<FragmentMoMeBinding>() {
val favoriteBeans = App.appFavoriteDBManager.getAllFavoriteBeans() val favoriteBeans = App.appFavoriteDBManager.getAllFavoriteBeans()
val favorites = favoriteBeans.count { it.isFavorite } val favorites = favoriteBeans.count { it.isFavorite }
binding.likedSongsTv.text = "$favorites" binding.likedSongsTv.text = "$favorites"
getPlaylistData()
} }
override fun onResume() { override fun onResume() {
@ -121,4 +169,44 @@ class MoMeFragment : MoBaseFragment<FragmentMoMeBinding>() {
initImmersionBar() initImmersionBar()
} }
} }
private fun initAdapter() {
adapter = NewPlayListAdapter(requireActivity(), playlist)
adapter?.setOnItemClickListener(this)
binding.newPlayListRv.layoutManager =
LinearLayoutManager(requireActivity(), LinearLayoutManager.VERTICAL, false)
binding.newPlayListRv.adapter = adapter
}
@SuppressLint("NotifyDataSetChanged")
private suspend fun getPlaylistData() {
playlist.clear()
playlist.addAll(App.appPlaylistDBManager.getAllPlaylists())
adapter?.notifyDataSetChanged()
}
override fun onItemClick(position: Int) {
}
private var bottomSheetDialog: BottomSheetDialog? = null
private fun showBottomDialog() {
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()) {
requests.trySend(Request.AddPlaylist(text))
}
}
// 设置对话框背景为透明以显示圆角
bottomSheetDialog?.window?.setBackgroundDrawableResource(android.R.color.transparent)
bottomSheetDialog?.window?.navigationBarColor =
ContextCompat.getColor(requireActivity(), R.color.main_bg_color)
bottomSheetDialog?.show()
}
} }

View File

@ -0,0 +1,8 @@
<!-- res/anim/slide_in_bottom.xml -->
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="false">
<translate
android:fromYDelta="100%p"
android:toYDelta="0%p"
android:duration="300"/>
</set>

View File

@ -0,0 +1,8 @@
<!-- res/anim/slide_out_bottom.xml -->
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="false">
<translate
android:fromYDelta="0%p"
android:toYDelta="100%p"
android:duration="300"/>
</set>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group>
<clip-path
android:pathData="M0,0h24v24h-24z"/>
<path
android:pathData="M12.04,2.182C12.401,2.182 12.748,2.325 13.004,2.581C13.26,2.837 13.403,3.184 13.403,3.545L13.403,10.734H21C21.361,10.734 21.708,10.878 21.964,11.133C22.22,11.389 22.364,11.736 22.364,12.098C22.364,12.459 22.22,12.806 21.964,13.062C21.708,13.318 21.361,13.461 21,13.461H13.403V21C13.403,21.362 13.259,21.708 13.003,21.964C12.748,22.22 12.401,22.364 12.039,22.364C11.677,22.364 11.331,22.22 11.075,21.964C10.819,21.708 10.675,21.362 10.675,21V13.461H3.545C3.184,13.461 2.837,13.318 2.581,13.062C2.325,12.806 2.182,12.459 2.182,12.098C2.182,11.736 2.325,11.389 2.581,11.133C2.837,10.878 3.184,10.734 3.545,10.734H10.675V3.545C10.675,3.184 10.819,2.837 11.075,2.581C11.331,2.325 11.677,2.182 12.039,2.182H12.04Z"
android:fillColor="#ffffff"/>
</group>
</vector>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#26FFFFFF" />
<corners android:radius="16dp" />
</shape>

View File

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/bottom_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/drw_bottom_layout_bg"
android:orientation="vertical"
android:visibility="visible">
<LinearLayout
android:id="@+id/layout_info"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="9"
android:orientation="vertical"
android:paddingBottom="32dp"
android:visibility="visible">
<LinearLayout
android:id="@+id/bottomCloseBtn"
android:layout_width="match_parent"
android:layout_height="32dp"
android:gravity="center">
<TextView
android:layout_width="30dp"
android:layout_height="4dp"
android:background="@drawable/drw_bottom_close_btn_bg"
android:gravity="center" />
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:fontFamily="@font/medium_font"
android:gravity="center_horizontal"
android:text="@string/add_to_playlist"
android:textColor="@color/white"
android:textSize="18dp" />
<LinearLayout
android:id="@+id/newPlayListBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<RelativeLayout
android:layout_width="56dp"
android:layout_height="56dp"
android:background="@drawable/drw_round_48_bg">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:src="@drawable/add_white_icon" />
</RelativeLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:fontFamily="@font/regular_font"
android:text="@string/new_playlist"
android:textColor="@color/white"
android:textSize="16dp" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/newPlayListRv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
</LinearLayout>

View File

@ -63,7 +63,10 @@
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="14dp" android:layout_marginStart="12dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="8dp"
android:orientation="horizontal"> android:orientation="horizontal">
<LinearLayout <LinearLayout
@ -201,10 +204,62 @@
android:layout_marginStart="14dp" android:layout_marginStart="14dp"
android:layout_marginEnd="14dp" /> android:layout_marginEnd="14dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:fontFamily="@font/medium_font"
android:text="@string/new_playlist"
android:textColor="@color/white"
android:textSize="20dp" />
<LinearLayout
android:id="@+id/newPlayListBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<RelativeLayout
android:layout_width="56dp"
android:layout_height="56dp"
android:background="@drawable/drw_round_48_bg">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:src="@drawable/add_white_icon" />
</RelativeLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:fontFamily="@font/regular_font"
android:text="@string/new_playlist"
android:textColor="@color/white"
android:textSize="16dp" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/newPlayListRv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:overScrollMode="never"
android:scrollbars="none" />
</LinearLayout> </LinearLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>
</LinearLayout> </LinearLayout>
</RelativeLayout> </RelativeLayout>

View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:ignore="MissingDefaultResource">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingBottom="12dp">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp">
<androidx.cardview.widget.CardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:elevation="0dp"
android:visibility="visible"
app:cardCornerRadius="4dp"
app:cardElevation="0dp">
<ImageView
android:id="@+id/image"
android:layout_width="56dp"
android:layout_height="56dp"
android:scaleType="centerCrop"
android:src="@mipmap/app_logo" />
</androidx.cardview.widget.CardView>
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="vertical">
<melody.offline.music.view.MarqueeTextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/regular_font"
android:maxLines="1"
android:text="@string/app_name"
android:textColor="@color/white"
android:textSize="14dp" />
<melody.offline.music.view.MarqueeTextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:fontFamily="@font/regular_font"
android:maxLines="1"
android:text="@string/app_name"
android:textColor="@color/white_60"
android:textSize="12dp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/bottom_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/drw_bottom_layout_bg"
android:orientation="vertical"
android:visibility="visible">
<LinearLayout
android:id="@+id/layout_info"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="9"
android:orientation="vertical"
android:paddingBottom="32dp"
android:visibility="visible">
<LinearLayout
android:id="@+id/bottomCloseBtn"
android:layout_width="match_parent"
android:layout_height="32dp"
android:gravity="center">
<TextView
android:layout_width="30dp"
android:layout_height="4dp"
android:background="@drawable/drw_bottom_close_btn_bg"
android:gravity="center" />
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:gravity="center"
android:text="@string/new_playlist"
android:textColor="@color/white"
android:textSize="16dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:background="@drawable/drw_round_16_bg">
<EditText
android:id="@+id/playlistEt"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:background="@null"
android:gravity="center"
android:text="Playlist 1"
android:textSize="14dp" />
</LinearLayout>
<TextView
android:id="@+id/confirmBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="32dp"
android:background="@drawable/drw_btn_bg"
android:fontFamily="@font/medium_font"
android:paddingStart="32dp"
android:paddingTop="16dp"
android:paddingEnd="32dp"
android:paddingBottom="16dp"
android:text="@string/confirm"
android:textColor="@color/black"
android:textSize="14dp" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
</LinearLayout>

View File

@ -47,4 +47,9 @@
<string name="download_save_offline">Save to Offline</string> <string name="download_save_offline">Save to Offline</string>
<string name="download_remove_offline">Remove from offline</string> <string name="download_remove_offline">Remove from offline</string>
<string name="download_remove_hint">Are you sure you want to delete this downloaded music file?</string> <string name="download_remove_hint">Are you sure you want to delete this downloaded music file?</string>
<string name="confirm">Confirm</string>
<string name="new_playlist_duplicate_name_hint">A playlist with the same name already exists.</string>
<string name="created_successfully">Created successfully</string>
<string name="song_exists_playlist_hint">This song already exists in this playlist.</string>
<string name="added_playlist_success_Hint">Successfully added to the playlist</string>
</resources> </resources>

View File

@ -7,4 +7,9 @@
<item name="android:minHeight">4dp</item> <!-- 设置进度条高度 --> <item name="android:minHeight">4dp</item> <!-- 设置进度条高度 -->
<item name="android:maxHeight">4dp</item> <!-- 设置进度条高度 --> <item name="android:maxHeight">4dp</item> <!-- 设置进度条高度 -->
</style> </style>
<style name="DialogAnimation">
<item name="android:windowEnterAnimation">@anim/slide_in_bottom</item>
<item name="android:windowExitAnimation">@anim/slide_out_bottom</item>
</style>
</resources> </resources>