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("org.jetbrains.kotlin.android")
id("kotlin-kapt") id("kotlin-kapt")
id("org.jetbrains.kotlin.plugin.serialization") id("org.jetbrains.kotlin.plugin.serialization")
id("kotlin-android")
} }
android { android {
@ -58,6 +59,9 @@ dependencies {
implementation("com.google.android.material:material:1.11.0") implementation("com.google.android.material:material:1.11.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.media3:media3-session:1.3.1") 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") testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.ext:junit:1.1.5")

View File

@ -66,6 +66,9 @@
<activity <activity
android:name=".activity.MoSingerMoreSongActivity" android:name=".activity.MoSingerMoreSongActivity"
android:screenOrientation="portrait" /> android:screenOrientation="portrait" />
<activity
android:name=".activity.MoOfflineSongsActivity"
android:screenOrientation="portrait" />
<service <service
android:name=".service.PlaybackService" android:name=".service.PlaybackService"
@ -83,8 +86,8 @@
android:foregroundServiceType="dataSync" android:foregroundServiceType="dataSync"
tools:ignore="ForegroundServicePermission"> tools:ignore="ForegroundServicePermission">
<intent-filter> <intent-filter>
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART"/> <action android:name="androidx.media3.exoplayer.downloadService.action.RESTART" />
<category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.DEFAULT" />
</intent-filter> </intent-filter>
</service> </service>

View File

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

View File

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

View File

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

View File

@ -51,8 +51,8 @@ class DetailsListAdapter(
) )
intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_PLAY_PARAMS, bean.params) intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_PLAY_PARAMS, bean.params)
intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_NAME, bean.name) intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_NAME, bean.title)
intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_DESC, bean.title) intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_DESC, bean.name)
context.startActivity(intent) 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 package com.player.musicoo.fragment
import android.content.Intent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast
import com.gyf.immersionbar.ktx.immersionBar 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.databinding.FragmentMoMeBinding
import com.player.musicoo.innertube.utils.BrotliEncoder.decode
import com.player.musicoo.util.DownloadUtil
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select import kotlinx.coroutines.selects.select
@ -38,13 +43,39 @@ class MoMeFragment : MoBaseFragment<FragmentMoMeBinding>() {
requests.onReceive { requests.onReceive {
} }
events.onReceive {
when (it) {
Event.FragmentOnResume -> {
fragmentOnResume()
}
}
}
} }
} }
} }
private fun initView() { 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() { override fun onResume() {
super.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.DownloadNotificationHelper
import androidx.media3.exoplayer.offline.DownloadService import androidx.media3.exoplayer.offline.DownloadService
import androidx.media3.exoplayer.scheduler.PlatformScheduler import androidx.media3.exoplayer.scheduler.PlatformScheduler
import com.player.musicoo.App
import com.player.musicoo.R import com.player.musicoo.R
import com.player.musicoo.bean.OfflineBean
import com.player.musicoo.util.DownloadUtil import com.player.musicoo.util.DownloadUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
@ -37,8 +42,9 @@ class MyDownloadService : DownloadService(
val downloadManager = DownloadUtil.getDownloadManager(this) val downloadManager = DownloadUtil.getDownloadManager(this)
downloadManager!!.maxParallelDownloads = 3 downloadManager!!.maxParallelDownloads = 3
val downloadNotificationHelper = DownloadUtil.getDownloadNotificationHelper(this) val downloadNotificationHelper = DownloadUtil.getDownloadNotificationHelper(this)
downloadManager.addListener( downloadManager.addListener(
TerminalStateNotificationHelper( DownloadUtil.TerminalStateNotificationHelper(
this, downloadNotificationHelper!!, FOREGROUND_NOTIFICATION_ID + 1 this, downloadNotificationHelper!!, FOREGROUND_NOTIFICATION_ID + 1
) )
) )
@ -63,43 +69,4 @@ class MyDownloadService : DownloadService(
notMetRequirements 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 package com.player.musicoo.util
import android.app.Notification
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.util.Log
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.lifecycle.MutableLiveData
import androidx.media3.common.PlaybackException import androidx.media3.common.PlaybackException
import androidx.media3.common.util.NotificationUtil
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.common.util.Util
import androidx.media3.database.DatabaseProvider import androidx.media3.database.DatabaseProvider
import androidx.media3.database.StandaloneDatabaseProvider import androidx.media3.database.StandaloneDatabaseProvider
import androidx.media3.datasource.DataSource 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.Download
import androidx.media3.exoplayer.offline.DownloadManager import androidx.media3.exoplayer.offline.DownloadManager
import androidx.media3.exoplayer.offline.DownloadNotificationHelper 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
import com.player.musicoo.innertube.Innertube.TAG import com.player.musicoo.innertube.Innertube.TAG
import com.player.musicoo.innertube.models.bodies.PlayerBody import com.player.musicoo.innertube.models.bodies.PlayerBody
import com.player.musicoo.innertube.requests.player import com.player.musicoo.innertube.requests.player
import com.player.musicoo.media.MediaControllerManager
import com.player.musicoo.service.LoginRequiredException import com.player.musicoo.service.LoginRequiredException
import com.player.musicoo.service.PlayableFormatNotFoundException import com.player.musicoo.service.PlayableFormatNotFoundException
import com.player.musicoo.service.UnplayableException import com.player.musicoo.service.UnplayableException
import com.player.musicoo.service.VideoIdMismatchException import com.player.musicoo.service.VideoIdMismatchException
import com.player.musicoo.service.ViewModelMain
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers 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 kotlinx.coroutines.runBlocking
import java.net.CookieHandler import java.net.CookieHandler
import java.net.CookieManager import java.net.CookieManager
@ -84,7 +102,6 @@ object DownloadUtil {
context.applicationContext, context.applicationContext,
getHttpDataSourceFactory(context.applicationContext)!! getHttpDataSourceFactory(context.applicationContext)!!
) )
val chunkLength = 512 * 1024L
val ringBuffer = RingBuffer<Pair<String, Uri>?>(2) { null } val ringBuffer = RingBuffer<Pair<String, Uri>?>(2) { null }
return ResolvingDataSource.Factory( return ResolvingDataSource.Factory(
CacheDataSource.Factory() CacheDataSource.Factory()
@ -158,31 +175,43 @@ object DownloadUtil {
.use { cursor -> .use { cursor ->
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
val download = cursor.download 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) { if (download.request.id == id) {
isExist = true isExist = true
} }
} }
} }
} }
LogTag.LogD(TAG, "isExist->$isExist")
return isExist return isExist
} }
fun currentDownload(id: String): Download? { fun getCurrentIdDownloadState(id: String): Int {
var download: Download? = null
if (downloadManager != null) { if (downloadManager != null) {
downloadManager?.currentDownloads?.map { val downloadIndex = downloadManager!!.downloadIndex
if (it.request.id === id) { downloadIndex.getDownloads()
download = it .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 @Synchronized
@ -212,13 +241,68 @@ object DownloadUtil {
return databaseProvider return databaseProvider
} }
private fun buildReadOnlyCacheDataSource( class TerminalStateNotificationHelper(
upstreamFactory: DataSource.Factory, cache: Cache? private val context: Context,
): CacheDataSource.Factory { private val notificationHelper: DownloadNotificationHelper,
return CacheDataSource.Factory() private var nextNotificationId: Int
.setCache(cache!!) ) :
.setUpstreamDataSourceFactory(upstreamFactory) DownloadManager.Listener {
.setCacheWriteDataSinkFactory(null)
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR) 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_API_LOG = "vo-api—log"
const val VO_SERVICE_LOG = "vo-service—log" const val VO_SERVICE_LOG = "vo-service—log"
const val VO_TEST_ONLY = "vo-only—log"
fun LogD(tag: String, message: String) { fun LogD(tag: String, message: String) {
Log.d(tag, message) 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,51 +212,57 @@
android:textSize="12dp" /> android:textSize="12dp" />
</LinearLayout> </LinearLayout>
<RelativeLayout <LinearLayout
android:id="@+id/favoritesBtn" android:id="@+id/likeAndDownloadLayout"
android:layout_width="40dp" android:layout_width="wrap_content"
android:layout_height="40dp" android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:gravity="center"> android:gravity="center">
<ImageView <RelativeLayout
android:id="@+id/favoritesImg" android:id="@+id/favoritesBtn"
android:layout_width="24dp" android:layout_width="40dp"
android:layout_height="24dp" android:layout_height="40dp"
android:src="@drawable/not_favorited_icon" /> android:gravity="center">
</RelativeLayout>
<RelativeLayout <ImageView
android:id="@+id/downloadBtn" android:id="@+id/favoritesImg"
android:layout_width="40dp" android:layout_width="24dp"
android:layout_height="40dp" android:layout_height="24dp"
android:layout_gravity="center_vertical" android:src="@drawable/not_favorited_icon" />
android:gravity="center"> </RelativeLayout>
<ImageView <RelativeLayout
android:id="@+id/downloadImg" android:id="@+id/downloadBtn"
android:layout_width="24dp" android:layout_width="40dp"
android:layout_height="24dp" android:layout_height="40dp"
android:src="@drawable/download_icon" /> android:gravity="center">
<ProgressBar <ImageView
android:id="@+id/downloadLoading" android:id="@+id/downloadImg"
android:visibility="gone" android:layout_width="24dp"
android:layout_width="24dp" android:layout_height="24dp"
android:layout_height="24dp" android:src="@drawable/download_icon" />
android:indeterminateTint="@color/green"
android:progressBackgroundTint="@color/green" <ProgressBar
android:progressTint="@color/green" /> android:id="@+id/downloadLoading"
android:layout_width="24dp"
android:layout_height="24dp"
android:indeterminateTint="@color/green"
android:progressBackgroundTint="@color/green"
android:progressTint="@color/green"
android:visibility="gone" />
</RelativeLayout>
</LinearLayout>
</RelativeLayout>
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal"> 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> </RelativeLayout>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="vertical"> android:orientation="vertical">
@ -90,6 +91,17 @@
</LinearLayout> </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>
</LinearLayout> </LinearLayout>

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <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_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@ -15,7 +16,161 @@
android:orientation="vertical"> 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> </LinearLayout>
</RelativeLayout> </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="main_bg_color">#151718</color>
<color name="green">#FF80F988</color> <color name="green">#FF80F988</color>
<color name="green_60">#9980F988</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="bottom_layout_bg_color">#1A1A1A</color>
<color name="transparent">#00000000</color> <color name="transparent">#00000000</color>
</resources> </resources>

View File

@ -29,4 +29,11 @@
<string name="more">More</string> <string name="more">More</string>
<string name="download">Download</string> <string name="download">Download</string>
<string name="downloads">Downloads</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> </resources>