This commit is contained in:
ocean 2024-05-24 13:43:13 +08:00
parent 0fa6e9156c
commit d0a2d1d2cf
28 changed files with 1198 additions and 214 deletions

View File

@ -3,6 +3,7 @@ plugins {
id("org.jetbrains.kotlin.android")
id("kotlin-kapt")
id("org.jetbrains.kotlin.plugin.serialization")
id("kotlin-android")
}
android {
@ -58,6 +59,9 @@ dependencies {
implementation("com.google.android.material:material:1.11.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.media3:media3-session:1.3.1")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.0")
// implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.8.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")

View File

@ -66,6 +66,9 @@
<activity
android:name=".activity.MoSingerMoreSongActivity"
android:screenOrientation="portrait" />
<activity
android:name=".activity.MoOfflineSongsActivity"
android:screenOrientation="portrait" />
<service
android:name=".service.PlaybackService"

View File

@ -7,11 +7,13 @@ import com.player.musicoo.bean.Audio
import com.player.musicoo.bean.CurrentPlayingAudio
import com.player.musicoo.bean.ResourcesList
import com.player.musicoo.database.AppDatabase
import com.player.musicoo.database.AppOfflineDBManager
import com.player.musicoo.database.CurrentAudioDatabase
import com.player.musicoo.database.CurrentAudioManager
import com.player.musicoo.database.DatabaseManager
import com.player.musicoo.media.MediaControllerManager
import com.player.musicoo.util.CacheManager
import com.player.musicoo.util.DownloadUtil
import com.player.musicoo.util.parseResources
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -24,6 +26,8 @@ class App : Application() {
companion object {
lateinit var app: App
private set
lateinit var appOfflineDBManager: AppOfflineDBManager
private set
lateinit var currentAudioManager: CurrentAudioManager
private set
lateinit var databaseManager: DatabaseManager
@ -102,11 +106,13 @@ class App : Application() {
app = this
initialize(this)
MediaControllerManager.init(this)
appOfflineDBManager = AppOfflineDBManager.getInstance(this)
currentAudioManager = CurrentAudioManager.getInstance(this)
databaseManager = DatabaseManager.getInstance(this)
initCurrentPlayingAudio()
initImportAudio()
CacheManager.initializeCaches(this)
DownloadUtil.getDownloadManager(this)
}
}

View File

@ -2,12 +2,9 @@ package com.player.musicoo.activity
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.renderscript.Allocation
import android.renderscript.Element
import android.renderscript.RenderScript
@ -19,16 +16,19 @@ import android.widget.RelativeLayout
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.LifecycleOwner
import androidx.media3.common.Player
import androidx.media3.session.MediaController
import com.bumptech.glide.Glide
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.Download
import androidx.media3.exoplayer.offline.DownloadManager
import com.player.musicoo.App
import com.player.musicoo.R
import com.player.musicoo.bean.OfflineBean
import com.player.musicoo.innertube.Innertube
import com.player.musicoo.innertube.Innertube.TAG
import com.player.musicoo.media.MediaControllerManager
import com.player.musicoo.sp.AppStore
import com.player.musicoo.util.DownloadUtil
import com.player.musicoo.util.FileSizeConverter
import com.player.musicoo.util.LogTag
import com.player.musicoo.view.MusicPlayerView
import kotlinx.coroutines.CoroutineScope
@ -38,11 +38,12 @@ import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.IOException
import java.io.InputStream
abstract class MoBaseActivity : AppCompatActivity(), CoroutineScope by MainScope() {
@UnstableApi
abstract class MoBaseActivity : AppCompatActivity(), CoroutineScope by MainScope(),
LifecycleOwner {
private var playerListener: Player.Listener? = null
private var downloadManagerListener: DownloadManager.Listener? = null
enum class Event {
ActivityStart,
@ -64,6 +65,7 @@ abstract class MoBaseActivity : AppCompatActivity(), CoroutineScope by MainScope
this.defer = operation
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
musicPlayerView = MusicPlayerView(this, meController)
@ -131,7 +133,6 @@ abstract class MoBaseActivity : AppCompatActivity(), CoroutineScope by MainScope
override fun onDestroy() {
super.onDestroy()
LogTag.LogD(TAG, "MoBaseActivity onDestroy")
if (meController != null && playerListener != null) {
meController.removeListener(playerListener!!)
}
@ -147,10 +148,6 @@ abstract class MoBaseActivity : AppCompatActivity(), CoroutineScope by MainScope
reason: Int
) {
if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
LogTag.LogD(
Innertube.TAG,
"MoBaseActivity DISCONTINUITY_REASON_AUTO_TRANSITION"
)
if (meController != null) {
musicPlayerView.updateInfoUi(meController.currentMediaItem)
musicPlayerView.updateSetProgress(meController)
@ -162,7 +159,6 @@ abstract class MoBaseActivity : AppCompatActivity(), CoroutineScope by MainScope
}
override fun onPlaybackStateChanged(playbackState: Int) {
LogTag.LogD(Innertube.TAG, "MoBaseActivity playbackState->$playbackState")
val meController = MediaControllerManager.getController()
if (meController != null) {
musicPlayerView.updateProgressState(meController)
@ -181,10 +177,6 @@ abstract class MoBaseActivity : AppCompatActivity(), CoroutineScope by MainScope
playWhenReady: Boolean,
reason: Int
) {
LogTag.LogD(
Innertube.TAG,
"MoBaseActivity onPlayWhenReadyChanged->$playWhenReady"
)
musicPlayerView.updatePlayState(playWhenReady)
val meController = MediaControllerManager.getController()
if (meController != null) {

View File

@ -103,6 +103,10 @@ class MoListDetailsActivity : MoBaseActivity() {
showLoadingUi()
Innertube.moPlaylistPage(browseId)
?.onSuccess {
if (this.isDestroyed || this.isFinishing) {
return
}
showDataUi()
Glide.with(this)
.load(it.thumbnail)

View File

@ -0,0 +1,143 @@
package com.player.musicoo.activity
import android.annotation.SuppressLint
import android.view.View
import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.LinearLayoutManager
import com.bumptech.glide.Glide
import com.gyf.immersionbar.ktx.immersionBar
import com.player.musicoo.App
import com.player.musicoo.adapter.DetailsListAdapter
import com.player.musicoo.adapter.OfflineSongsAdapter
import com.player.musicoo.bean.OfflineBean
import com.player.musicoo.databinding.ActivityDetailsBinding
import com.player.musicoo.databinding.ActivityOfflineSongsBinding
import com.player.musicoo.innertube.Innertube
import com.player.musicoo.innertube.requests.moPlaylistPage
import com.player.musicoo.util.DownloadUtil
import com.player.musicoo.util.LogTag.LogD
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
@UnstableApi
class MoOfflineSongsActivity : MoBaseActivity() {
private val requests: Channel<Request> = Channel(Channel.UNLIMITED)
enum class Request {
TryAgain,
}
private lateinit var binding: ActivityOfflineSongsBinding
private var adapter: OfflineSongsAdapter? = null
private var offlineList: MutableList<OfflineBean> = mutableListOf()
override suspend fun main() {
binding = ActivityOfflineSongsBinding.inflate(layoutInflater)
setContentView(binding.root)
initImmersionBar()
initView()
initAdapter()
initData()
onReceive()
}
private fun initImmersionBar() {
immersionBar {
statusBarDarkFont(false)
statusBarView(binding.view)
}
}
@SuppressLint("NotifyDataSetChanged")
private suspend fun onReceive() {
while (isActive) {
select<Unit> {
requests.onReceive {
when (it) {
Request.TryAgain -> {
initData()
}
}
}
events.onReceive {
when (it) {
Event.ActivityOnResume -> {
activityOnResume()
}
Event.AutomaticallySwitchSongs -> {
if (adapter != null) {
adapter?.notifyDataSetChanged()
}
}
else -> {}
}
}
}
}
}
@SuppressLint("NotifyDataSetChanged")
private fun activityOnResume() {
addMusicPlayerViewToLayout(binding.playMusicLayout)
if (adapter != null) {
adapter?.notifyDataSetChanged()
}
}
private fun initView() {
binding.backBtn.setOnClickListener {
finish()
}
binding.tryAgainBtn.setOnClickListener {
requests.trySend(Request.TryAgain)
}
}
private fun initAdapter() {
adapter = OfflineSongsAdapter(this, offlineList)
binding.rv.layoutManager =
LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
binding.rv.adapter = adapter
}
@SuppressLint("NotifyDataSetChanged")
private suspend fun initData() {
showLoadingUi()
offlineList.clear()
offlineList.addAll(App.appOfflineDBManager.getAllOfflineBeans())
for (offline in offlineList){
LogD(TAG,"offline id->${offline.videoId}")
}
if (offlineList.size > 0) {
showDataUi()
} else {
showNoContentUi()
}
if (adapter != null) {
adapter?.notifyDataSetChanged()
}
}
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() {
binding.loadingLayout.visibility = View.GONE
binding.noContentLayout.visibility = View.VISIBLE
}
}

View File

@ -10,6 +10,7 @@ import android.view.View
import android.view.animation.AnimationUtils
import androidx.annotation.OptIn
import androidx.core.net.toUri
import androidx.lifecycle.LifecycleOwner
import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
@ -23,25 +24,29 @@ import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import com.gyf.immersionbar.ktx.immersionBar
import com.player.musicoo.App
import com.player.musicoo.R
import com.player.musicoo.adapter.PlayListAdapter
import com.player.musicoo.bean.OfflineBean
import com.player.musicoo.databinding.ActivityMoPlayDetailsBinding
import com.player.musicoo.innertube.Innertube
import com.player.musicoo.media.MediaControllerManager
import com.player.musicoo.media.SongRadio
import com.player.musicoo.service.MyDownloadService
import com.player.musicoo.service.ViewModelMain
import com.player.musicoo.sp.AppStore
import com.player.musicoo.util.DownloadUtil
import com.player.musicoo.util.FileSizeConverter
import com.player.musicoo.util.LogTag
import com.player.musicoo.util.LogTag.LogD
import com.player.musicoo.util.PlayMode
import com.player.musicoo.util.asMediaItem
import com.player.musicoo.util.convertMillisToMinutesAndSecondsString
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.select
import java.lang.Exception
@OptIn(UnstableApi::class)
class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
@ -77,7 +82,6 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
initImmersionBar()
initClick()
initPlayerListener()
initDownloadListener()
initPlayListAdapter()
updatePlayModeUi()
val videoId = intent.getStringExtra(PLAY_DETAILS_VIDEO_ID)
@ -87,14 +91,21 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
comeFrom = intent.getSerializableExtra(PLAY_DETAILS_COME_FROM) as Class<*>?
if (comeFrom != null && comeFrom == PrimaryActivity::class.java) {
LogD(TAG, "从当前播放的悬浮layout进入")
// 处理来自 PrimaryActivity 的情况
updateCurrentMediaItemInfo()
} else {
if (meController != null && meController.currentMediaItem != null && videoId == meController.currentMediaItem?.mediaId) {
//进入的id与当前的id一样就不重新去获取播放
updateCurrentMediaItemInfo()
if (meController != null && meController.currentMediaItem != null) {
updateInfoUi(meController.currentMediaItem)
}
} else {
LogD(TAG, "从点击任意歌曲进入")
// if (meController != null && meController.currentMediaItem != null && videoId == meController.currentMediaItem?.mediaId) {
// //进入的id与当前的id一样就不重新去获取播放
// updateCurrentMediaItemInfo()
// updateInfoUi(meController.currentMediaItem)
// } else {
//
// }
binding.nameTv.text = intent.getStringExtra(PLAY_DETAILS_NAME)
binding.descTv.text = intent.getStringExtra(PLAY_DETAILS_DESC)
@ -102,6 +113,8 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
finish()
return
}
//要加载数据的话就隐藏喜欢和下载按钮
binding.likeAndDownloadLayout.visibility = View.GONE
//传入进来的ID就是进入此界面的当前ID
currentVideoID = videoId
//根据进来界面的当前ID来获取资源。
@ -112,8 +125,7 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
params
)
}
}
initDownloadFlow()
onReceive()
}
@ -134,47 +146,35 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
}
private fun activityOnResume() {
if (comeFrom != null && comeFrom == PrimaryActivity::class.java) {
// if (meController != null && meController.currentMediaItem != null) {
// updateInfoUi(meController.currentMediaItem)
// }
}
private fun initDownloadFlow() {
ViewModelMain.modelDownloadsFlow.observe(this) { downloads ->
if (meController != null && meController.currentMediaItem != null) {
updateInfoUi(meController.currentMediaItem)
}
}
if (meController != null && meController.currentMediaItem != null) {
LogD(TAG,"meController.currentMediaItem != null->${meController.currentMediaItem?.mediaId!!}")
updateDownloadUi(meController.currentMediaItem?.mediaId!!)
} else {
LogD(TAG,"currentVideoID->${currentVideoID}")
updateDownloadUi(currentVideoID)
val id = meController.currentMediaItem?.mediaId
LogD(TAG, "initDownloadFlow id ->${id}")
val currentScreenDownloads = downloads[id]
LogD(TAG, "currentScreenDownloads->${currentScreenDownloads}")
if (currentScreenDownloads != null) {
updateDownloadUI(currentScreenDownloads)
}
}
private fun updateDownloadUi(id: String) {
if (DownloadUtil.downloadResourceExist(id)) {
binding.downloadImg.setImageResource(R.drawable.download_done_icon)
} else {
binding.downloadImg.setImageResource(R.drawable.download_icon)
}
}
private fun initPlayerListener() {
meController?.addListener(playerListener)
}
private fun initDownloadListener() {
downloadManager = DownloadUtil.getDownloadManager(this)
if (downloadManager != null) {
downloadManager?.addListener(object : DownloadManager.Listener {
override fun onDownloadChanged(
downloadManager: DownloadManager,
download: Download,
finalException: Exception?
) {
private fun updateDownloadUI(download: Download) {
when (download.state) {
Download.STATE_DOWNLOADING -> {
binding.downloadLoading.visibility = View.VISIBLE
binding.downloadImg.setImageResource(R.drawable.download_icon)
binding.downloadImg.visibility = View.GONE
binding.downloadBtn.isClickable = false
binding.downloadBtn.isEnabled = false
}
Download.STATE_COMPLETED -> {
@ -183,16 +183,41 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
binding.downloadImg.visibility = View.VISIBLE
}
else -> {
Download.STATE_FAILED -> {
binding.downloadLoading.visibility = View.GONE
binding.downloadImg.setImageResource(R.drawable.error)
binding.downloadImg.visibility = View.VISIBLE
binding.downloadBtn.isClickable = true
binding.downloadBtn.isEnabled = true
}
else -> {
binding.downloadLoading.visibility = View.GONE
binding.downloadImg.setImageResource(R.drawable.download_icon)
binding.downloadImg.visibility = View.VISIBLE
binding.downloadBtn.isClickable = true
binding.downloadBtn.isEnabled = true
}
}
}
private fun updateDownloadUi(id: String) {
if (DownloadUtil.downloadResourceExist(id)) {//已经下载,按钮不可点击
binding.downloadImg.setImageResource(R.drawable.download_done_icon)
binding.downloadBtn.isClickable = false
binding.downloadBtn.isEnabled = false
} else {
binding.downloadImg.setImageResource(R.drawable.download_icon)
binding.downloadBtn.isClickable = true
binding.downloadBtn.isEnabled = true
}
})
}
private fun initPlayerListener() {
meController?.addListener(playerListener)
}
private fun updateCurrentMediaItemInfo() {
@ -289,11 +314,6 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
PlayMode.RANDOM.value -> {
meController.repeatMode = Player.REPEAT_MODE_ALL
// val availableCommands = meController.availableCommands
// //控制器支持设置随机播放模式的命令
// if (availableCommands.contains(Player.COMMAND_SET_SHUFFLE_MODE)) {
// meController.shuffleModeEnabled = true
// }
meController.shuffleModeEnabled = true
}
}
@ -363,22 +383,12 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
binding.downloadBtn.setOnClickListener {
if (meController != null && meController.currentMediaItem != null) {
val contentId = meController.currentMediaItem?.mediaId!!
val currentMediaItem = meController.currentMediaItem
val contentId = currentMediaItem?.mediaId ?: ""
//如果已经存在就不进行下载
if (DownloadUtil.downloadResourceExist(contentId)) {
return@setOnClickListener
}
val currentDownload = DownloadUtil.currentDownload(contentId)
if (currentDownload != null) {
if (currentDownload.state == Download.STATE_DOWNLOADING) {
DownloadService.sendRemoveDownload(
this,
MyDownloadService::class.java,
contentId,
false
)
}
} else {
val downloadRequest = DownloadRequest
.Builder(contentId, contentId.toUri())
.setCustomCacheKey(contentId)
@ -393,7 +403,6 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
}
}
}
private fun updatePlayModeUi() {
binding.modePlayImg.setImageResource(
@ -453,6 +462,8 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
}
if (mediaItem != null) {
//数据请求完毕mediaItem不等于空就显示喜欢与下载按钮
binding.likeAndDownloadLayout.visibility = View.VISIBLE
updateInfoUi(mediaItem)
binding.playbackErrorLayout.visibility = View.GONE
@ -573,6 +584,8 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
binding.playbackErrorLayout.visibility = View.VISIBLE
return
}
// currentVideoID = mediaItem.mediaId
updateDownloadUi(mediaItem.mediaId)
Glide.with(this)
.asBitmap()

View File

@ -51,8 +51,8 @@ class DetailsListAdapter(
)
intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_PLAY_PARAMS, bean.params)
intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_NAME, bean.name)
intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_DESC, bean.title)
intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_NAME, bean.title)
intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_DESC, bean.name)
context.startActivity(intent)
}
}

View File

@ -0,0 +1,99 @@
package com.player.musicoo.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.media3.common.Player
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.player.musicoo.R
import com.player.musicoo.bean.OfflineBean
import com.player.musicoo.databinding.OfflineListItemBinding
import com.player.musicoo.databinding.PlayListItemBinding
import com.player.musicoo.media.MediaControllerManager
class OfflineSongsAdapter(
private val context: Context,
private val list: List<OfflineBean>,
) :
RecyclerView.Adapter<OfflineSongsAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = OfflineListItemBinding.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 {
// val meController = MediaControllerManager.getController()
// if (meController != null && meController.currentMediaItem != null) {
// var index = holder.bindingAdapterPosition
// if (index > meController.mediaItemCount) {
// index = 1
// }
// meController.seekTo(index, C.TIME_UNSET)
// if (!meController.isPlaying) {
// meController.prepare()
// meController.play()
// }
// }
}
}
override fun getItemCount(): Int = list.size
inner class ViewHolder(private val binding: OfflineListItemBinding) :
RecyclerView.ViewHolder(binding.root) {
@SuppressLint("SetTextI18n")
fun bind(bean: OfflineBean) {
binding.apply {
Glide.with(context)
.load(bean.thumbnail)
.into(image)
title.text = bean.title
if (bean.name.isEmpty()) {
name.visibility = View.GONE
} else {
name.visibility = View.VISIBLE
name.text = bean.name
}
size.text = bean.size
val meController = MediaControllerManager.getController()
if (meController != null && meController.currentMediaItem != null) {
if (meController.currentMediaItem?.mediaId == bean.videoId) {
binding.listPlayView.visibility = View.VISIBLE
binding.title.setTextColor(context.getColor(R.color.green))
binding.name.setTextColor(context.getColor(R.color.green_60))
binding.size.setTextColor(context.getColor(R.color.green_60))
} else {
binding.title.setTextColor(context.getColor(R.color.white))
binding.name.setTextColor(context.getColor(R.color.white_60))
binding.size.setTextColor(context.getColor(R.color.white_60))
binding.listPlayView.visibility = View.GONE
}
}
}
}
}
private var itemClickListener: OnItemClickListener? = null
fun setOnItemClickListener(listener: OnItemClickListener) {
itemClickListener = listener
}
interface OnItemClickListener {
fun onItemClick(position: Int)
}
}

View File

@ -0,0 +1,22 @@
package com.player.musicoo.bean
import android.net.Uri
import androidx.annotation.Keep
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.io.Serializable
@Keep
@Entity
data class OfflineBean(
@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 = "size") var size: String? = null,
@ColumnInfo(name = "isOffline") var isOffline: Boolean
) : Serializable {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}

View File

@ -0,0 +1,83 @@
package com.player.musicoo.database
import android.content.Context
import androidx.room.Room
import com.player.musicoo.bean.OfflineBean
import com.player.musicoo.util.LogTag
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class AppOfflineDBManager private constructor(context: Context) {
companion object {
@Volatile
private var instance: AppOfflineDBManager? = null
fun getInstance(context: Context): AppOfflineDBManager {
return instance ?: synchronized(this) {
instance ?: AppOfflineDBManager(context).also { instance = it }
}
}
}
private val database = Room.databaseBuilder(
context.applicationContext,
AppOfflineDatabase::class.java, "offline_data_base"
).build()
private val dao = database.localOfflineDao()
suspend fun insertOfflineBean(bean: OfflineBean) {
withContext(Dispatchers.IO) {
val offlineBean = getOfflineBeanByID(bean.videoId)
if (offlineBean == null) {
LogTag.LogD(LogTag.VO_TEST_ONLY,"insertOfflineBean")
dao.insertOfflineBean(bean)
} else {
LogTag.LogD(LogTag.VO_TEST_ONLY,"updateOfflineBean")
dao.updateOfflineBean(bean)
}
}
}
suspend fun insertOfflineListBean(list: List<OfflineBean>) {
withContext(Dispatchers.IO) {
for (bean in list) {
val offlineBean = getOfflineBeanByID(bean.videoId)
if (offlineBean == null) {
dao.insertOfflineBean(bean)
} else {
dao.updateOfflineBean(bean)
}
}
}
}
suspend fun getAllOfflineBeans(): List<OfflineBean> {
return withContext(Dispatchers.IO) {
dao.getAllOfflineBeans()
}
}
suspend fun deleteOfflineBean(bean: OfflineBean) {
withContext(Dispatchers.IO) {
dao.deleteOfflineBean(bean)
}
}
suspend fun deleteAllOfflineBean() {
withContext(Dispatchers.IO) {
dao.deleteAllOfflineBean()
}
}
suspend fun updateOfflineBean(bean: OfflineBean) {
withContext(Dispatchers.IO) {
dao.updateOfflineBean(bean)
}
}
private suspend fun getOfflineBeanByID(id: String): OfflineBean? {
return dao.getOfflineBeanByID(id)
}
}

View File

@ -0,0 +1,11 @@
package com.player.musicoo.database
import androidx.room.Database
import androidx.room.RoomDatabase
import com.player.musicoo.bean.OfflineBean
@Database(entities = [OfflineBean::class], version = 1, exportSchema = false)
abstract class AppOfflineDatabase : RoomDatabase() {
abstract fun localOfflineDao(): OfflineDao
}

View File

@ -0,0 +1,33 @@
package com.player.musicoo.database
import androidx.room.*
import com.player.musicoo.bean.OfflineBean
@Dao
interface OfflineDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertOfflineBean(bean: OfflineBean)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertOfflineBeans(beans: List<OfflineBean>)
@Query("SELECT * FROM OfflineBean")
suspend fun getAllOfflineBeans(): List<OfflineBean>
@Delete
suspend fun deleteOfflineBean(bean: OfflineBean)
@Query("DELETE FROM OfflineBean")
suspend fun deleteAllOfflineBean()
@Update
suspend fun updateOfflineBean(bean: OfflineBean)
@Query("SELECT * FROM OfflineBean WHERE videoId = :id LIMIT 1")
suspend fun getOfflineBeanByID(id: String): OfflineBean?
@Query("SELECT * FROM OfflineBean WHERE isOffline = 1")
suspend fun getOfflineBeanByIsOffline(): List<OfflineBean>
}

View File

@ -1,10 +1,15 @@
package com.player.musicoo.fragment
import android.content.Intent
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.Toast
import com.gyf.immersionbar.ktx.immersionBar
import com.player.musicoo.databinding.FragmentMoHomeBinding
import com.player.musicoo.R
import com.player.musicoo.activity.MoOfflineSongsActivity
import com.player.musicoo.databinding.FragmentMoMeBinding
import com.player.musicoo.innertube.utils.BrotliEncoder.decode
import com.player.musicoo.util.DownloadUtil
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
@ -38,13 +43,39 @@ class MoMeFragment : MoBaseFragment<FragmentMoMeBinding>() {
requests.onReceive {
}
events.onReceive {
when (it) {
Event.FragmentOnResume -> {
fragmentOnResume()
}
}
}
}
}
}
private fun initView() {
binding.likedSongsBtn.setOnClickListener {
}
binding.offlineSongsBtn.setOnClickListener {
if (DownloadUtil.getDownloadCount() > 0) {
val intent = Intent(context, MoOfflineSongsActivity::class.java)
startActivity(intent)
} else {
Toast.makeText(
activity,
getString(R.string.offline_songs_no_data_prompt),
Toast.LENGTH_LONG
).show()
}
}
}
private fun fragmentOnResume() {
binding.offlineSongsTv.text = "${DownloadUtil.getDownloadCount()}"
}
override fun onResume() {
super.onResume()

View File

@ -12,8 +12,13 @@ import androidx.media3.exoplayer.offline.DownloadManager
import androidx.media3.exoplayer.offline.DownloadNotificationHelper
import androidx.media3.exoplayer.offline.DownloadService
import androidx.media3.exoplayer.scheduler.PlatformScheduler
import com.player.musicoo.App
import com.player.musicoo.R
import com.player.musicoo.bean.OfflineBean
import com.player.musicoo.util.DownloadUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@OptIn(UnstableApi::class)
@ -37,8 +42,9 @@ class MyDownloadService : DownloadService(
val downloadManager = DownloadUtil.getDownloadManager(this)
downloadManager!!.maxParallelDownloads = 3
val downloadNotificationHelper = DownloadUtil.getDownloadNotificationHelper(this)
downloadManager.addListener(
TerminalStateNotificationHelper(
DownloadUtil.TerminalStateNotificationHelper(
this, downloadNotificationHelper!!, FOREGROUND_NOTIFICATION_ID + 1
)
)
@ -63,43 +69,4 @@ class MyDownloadService : DownloadService(
notMetRequirements
)
}
private class TerminalStateNotificationHelper(
private val context: Context,
private val notificationHelper: DownloadNotificationHelper,
private var nextNotificationId: Int
) :
DownloadManager.Listener {
override fun onDownloadChanged(
downloadManager: DownloadManager,
download: Download,
finalException: Exception?
) {
val notification: Notification = when (download.state) {
Download.STATE_COMPLETED -> {
notificationHelper.buildDownloadCompletedNotification(
context,
R.drawable.download_done_icon,
null,
Util.fromUtf8Bytes(download.request.data)
)
}
Download.STATE_FAILED -> {
notificationHelper.buildDownloadFailedNotification(
context,
R.drawable.error,
null,
Util.fromUtf8Bytes(download.request.data)
)
}
else -> {
return
}
}
NotificationUtil.setNotification(context, nextNotificationId++, notification)
}
}
}

View File

@ -0,0 +1,9 @@
package com.player.musicoo.service
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.media3.exoplayer.offline.Download
object ViewModelMain : ViewModel() {
var modelDownloadsFlow = MutableLiveData<Map<String, Download>>()
}

View File

@ -1,11 +1,16 @@
package com.player.musicoo.util
import android.app.Notification
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.annotation.OptIn
import androidx.core.net.toUri
import androidx.lifecycle.MutableLiveData
import androidx.media3.common.PlaybackException
import androidx.media3.common.util.NotificationUtil
import androidx.media3.common.util.UnstableApi
import androidx.media3.common.util.Util
import androidx.media3.database.DatabaseProvider
import androidx.media3.database.StandaloneDatabaseProvider
import androidx.media3.datasource.DataSource
@ -23,15 +28,28 @@ import androidx.media3.datasource.cronet.CronetUtil
import androidx.media3.exoplayer.offline.Download
import androidx.media3.exoplayer.offline.DownloadManager
import androidx.media3.exoplayer.offline.DownloadNotificationHelper
import com.player.musicoo.App
import com.player.musicoo.R
import com.player.musicoo.bean.OfflineBean
import com.player.musicoo.innertube.Innertube
import com.player.musicoo.innertube.Innertube.TAG
import com.player.musicoo.innertube.models.bodies.PlayerBody
import com.player.musicoo.innertube.requests.player
import com.player.musicoo.media.MediaControllerManager
import com.player.musicoo.service.LoginRequiredException
import com.player.musicoo.service.PlayableFormatNotFoundException
import com.player.musicoo.service.UnplayableException
import com.player.musicoo.service.VideoIdMismatchException
import com.player.musicoo.service.ViewModelMain
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.net.CookieHandler
import java.net.CookieManager
@ -84,7 +102,6 @@ object DownloadUtil {
context.applicationContext,
getHttpDataSourceFactory(context.applicationContext)!!
)
val chunkLength = 512 * 1024L
val ringBuffer = RingBuffer<Pair<String, Uri>?>(2) { null }
return ResolvingDataSource.Factory(
CacheDataSource.Factory()
@ -158,31 +175,43 @@ object DownloadUtil {
.use { cursor ->
while (cursor.moveToNext()) {
val download = cursor.download
LogTag.LogD(TAG, "download.request.id->${download.request.id}")
LogTag.LogD(
TAG,
"download formattedSize->${FileSizeConverter(download.bytesDownloaded).formattedSize()}"
)
if (download.request.id == id) {
isExist = true
}
}
}
}
LogTag.LogD(TAG, "isExist->$isExist")
return isExist
}
fun currentDownload(id: String): Download? {
var download: Download? = null
fun getCurrentIdDownloadState(id: String): Int {
if (downloadManager != null) {
downloadManager?.currentDownloads?.map {
if (it.request.id === id) {
download = it
val downloadIndex = downloadManager!!.downloadIndex
downloadIndex.getDownloads()
.use { cursor ->
while (cursor.moveToNext()) {
val download = cursor.download
if (download.request.id == id) {
return download.state
}
}
}
return download
}
return -1
}
fun getDownloadCount(): Int {
var count = 0
if (downloadManager != null) {
val downloadIndex = downloadManager!!.downloadIndex
downloadIndex.getDownloads()
.use { cursor ->
while (cursor.moveToNext()) {
count++
}
}
}
return count
}
@Synchronized
@ -212,13 +241,68 @@ object DownloadUtil {
return databaseProvider
}
private fun buildReadOnlyCacheDataSource(
upstreamFactory: DataSource.Factory, cache: Cache?
): CacheDataSource.Factory {
return CacheDataSource.Factory()
.setCache(cache!!)
.setUpstreamDataSourceFactory(upstreamFactory)
.setCacheWriteDataSinkFactory(null)
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
class TerminalStateNotificationHelper(
private val context: Context,
private val notificationHelper: DownloadNotificationHelper,
private var nextNotificationId: Int
) :
DownloadManager.Listener {
override fun onDownloadChanged(
downloadManager: DownloadManager,
download: Download,
finalException: Exception?
) {
val downloadsMap = mutableMapOf<String, Download>()
downloadsMap[download.request.id] = download
ViewModelMain.modelDownloadsFlow.postValue(downloadsMap)
val notification: Notification = when (download.state) {
Download.STATE_COMPLETED -> {
insertOfflineData(download)
notificationHelper.buildDownloadCompletedNotification(
context,
R.drawable.download_done_icon,
null,
Util.fromUtf8Bytes(download.request.data)
)
}
Download.STATE_FAILED -> {
notificationHelper.buildDownloadFailedNotification(
context,
R.drawable.error,
null,
Util.fromUtf8Bytes(download.request.data)
)
}
else -> {
return
}
}
NotificationUtil.setNotification(context, nextNotificationId++, notification)
}
}
private fun insertOfflineData(download: Download) {
val meController = MediaControllerManager.getController()
if (meController != null && meController.currentMediaItem != null) {
val currentMediaItem = meController.currentMediaItem
val mediaMetadata = currentMediaItem?.mediaMetadata
CoroutineScope(Dispatchers.IO).launch {
val bean = OfflineBean(
videoId = download.request.id,
title = mediaMetadata?.title.toString(),
name = mediaMetadata?.artist.toString(),
thumbnail = mediaMetadata?.artworkUri.toString(),
size = FileSizeConverter(download.bytesDownloaded).formattedSize(),
isOffline = true
)
LogTag.LogD(TAG, "insertOfflineBean bean->${bean}")
App.appOfflineDBManager.insertOfflineBean(bean)
}
}
}
}

View File

@ -8,6 +8,8 @@ object LogTag {
const val VO_API_LOG = "vo-api—log"
const val VO_SERVICE_LOG = "vo-service—log"
const val VO_TEST_ONLY = "vo-only—log"
fun LogD(tag: String, message: String) {
Log.d(tag, message)
}

View File

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/green"
android:pathData="M2.623,8.779C3.339,5.724 5.724,3.339 8.779,2.623C10.898,2.126 13.102,2.126 15.221,2.623C18.276,3.339 20.661,5.724 21.377,8.779C21.874,10.898 21.874,13.102 21.377,15.221C20.661,18.276 18.276,20.661 15.221,21.377C13.102,21.874 10.898,21.874 8.779,21.377C5.724,20.661 3.339,18.276 2.623,15.221C2.126,13.102 2.126,10.898 2.623,8.779L4.083,9.122C3.639,11.015 3.639,12.985 4.083,14.878C4.669,17.378 6.622,19.33 9.122,19.917C11.015,20.361 12.985,20.361 14.878,19.917C17.378,19.33 19.33,17.378 19.917,14.878C20.361,12.985 20.361,11.015 19.917,9.122C19.33,6.622 17.378,4.669 14.878,4.083C12.985,3.639 11.015,3.639 9.122,4.083C6.622,4.669 4.669,6.622 4.083,9.122L2.623,8.779Z" />
<path
android:fillColor="#00000000"
android:pathData="M8.5,12L11,14.5L16,9.5"
android:strokeWidth="2"
android:strokeColor="@color/green"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="54dp"
android:height="54dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M15.698,3.065C17.377,2.805 18.953,3.333 20.229,4.577C21.526,5.841 22.147,7.507 21.97,9.254C21.795,10.979 20.879,12.684 19.338,14.186C18.908,14.606 17.752,15.738 16.093,17.365C15.347,18.097 14.553,18.876 13.753,19.662L12.975,20.425L12.666,20.729C12.488,20.903 12.249,21.001 12,21.001C11.75,21.001 11.511,20.903 11.334,20.729L8.648,18.088L8.08,17.532C6.941,16.417 5.801,15.302 4.662,14.186C3.12,12.684 2.205,10.98 2.03,9.254C1.853,7.507 2.474,5.841 3.771,4.577C5.047,3.333 6.623,2.805 8.302,3.065C9.548,3.257 10.812,3.878 12,4.868C13.189,3.878 14.452,3.257 15.698,3.065H15.698Z"
android:fillColor="@color/white"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="54dp"
android:height="54dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/white"
android:pathData="M12.972,22.844C12.385,22.701 11.834,22.446 11.349,22.094C10.865,21.742 10.458,21.3 10.151,20.794C9.845,20.288 9.645,19.728 9.564,19.146C9.483,18.565 9.522,17.973 9.678,17.407C9.835,16.84 10.106,16.309 10.476,15.845C10.847,15.38 11.308,14.992 11.834,14.703C12.361,14.413 12.941,14.228 13.542,14.158C14.143,14.088 14.752,14.135 15.334,14.296C16.118,14.498 16.835,14.895 17.412,15.448L19.193,8.997L19.435,8.12C19.486,7.935 19.586,7.766 19.725,7.629C19.864,7.492 20.037,7.393 20.227,7.34C20.417,7.288 20.618,7.284 20.811,7.329C21.003,7.375 21.18,7.467 21.324,7.599L23.641,9.714C23.791,9.851 23.9,10.026 23.957,10.218C24.013,10.411 24.014,10.615 23.961,10.809C23.907,11.002 23.801,11.178 23.653,11.318C23.504,11.457 23.32,11.555 23.119,11.602L20.714,12.15L18.477,20.267L18.443,20.258C18.094,21.072 17.503,21.767 16.746,22.256C15.989,22.744 15.099,23.003 14.19,23C13.778,23 13.369,22.947 12.972,22.844ZM8.582,20.195C6.203,19.876 4.024,18.733 2.447,16.977C0.869,15.221 0,12.971 0,10.642C-0,8.735 0.583,6.871 1.675,5.285C2.767,3.7 4.32,2.464 6.136,1.734C7.953,1.004 9.951,0.813 11.88,1.185C13.808,1.557 15.579,2.476 16.969,3.824C17.571,4.408 18.094,5.064 18.525,5.776C18.512,5.809 18.5,5.844 18.491,5.881L18.139,7.152L16.447,13.273C15.977,13.069 15.481,12.927 14.971,12.851C15.297,12.158 15.465,11.405 15.463,10.643C15.463,9.627 15.165,8.632 14.604,7.774C14.043,6.917 13.242,6.231 12.295,5.798C11.348,5.365 10.293,5.203 9.254,5.329C8.215,5.455 7.234,5.866 6.427,6.512C5.619,7.159 5.017,8.014 4.692,8.98C4.367,9.945 4.331,10.981 4.59,11.965C4.848,12.949 5.389,13.842 6.151,14.539C6.913,15.236 7.863,15.709 8.891,15.902C8.489,16.68 8.281,17.539 8.284,18.41C8.283,19.016 8.384,19.619 8.582,20.195H8.582Z" />
</vector>

View File

@ -212,11 +212,16 @@
android:textSize="12dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/likeAndDownloadLayout"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center">
<RelativeLayout
android:id="@+id/favoritesBtn"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center_vertical"
android:gravity="center">
<ImageView
@ -230,7 +235,6 @@
android:id="@+id/downloadBtn"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center_vertical"
android:gravity="center">
<ImageView
@ -241,22 +245,24 @@
<ProgressBar
android:id="@+id/downloadLoading"
android:visibility="gone"
android:layout_width="24dp"
android:layout_height="24dp"
android:indeterminateTint="@color/green"
android:progressBackgroundTint="@color/green"
android:progressTint="@color/green" />
android:progressTint="@color/green"
android:visibility="gone" />
</RelativeLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:gravity="center_vertical"
android:orientation="horizontal">

View File

@ -0,0 +1,146 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/main_bg_color"
android:orientation="vertical">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@mipmap/settings_bg_img" />
<View
android:id="@+id/view"
android:layout_width="match_parent"
android:layout_height="0dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/view"
android:orientation="vertical">
<LinearLayout
android:id="@+id/title_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:orientation="horizontal">
<RelativeLayout
android:id="@+id/back_btn"
android:layout_width="42dp"
android:layout_height="42dp"
android:background="@drawable/drw_back_bg">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:src="@drawable/back_icon" />
</RelativeLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginEnd="58dp"
android:fontFamily="@font/medium_font"
android:gravity="center"
android:text="@string/offline_songs"
android:textColor="@color/white"
android:textSize="18dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/loadingLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:visibility="gone">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminateTint="@color/green"
android:progressBackgroundTint="@color/green"
android:progressTint="@color/green" />
</LinearLayout>
<LinearLayout
android:id="@+id/no_content_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">
<ImageView
android:id="@+id/no_content_iv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/no_content_img" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:fontFamily="@font/medium_font"
android:gravity="center"
android:text="@string/content_loading_failed"
android:textSize="14dp" />
<TextView
android:id="@+id/tryAgainBtn"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_marginTop="16dp"
android:background="@drawable/drw_btn_bg"
android:fontFamily="@font/medium_font"
android:gravity="center"
android:paddingStart="32dp"
android:paddingEnd="32dp"
android:text="@string/try_again"
android:textColor="@color/black"
android:textSize="16dp" />
</LinearLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/play_music_layout"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:overScrollMode="never"
android:scrollbars="none" />
</LinearLayout>
<LinearLayout
android:id="@+id/play_music_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:orientation="vertical" />
</RelativeLayout>
</LinearLayout>
</RelativeLayout>

View File

@ -63,8 +63,9 @@
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center_vertical"
android:orientation="vertical">
@ -90,6 +91,17 @@
</LinearLayout>
<RelativeLayout
android:id="@+id/downloadBtn"
android:layout_width="40dp"
android:layout_height="40dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:src="@drawable/download_icon" />
</RelativeLayout>
</LinearLayout>
</LinearLayout>

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -15,7 +16,161 @@
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:layout_marginTop="28dp"
android:layout_marginEnd="18dp"
android:fontFamily="@font/medium_font"
android:text="@string/library"
android:textColor="@color/white"
android:textSize="32dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="14dp"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/likedSongsBtn"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="0dp"
app:cardBackgroundColor="@color/green_60"
app:cardCornerRadius="10dp"
app:cardElevation="0dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:padding="24dp"
android:src="@drawable/library_liked_icon" />
</androidx.cardview.widget.CardView>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:fontFamily="@font/medium_font"
android:text="@string/liked_songs"
android:textColor="@color/white"
android:textSize="14dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/likedSongsTv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/regular_font"
android:text="0"
android:textColor="@color/white_60"
android:textSize="12dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:fontFamily="@font/regular_font"
android:text="@string/songs"
android:textColor="@color/white_60"
android:textSize="12dp" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/offlineSongsBtn"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="0dp"
app:cardBackgroundColor="@color/offline_bg_color_60"
app:cardCornerRadius="10dp"
app:cardElevation="0dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:padding="24dp"
android:src="@drawable/library_offline_icon" />
</androidx.cardview.widget.CardView>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:fontFamily="@font/medium_font"
android:text="@string/offline_songs"
android:textColor="@color/white"
android:textSize="14dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/offlineSongsTv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/regular_font"
android:text="0"
android:textColor="@color/white_60"
android:textSize="12dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:fontFamily="@font/regular_font"
android:text="@string/songs"
android:textColor="@color/white_60"
android:textSize="12dp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:layout_marginEnd="18dp"
android:fontFamily="@font/medium_font"
android:text="@string/new_playlist"
android:textColor="@color/white"
android:textSize="20dp" />
</LinearLayout>
</RelativeLayout>

View File

@ -0,0 +1,116 @@
<?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">
<RelativeLayout
android:layout_width="48dp"
android:layout_height="48dp">
<ImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@mipmap/musicoo_logo_img" />
<RelativeLayout
android:id="@+id/listPlayView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black_60"
android:visibility="gone">
<com.player.musicoo.view.MusicBarsView
android:layout_width="wrap_content"
android:layout_height="16dp"
android:layout_centerInParent="true" />
</RelativeLayout>
</RelativeLayout>
</androidx.cardview.widget.CardView>
</RelativeLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="vertical">
<com.player.musicoo.view.MarqueeTextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/medium_font"
android:maxLines="1"
android:text="@string/app_name"
android:textColor="@color/white"
android:textSize="14dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:layout_marginTop="4dp"
android:orientation="horizontal">
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@drawable/download_green_done_icon" />
<TextView
android:id="@+id/size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:fontFamily="@font/regular_font"
android:text="@string/app_name"
android:textColor="@color/white_60"
android:textSize="12dp"/>
<com.player.musicoo.view.MarqueeTextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="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>
</LinearLayout>

View File

@ -9,6 +9,8 @@
<color name="main_bg_color">#151718</color>
<color name="green">#FF80F988</color>
<color name="green_60">#9980F988</color>
<color name="offline_bg_color">#FFFC746F</color>
<color name="offline_bg_color_60">#99FC746F</color>
<color name="bottom_layout_bg_color">#1A1A1A</color>
<color name="transparent">#00000000</color>
</resources>

View File

@ -29,4 +29,11 @@
<string name="more">More</string>
<string name="download">Download</string>
<string name="downloads">Downloads</string>
<string name="library">Library</string>
<string name="liked_songs">Liked songs</string>
<string name="offline_songs">Offline songs</string>
<string name="songs">Songs</string>
<string name="new_playlist">New playlist</string>
<string name="liked_songs_no_data_prompt">You haven\'t liked any songs yet.</string>
<string name="offline_songs_no_data_prompt">You haven\'t saved any songs for offline listening yet.</string>
</resources>