This commit is contained in:
ocean 2024-05-10 16:56:04 +08:00
parent 619a588559
commit 195ec0f242
18 changed files with 594 additions and 259 deletions

View File

@ -3,15 +3,17 @@ package com.player.musicoo.activity
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.CountDownTimer import android.os.CountDownTimer
import android.util.Log
import com.gyf.immersionbar.ktx.immersionBar import com.gyf.immersionbar.ktx.immersionBar
import com.player.musicoo.databinding.ActivityLaunchBinding import com.player.musicoo.databinding.ActivityLaunchBinding
import com.player.musicoo.util.LogTag
class LaunchActivity : BaseActivity() { class LaunchActivity : BaseActivity() {
private lateinit var binding: ActivityLaunchBinding private lateinit var binding: ActivityLaunchBinding
private val totalTime = 3000 // 5秒 private val totalTime = 3000L // 5秒
private val interval = 50 // 更新间隔,毫秒 private val interval = 50L // 更新间隔,毫秒
private val steps = totalTime / interval private val steps = totalTime / interval
private val progressPerStep = 100 / steps private val progressPerStep = 100f / steps.toFloat()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityLaunchBinding.inflate(layoutInflater) binding = ActivityLaunchBinding.inflate(layoutInflater)
@ -26,13 +28,13 @@ class LaunchActivity : BaseActivity() {
private fun initTimer() { private fun initTimer() {
val progressBar = binding.customProgressBar val progressBar = binding.customProgressBar
val timer = object : CountDownTimer(totalTime.toLong(), interval.toLong()) { val timer = object : CountDownTimer(totalTime, interval) {
override fun onTick(millisUntilFinished: Long) { override fun onTick(millisUntilFinished: Long) {
progressBar.setProgress(progressBar.getProgress() + progressPerStep) progressBar.setProgress(progressBar.getProgress() + progressPerStep)
} }
override fun onFinish() { override fun onFinish() {
progressBar.setProgress(100) progressBar.setProgress(100f)
toMainActivity() toMainActivity()
} }
} }

View File

@ -17,6 +17,7 @@ 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.player.musicoo.R import com.player.musicoo.R
import com.player.musicoo.media.MediaControllerManager
import com.player.musicoo.sp.AppStore import com.player.musicoo.sp.AppStore
import com.player.musicoo.util.LogTag import com.player.musicoo.util.LogTag
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -40,7 +41,7 @@ abstract class MoBaseActivity : AppCompatActivity(), CoroutineScope by MainScope
protected val TAG = LogTag.VO_ACT_LOG protected val TAG = LogTag.VO_ACT_LOG
protected val appStore by lazy { AppStore(this) } protected val appStore by lazy { AppStore(this) }
protected val events = Channel<Event>(Channel.UNLIMITED) protected val events = Channel<Event>(Channel.UNLIMITED)
protected val meController = MediaControllerManager.getController()
protected abstract suspend fun main() protected abstract suspend fun main()
private var defer: suspend () -> Unit = {} private var defer: suspend () -> Unit = {}
private var deferRunning = false private var deferRunning = false

View File

@ -1,6 +1,5 @@
package com.player.musicoo.activity package com.player.musicoo.activity
import android.util.Log
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.gyf.immersionbar.ktx.immersionBar import com.gyf.immersionbar.ktx.immersionBar
@ -8,6 +7,7 @@ import com.player.musicoo.adapter.DetailsListAdapter
import com.player.musicoo.databinding.ActivityDetailsBinding import com.player.musicoo.databinding.ActivityDetailsBinding
import com.player.musicoo.innertube.Innertube import com.player.musicoo.innertube.Innertube
import com.player.musicoo.innertube.requests.moPlaylistPage import com.player.musicoo.innertube.requests.moPlaylistPage
import com.player.musicoo.util.LogTag.LogD
class MoListDetailsActivity : MoBaseActivity() { class MoListDetailsActivity : MoBaseActivity() {
@ -27,7 +27,7 @@ class MoListDetailsActivity : MoBaseActivity() {
return return
} }
initView() initView()
Log.d(TAG, "browseId->${browseId}") LogD(TAG, "browseId->${browseId}")
initData(browseId) initData(browseId)
} }
@ -61,7 +61,7 @@ class MoListDetailsActivity : MoBaseActivity() {
binding.rv.adapter = adapter binding.rv.adapter = adapter
}?.onFailure { }?.onFailure {
Log.d(TAG, "moPlaylistPage onFailure->${it}") LogD(TAG, "moPlaylistPage onFailure->${it}")
} }
} }
} }

View File

@ -3,33 +3,26 @@ package com.player.musicoo.activity
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.os.Message import android.os.Message
import android.util.Log
import android.view.View import android.view.View
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.PlaybackException import androidx.media3.common.PlaybackException
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.session.MediaController
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.gyf.immersionbar.ktx.immersionBar import com.gyf.immersionbar.ktx.immersionBar
import com.player.musicoo.R import com.player.musicoo.R
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.innertube.models.bodies.PlayerBody
import com.player.musicoo.innertube.requests.player
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.LoginRequiredException
import com.player.musicoo.service.PlayableFormatNotFoundException
import com.player.musicoo.service.UnplayableException
import com.player.musicoo.service.VideoIdMismatchException
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.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.selects.select
import com.player.musicoo.util.LogTag.LogD
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener { class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
@ -41,12 +34,15 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
const val PLAY_DETAILS_PLAY_PARAMS = "play_details_play_params" const val PLAY_DETAILS_PLAY_PARAMS = "play_details_play_params"
const val PLAY_DETAILS_NAME = "play_details_name" const val PLAY_DETAILS_NAME = "play_details_name"
const val PLAY_DETAILS_DESC = "play_details_desc" const val PLAY_DETAILS_DESC = "play_details_desc"
const val PLAY_DETAILS_COME_FROM = "PLAY_DETAILS_COME_FROM"
} }
private lateinit var binding: ActivityMoPlayDetailsBinding private lateinit var binding: ActivityMoPlayDetailsBinding
private var currentVideoID = "" private var currentVideoID = ""
private var comeFrom: Class<*>? = null
private fun initImmersionBar() { private fun initImmersionBar() {
immersionBar { immersionBar {
statusBarDarkFont(false) statusBarDarkFont(false)
@ -59,77 +55,146 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
setContentView(binding.root) setContentView(binding.root)
initImmersionBar() initImmersionBar()
initClick() initClick()
initPlayerListener()
val videoId = intent.getStringExtra(PLAY_DETAILS_VIDEO_ID) val videoId = intent.getStringExtra(PLAY_DETAILS_VIDEO_ID)
Log.d(TAG, "MoPlayDetailsActivity main videoId->$videoId")
val playlistId = intent.getStringExtra(PLAY_DETAILS_PLAY_LIST_ID) val playlistId = intent.getStringExtra(PLAY_DETAILS_PLAY_LIST_ID)
Log.d(TAG, "MoPlayDetailsActivity main playlistId->$playlistId")
val playlistSetVideoId = intent.getStringExtra(PLAY_DETAILS_PLAY_LIST_SET_VIDEO_ID) val playlistSetVideoId = intent.getStringExtra(PLAY_DETAILS_PLAY_LIST_SET_VIDEO_ID)
Log.d(TAG, "MoPlayDetailsActivity main playlistSetVideoId->$playlistSetVideoId")
val params = intent.getStringExtra(PLAY_DETAILS_PLAY_PARAMS) val params = intent.getStringExtra(PLAY_DETAILS_PLAY_PARAMS)
binding.nameTv.text = intent.getStringExtra(PLAY_DETAILS_NAME) comeFrom = intent.getSerializableExtra(PLAY_DETAILS_COME_FROM) as Class<*>?
binding.descTv.text = intent.getStringExtra(PLAY_DETAILS_DESC) if (comeFrom != null && comeFrom == PrimaryActivity::class.java) {
// 处理来自 PrimaryActivity 的情况
updateCurrentMediaItemInfo()
} else {
binding.nameTv.text = intent.getStringExtra(PLAY_DETAILS_NAME)
binding.descTv.text = intent.getStringExtra(PLAY_DETAILS_DESC)
if (videoId.isNullOrEmpty()) { if (videoId.isNullOrEmpty()) {
finish() finish()
return return
}
//传入进来的ID就是进入此界面的当前ID
currentVideoID = videoId
//根据进来界面的当前ID来获取资源。
initData(
videoId,
playlistId,
playlistSetVideoId,
params
)
}
onReceive()
}
private suspend fun onReceive() {
while (isActive) {
select<Unit> {
events.onReceive {
when (it) {
Event.ActivityOnResume -> {
activityOnResume()
}
else -> {}
}
}
}
}
}
private fun activityOnResume() {
if (comeFrom != null && comeFrom == PrimaryActivity::class.java) {
if (meController != null && meController.currentMediaItem != null) {
updateInfoUi(meController.currentMediaItem)
}
}
}
private fun initPlayerListener() {
meController?.addListener(playerListener)
}
private fun updateCurrentMediaItemInfo() {
if (meController != null && meController.currentMediaItem != null) {
binding.playbackErrorLayout.visibility = View.GONE
binding.loadingView.visibility = View.GONE
val currentString = convertMillisToMinutesAndSecondsString(meController.currentPosition)
binding.progressDurationTv.text = currentString
if (MediaControllerManager.getDuration() > 0) {
binding.totalDurationTv.visibility = View.VISIBLE
} else {
binding.totalDurationTv.visibility = View.GONE
}
binding.totalDurationTv.text =
convertMillisToMinutesAndSecondsString(MediaControllerManager.getDuration())
binding.sbProgress.value = meController.currentPosition.toFloat()
binding.sbProgress.valueTo = MediaControllerManager.getDuration().toFloat()
updateProgressState()
binding.progressBar.progress = meController.bufferedPosition.toInt()
binding.progressBar.max = MediaControllerManager.getDuration().toInt()
updateProgressBufferingState()
} }
//传入进来的ID就是进入此界面的当前ID
currentVideoID = videoId
//根据进来界面的当前ID来获取资源。
initData(
videoId,
playlistId,
playlistSetVideoId,
params
)
} }
private fun initClick() { private fun initClick() {
val currentPlayer = MediaControllerManager.getController()
binding.disableClicksLayout.setOnClickListener { }
binding.backBtn.setOnClickListener { binding.backBtn.setOnClickListener {
finish() finish()
} }
binding.playLayoutBtn.setOnClickListener { binding.playLayoutBtn.setOnClickListener {
if (currentPlayer != null) { if (meController != null) {
if (currentPlayer.isPlaying) { if (meController.isPlaying) {
currentPlayer.pause() meController.pause()
updatePlayState(false) updatePlayState(false)
} else { } else {
currentPlayer.play() meController.play()
updatePlayState(true) updatePlayState(true)
} }
updateProgressState() updateProgressState()
} }
} }
binding.playSkipBackBtn.setOnClickListener { binding.playSkipBackBtn.setOnClickListener {
if (currentPlayer != null) { if (meController != null) {
currentPlayer.seekToPreviousMediaItem() meController.seekToPreviousMediaItem()
updateProgressUi() updateProgressUi()
updateInfoUi(currentPlayer.currentMediaItem) updateInfoUi(meController.currentMediaItem)
updateProgressState()
if (!meController.isPlaying) {
meController.prepare()
meController.play()
}
} }
} }
binding.playSkipForwardBtn.setOnClickListener { binding.playSkipForwardBtn.setOnClickListener {
if (currentPlayer != null) { if (meController != null) {
currentPlayer.seekToNextMediaItem() meController.seekToNextMediaItem()
updateProgressUi() updateProgressUi()
updateInfoUi(currentPlayer.currentMediaItem) updateInfoUi(meController.currentMediaItem)
updateProgressState()
if (!meController.isPlaying) {
meController.prepare()
meController.play()
}
} }
} }
binding.listLayoutBtn.setOnClickListener { binding.listLayoutBtn.setOnClickListener {
Log.d(TAG, "currentPlayer?.mediaItemCount->${currentPlayer?.mediaItemCount}") LogD(TAG, "meController?.mediaItemCount->${meController?.mediaItemCount}")
} }
binding.progressBar.progress = 0 binding.progressBar.progress = 0
binding.sbProgress.value = 0f binding.sbProgress.value = 0f
binding.sbProgress.addOnChangeListener { slider, value, fromUser -> binding.sbProgress.addOnChangeListener { slider, value, fromUser ->
if (fromUser) { if (fromUser) {
if (currentPlayer != null) { if (meController != null) {
currentPlayer.seekTo(value.toLong()) meController.seekTo(value.toLong())
val ss = currentPlayer.isPlaying val ss = meController.isPlaying
if (!ss) { if (!ss) {
currentPlayer.play() meController.play()
} }
} }
} }
@ -149,12 +214,11 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
parameters parameters
).let { ).let {
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
val ss = it.process()//获取到的资源集合 val songRadioList = it.process()//获取到的资源集合
if (ss.isEmpty()) {//集合为空则展示错误提示 if (songRadioList.isEmpty()) {//集合为空则展示错误提示
binding.loadingView.visibility = View.GONE binding.loadingView.visibility = View.GONE
binding.playbackErrorLayout.visibility = View.VISIBLE binding.playbackErrorLayout.visibility = View.VISIBLE
binding.disableClicksLayout.visibility = View.VISIBLE
} }
if (isFinishing) { if (isFinishing) {
@ -162,9 +226,7 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
} }
var mediaItem: MediaItem? = null var mediaItem: MediaItem? = null
Log.d(TAG, "size = ${ss.size}") for (song: Innertube.SongItem in songRadioList) {
Log.d(TAG, "MoPlayDetailsActivity initData currentVideoID->$currentVideoID")
for (song: Innertube.SongItem in ss) {
if (song.key == currentVideoID) {//判断当前ID得到一个mediaItem if (song.key == currentVideoID) {//判断当前ID得到一个mediaItem
mediaItem = song.asMediaItem mediaItem = song.asMediaItem
break break
@ -175,7 +237,6 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
updateInfoUi(mediaItem) updateInfoUi(mediaItem)
binding.playbackErrorLayout.visibility = View.GONE binding.playbackErrorLayout.visibility = View.GONE
binding.disableClicksLayout.visibility = View.GONE
binding.totalDurationTv.visibility = View.GONE binding.totalDurationTv.visibility = View.GONE
@ -190,18 +251,18 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
) )
.build() .build()
val meController = MediaControllerManager.getController()
meController?.let { meController?.let {
it.addListener(playerListener)
it.setMediaItem(newMediaItem, true) it.setMediaItem(newMediaItem, true)
it.prepare() it.prepare()
it.play() it.play()
it.addMediaItems(ss.map(Innertube.SongItem::asMediaItem).drop(1)) //过滤掉进入页面的id得到集合addMediaItems
val mediaItems = songRadioList.map(Innertube.SongItem::asMediaItem)
.filter { filter -> filter.mediaId != videoId }
it.addMediaItems(mediaItems)
} }
} else { } else {
binding.playbackErrorLayout.visibility = View.VISIBLE binding.playbackErrorLayout.visibility = View.VISIBLE
binding.disableClicksLayout.visibility = View.VISIBLE
} }
} }
} }
@ -213,29 +274,30 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
newPosition: Player.PositionInfo, newPosition: Player.PositionInfo,
reason: Int reason: Int
) { ) {
if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) { //保证自动播放完毕当前歌曲与通知切换歌曲可以更新UI信息
val meController = MediaControllerManager.getController() if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION
|| reason == Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT
) {
updateInfoUi(meController?.currentMediaItem) updateInfoUi(meController?.currentMediaItem)
} }
} }
override fun onPlayerError(error: PlaybackException) { override fun onPlayerError(error: PlaybackException) {
Log.d(TAG, "onPlayerError error= $error") LogD(TAG, "onPlayerError error= $error")
binding.playbackErrorLayout.visibility = View.VISIBLE//展示错误提示 binding.playbackErrorLayout.visibility = View.VISIBLE//展示错误提示
updatePlayState(false)
} }
override fun onPlayWhenReadyChanged( override fun onPlayWhenReadyChanged(
playWhenReady: Boolean, playWhenReady: Boolean,
reason: Int reason: Int
) { ) {
Log.d(TAG, "onPlayWhenReadyChanged = $playWhenReady")
updatePlayState(playWhenReady) updatePlayState(playWhenReady)
updateProgressState() updateProgressState()
} }
override fun onPlaybackStateChanged(playbackState: Int) { override fun onPlaybackStateChanged(playbackState: Int) {
Log.d(TAG, "onPlaybackStateChanged = $playbackState")
when (playbackState) { when (playbackState) {
Player.STATE_BUFFERING -> { Player.STATE_BUFFERING -> {
binding.loadingView.visibility = View.VISIBLE binding.loadingView.visibility = View.VISIBLE
@ -266,13 +328,14 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
MediaControllerManager.getController()?.removeListener(playerListener) meController?.removeListener(playerListener)
} }
private fun updateProgressUi() { private fun updateProgressUi() {
binding.sbProgress.value = 0f binding.sbProgress.value = 0f
binding.progressBar.progress = 0 binding.progressBar.progress = 0
binding.progressDurationTv.text = convertMillisToMinutesAndSecondsString(0L) binding.progressDurationTv.text = convertMillisToMinutesAndSecondsString(0L)
binding.totalDurationTv.visibility = View.GONE
} }
private fun updateInfoUi(mediaItem: MediaItem?) { private fun updateInfoUi(mediaItem: MediaItem?) {
@ -292,10 +355,9 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
* 更新播放进度 * 更新播放进度
*/ */
private fun updateProgressState() { private fun updateProgressState() {
val currentPlayer = MediaControllerManager.getController()
//判断是否ready与播放中否则停止更新进度 //判断是否ready与播放中否则停止更新进度
if (currentPlayer != null && currentPlayer.playbackState == Player.STATE_READY && currentPlayer.isPlaying) { if (meController != null && meController.playbackState == Player.STATE_READY && meController.isPlaying) {
updatePlayState(currentPlayer.isPlaying) updatePlayState(meController.isPlaying)
progressHandler.removeCallbacksAndMessages(null) progressHandler.removeCallbacksAndMessages(null)
progressHandler.sendEmptyMessage(1) progressHandler.sendEmptyMessage(1)
} else { } else {
@ -308,14 +370,13 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
*/ */
private val progressHandler = object : Handler(Looper.myLooper()!!) { private val progressHandler = object : Handler(Looper.myLooper()!!) {
override fun handleMessage(msg: Message) { override fun handleMessage(msg: Message) {
val currentPlayer = MediaControllerManager.getController()
//判断是否ready与播放中否则停止更新进度 //判断是否ready与播放中否则停止更新进度
if (currentPlayer != null && currentPlayer.playbackState == Player.STATE_READY && currentPlayer.isPlaying) { if (meController != null && meController.playbackState == Player.STATE_READY && meController.isPlaying) {
val currentPosition = currentPlayer.currentPosition val currentPosition = meController.currentPosition
val currentString = convertMillisToMinutesAndSecondsString(currentPosition) val currentString = convertMillisToMinutesAndSecondsString(currentPosition)
binding.progressDurationTv.text = currentString binding.progressDurationTv.text = currentString
val currentBufferedPosition = currentPlayer.bufferedPosition val currentBufferedPosition = meController.bufferedPosition
binding.progressBar.progress = currentBufferedPosition.toInt() binding.progressBar.progress = currentBufferedPosition.toInt()
// 更新 SeekBar 的进度 // 更新 SeekBar 的进度
@ -330,8 +391,7 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
* 更新缓冲进度 * 更新缓冲进度
*/ */
private fun updateProgressBufferingState() { private fun updateProgressBufferingState() {
val currentPlayer = MediaControllerManager.getController() if (meController != null && meController.isLoading) {
if (currentPlayer != null && currentPlayer.isLoading) {
progressBufferingHandler.removeCallbacksAndMessages(null) progressBufferingHandler.removeCallbacksAndMessages(null)
progressBufferingHandler.sendEmptyMessage(1) progressBufferingHandler.sendEmptyMessage(1)
} else { } else {
@ -344,9 +404,8 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
*/ */
private val progressBufferingHandler = object : Handler(Looper.myLooper()!!) { private val progressBufferingHandler = object : Handler(Looper.myLooper()!!) {
override fun handleMessage(msg: Message) { override fun handleMessage(msg: Message) {
val currentPlayer = MediaControllerManager.getController() if (meController != null && meController.isLoading) {
if (currentPlayer != null && currentPlayer.isLoading) { val currentBufferedPosition = meController.bufferedPosition
val currentBufferedPosition = currentPlayer.bufferedPosition
binding.progressBar.progress = currentBufferedPosition.toInt() binding.progressBar.progress = currentBufferedPosition.toInt()
sendEmptyMessageDelayed(1, 50) sendEmptyMessageDelayed(1, 50)
} }
@ -360,44 +419,4 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
binding.playImg.setImageResource(R.drawable.play_green_icon) binding.playImg.setImageResource(R.drawable.play_green_icon)
} }
} }
private fun getSourceUrl(videoId: String): String {
return runBlocking(Dispatchers.IO) {
Innertube.player(PlayerBody(videoId = videoId))
}?.mapCatching { body ->
if (body.videoDetails?.videoId != videoId) {
throw VideoIdMismatchException()
}
when (val status = body.playabilityStatus?.status) {
"OK" -> body.streamingData?.highestQualityFormat?.let { format ->
format.url
} ?: throw PlayableFormatNotFoundException()
"UNPLAYABLE" -> throw UnplayableException()
"LOGIN_REQUIRED" -> throw LoginRequiredException()
else -> throw PlaybackException(
status,
null,
PlaybackException.ERROR_CODE_REMOTE_ERROR
)
}
}?.getOrNull() ?: ""
}
private fun retryPlayback(
player: MediaController,
mediaId: String,
url: String,
mediaMetadata: MediaMetadata
) {
val mediaItem =
MediaItem.Builder()
.setMediaId(mediaId)
.setUri(url)
.setMediaMetadata(mediaMetadata)
.build()
player.setMediaItem(mediaItem)
player.prepare()
player.play()
}
} }

View File

@ -1,11 +1,24 @@
package com.player.musicoo.activity package com.player.musicoo.activity
import android.content.Intent
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.view.View
import android.widget.Toast
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction import androidx.fragment.app.FragmentTransaction
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import com.bumptech.glide.Glide
import com.player.musicoo.R import com.player.musicoo.R
import com.player.musicoo.databinding.ActivityPrimaryBinding import com.player.musicoo.databinding.ActivityPrimaryBinding
import com.player.musicoo.fragment.ImportFragment import com.player.musicoo.fragment.ImportFragment
import com.player.musicoo.fragment.MoHomeFragment import com.player.musicoo.fragment.MoHomeFragment
import com.player.musicoo.media.MediaControllerManager
import com.player.musicoo.util.LogTag.LogD
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
class PrimaryActivity : MoBaseActivity() { class PrimaryActivity : MoBaseActivity() {
/** /**
@ -22,6 +35,9 @@ class PrimaryActivity : MoBaseActivity() {
binding = ActivityPrimaryBinding.inflate(layoutInflater) binding = ActivityPrimaryBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
initView() initView()
initPlayerListener()
onReceive()
} }
private fun initView() { private fun initView() {
@ -38,6 +54,28 @@ class PrimaryActivity : MoBaseActivity() {
changeFragment(1) changeFragment(1)
updateBtnState(1) updateBtnState(1)
} }
binding.playBlackBtn.setOnClickListener {
if (meController != null) {
if (meController.isPlaying) {
meController.pause()
updatePlayState(false)
} else {
meController.play()
updatePlayState(true)
}
updateProgressState()
}
}
binding.goDetailsBtn.setOnClickListener {
val intent = Intent(this, MoPlayDetailsActivity::class.java)
intent.putExtra(
MoPlayDetailsActivity.PLAY_DETAILS_COME_FROM,
PrimaryActivity::class.java
)
startActivity(intent)
}
} }
private fun initFragment() { private fun initFragment() {
@ -86,4 +124,141 @@ class PrimaryActivity : MoBaseActivity() {
) )
} }
} }
private suspend fun onReceive() {
while (isActive) {
select<Unit> {
events.onReceive {
when (it) {
Event.ActivityOnResume -> {
activityOnResume()
}
else -> {}
}
}
}
}
}
private fun activityOnResume() {
if (meController != null && meController.mediaItemCount > 0 && meController.duration > 0) {
binding.playingStatusLayout.visibility = View.VISIBLE
updateInfoUi(meController.currentMediaItem)
} else {
binding.playingStatusLayout.visibility = View.GONE
}
}
private fun initPlayerListener() {
meController?.addListener(playerListener)
}
private val playerListener = object : Player.Listener {
override fun onPositionDiscontinuity(
oldPosition: Player.PositionInfo,
newPosition: Player.PositionInfo,
reason: Int
) {
if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
updateInfoUi(meController?.currentMediaItem)
}
}
override fun onPlaybackStateChanged(playbackState: Int) {
LogD(TAG, "playbackState->$playbackState")
updateProgressState()
when (playbackState) {
Player.STATE_READY -> {
if (meController != null) {
LogD(TAG, "meController.duration->${meController.duration}")
binding.progressBar.setMaxProgress(MediaControllerManager.getDuration())
val currentPosition = meController.currentPosition
binding.progressBar.setProgress(currentPosition)
}
}
else -> {}
}
}
override fun onPlayWhenReadyChanged(
playWhenReady: Boolean,
reason: Int
) {
updatePlayState(playWhenReady)
updateProgressState()
}
}
override fun onDestroy() {
super.onDestroy()
meController?.removeListener(playerListener)
}
private var backPressedTime: Long = 0
private val backToast: Toast by lazy {
Toast.makeText(baseContext, getString(R.string.exit_main_text), Toast.LENGTH_SHORT)
}
override fun onBackPressed() {
if (backPressedTime + 2000 > System.currentTimeMillis()) {
super.onBackPressed()
backToast.cancel()
return
} else {
backToast.show()
}
backPressedTime = System.currentTimeMillis()
}
/**
* 更新播放进度
*/
private fun updateProgressState() {
//判断是否ready与播放中否则停止更新进度
if (meController != null && meController.playbackState == Player.STATE_READY && meController.isPlaying) {
updatePlayState(meController.isPlaying)
progressHandler.removeCallbacksAndMessages(null)
progressHandler.sendEmptyMessage(1)
} else {
progressHandler.removeCallbacksAndMessages(null)
}
}
/**
* 播放进度
*/
private val progressHandler = object : Handler(Looper.myLooper()!!) {
override fun handleMessage(msg: Message) {
//判断是否ready与播放中否则停止更新进度
if (meController != null && meController.playbackState == Player.STATE_READY && meController.isPlaying) {
val currentPosition = meController.currentPosition
binding.progressBar.setProgress(currentPosition)
sendEmptyMessageDelayed(1, 50)
}
}
}
private fun updatePlayState(b: Boolean) {
if (b) {
binding.playStatusImg.setImageResource(R.drawable.playing_black_icon)
} else {
binding.playStatusImg.setImageResource(R.drawable.play_black_icon)
}
}
private fun updateInfoUi(mediaItem: MediaItem?) {
if (mediaItem == null) {
return
}
Glide.with(this@PrimaryActivity)
.load(mediaItem.mediaMetadata.artworkUri)
.into(binding.audioImg)
binding.name.text = mediaItem.mediaMetadata.title
binding.desc.text = mediaItem.mediaMetadata.artist
}
} }

View File

@ -1,7 +1,7 @@
package com.player.musicoo.fragment package com.player.musicoo.fragment
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import com.gyf.immersionbar.ktx.immersionBar import com.gyf.immersionbar.ktx.immersionBar
import com.player.musicoo.databinding.FragmentMoHomeBinding import com.player.musicoo.databinding.FragmentMoHomeBinding
@ -9,16 +9,28 @@ import com.player.musicoo.innertube.Innertube
import com.player.musicoo.innertube.models.MusicCarouselShelfRenderer import com.player.musicoo.innertube.models.MusicCarouselShelfRenderer
import com.player.musicoo.innertube.requests.homePage import com.player.musicoo.innertube.requests.homePage
import com.player.musicoo.innertube.requests.homePageMore import com.player.musicoo.innertube.requests.homePageMore
import com.player.musicoo.util.LogTag.LogD
import com.player.musicoo.view.MusicResponsiveListView import com.player.musicoo.view.MusicResponsiveListView
import com.player.musicoo.view.MusicTowRowListView import com.player.musicoo.view.MusicTowRowListView
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
class MoHomeFragment : MoBaseFragment<FragmentMoHomeBinding>() { class MoHomeFragment : MoBaseFragment<FragmentMoHomeBinding>() {
private val requests: Channel<Request> = Channel(Channel.UNLIMITED)
enum class Request {
TryAgain,
}
override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentMoHomeBinding override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentMoHomeBinding
get() = FragmentMoHomeBinding::inflate get() = FragmentMoHomeBinding::inflate
override suspend fun onViewCreated() { override suspend fun onViewCreated() {
initView() initView()
initData()
onReceive()
} }
private fun initImmersionBar() { private fun initImmersionBar() {
@ -28,8 +40,30 @@ class MoHomeFragment : MoBaseFragment<FragmentMoHomeBinding>() {
} }
} }
private suspend fun initView() { private suspend fun onReceive() {
while (isActive) {
select<Unit> {
requests.onReceive {
when (it) {
Request.TryAgain -> {
initData()
}
}
}
}
}
}
private fun initView() {
binding.tryAgainBtn.setOnClickListener {
requests.trySend(Request.TryAgain)
}
}
private suspend fun initData() {
showLoadingUi()
Innertube.homePage()?.onSuccess { Innertube.homePage()?.onSuccess {
showDataUi()
if (it.homePage.isNotEmpty()) { if (it.homePage.isNotEmpty()) {
for (home: Innertube.HomePage in it.homePage) { for (home: Innertube.HomePage in it.homePage) {
for (content: MusicCarouselShelfRenderer.Content in home.contents) { for (content: MusicCarouselShelfRenderer.Content in home.contents) {
@ -55,9 +89,12 @@ class MoHomeFragment : MoBaseFragment<FragmentMoHomeBinding>() {
} }
initHomeDataMore(it) initHomeDataMore(it)
} else { } else {
Log.d(TAG, "homePage size 0") LogD(TAG, "homePage size 0")
} }
}?.onFailure { Log.d(TAG, "homePage onFailure->${it}") } }?.onFailure {
showNoContentUi()
LogD(TAG, "homePage onFailure->${it}")
}
} }
private suspend fun initHomeDataMore(baseHomePage: Innertube.BaseHomePage) { private suspend fun initHomeDataMore(baseHomePage: Innertube.BaseHomePage) {
@ -87,7 +124,7 @@ class MoHomeFragment : MoBaseFragment<FragmentMoHomeBinding>() {
} }
initHomeDataMore(it) initHomeDataMore(it)
}?.onFailure { }?.onFailure {
Log.d(TAG, "initHomeDataMore onFailure ->${it}") LogD(TAG, "initHomeDataMore onFailure ->${it}")
} }
} }
} }
@ -103,4 +140,19 @@ class MoHomeFragment : MoBaseFragment<FragmentMoHomeBinding>() {
initImmersionBar() initImmersionBar()
} }
} }
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

@ -167,42 +167,13 @@ object MediaControllerManager {
return false return false
} }
fun isPlaying(): Boolean {
mediaController?.let {
return it.isPlaying
}
return false
}
fun play() {
mediaController?.play()
}
fun getMediaItemCount(): Int {
mediaController?.let {
return it.mediaItemCount
}
return 0
}
fun getCurrentMediaItem(): MediaItem? {
mediaController?.let {
return it.currentMediaItem
}
return null
}
fun getDuration(): Long { fun getDuration(): Long {
mediaController?.let { mediaController?.let {
return it.duration if (it.duration > 0) {
} return it.duration
return 0 } else {
} return 0
}
fun getTotalBufferedDuration():Long{
mediaController?.let {
return it.totalBufferedDuration
} }
return 0 return 0
} }

View File

@ -1,12 +1,15 @@
package com.player.musicoo.service package com.player.musicoo.service
import android.content.Context import android.content.Context
import android.util.Log
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.session.* import androidx.media3.session.CommandButton
import androidx.media3.session.DefaultMediaNotificationProvider
import androidx.media3.session.MediaNotification
import androidx.media3.session.MediaSession
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import com.player.musicoo.util.LogTag import com.player.musicoo.util.LogTag
import com.player.musicoo.util.LogTag.LogD
@UnstableApi @UnstableApi
class CustomMediaNotificationProvider(context: Context) : class CustomMediaNotificationProvider(context: Context) :
@ -19,12 +22,12 @@ class CustomMediaNotificationProvider(context: Context) :
actionFactory: MediaNotification.ActionFactory actionFactory: MediaNotification.ActionFactory
): IntArray { ): IntArray {
Log.d(LogTag.VO_API_LOG, "mediaButtons->$mediaButtons") LogD(LogTag.VO_API_LOG, "mediaButtons->$mediaButtons")
for (com: CommandButton in mediaButtons) { for (com: CommandButton in mediaButtons) {
Log.d(LogTag.VO_API_LOG, "displayName->${com.displayName}") LogD(LogTag.VO_API_LOG, "displayName->${com.displayName}")
Log.d(LogTag.VO_API_LOG, "playerCommand->${com.playerCommand}") LogD(LogTag.VO_API_LOG, "playerCommand->${com.playerCommand}")
Log.d(LogTag.VO_API_LOG, "------------------------------------------") LogD(LogTag.VO_API_LOG, "------------------------------------------")
} }
val notificationMediaButtons = ImmutableList.builder<CommandButton>().apply { val notificationMediaButtons = ImmutableList.builder<CommandButton>().apply {

View File

@ -41,6 +41,7 @@ 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.util.ExoPlayerDiskCacheMaxSize import com.player.musicoo.util.ExoPlayerDiskCacheMaxSize
import com.player.musicoo.util.LogTag import com.player.musicoo.util.LogTag
import com.player.musicoo.util.LogTag.LogD
import com.player.musicoo.util.RingBuffer import com.player.musicoo.util.RingBuffer
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -152,7 +153,6 @@ class PlaybackService : MediaSessionService(), Player.Listener {
return ResolvingDataSource.Factory(createCacheDataSource()) { dataSpec -> return ResolvingDataSource.Factory(createCacheDataSource()) { dataSpec ->
val videoId = dataSpec.key ?: error("A key must be set") val videoId = dataSpec.key ?: error("A key must be set")
Log.d(TAG,"111111 videoId")
if (cache.isCached(videoId, dataSpec.position, chunkLength)) { if (cache.isCached(videoId, dataSpec.position, chunkLength)) {
dataSpec dataSpec
} else { } else {
@ -181,7 +181,7 @@ class PlaybackService : MediaSessionService(), Player.Listener {
) )
} }
} }
Log.d(TAG,"1111 urlResult->$urlResult") LogD(TAG,"service urlResult->$urlResult")
urlResult?.getOrThrow()?.let { url -> urlResult?.getOrThrow()?.let { url ->
ringBuffer.append(videoId to url.toUri()) ringBuffer.append(videoId to url.toUri())

View File

@ -1,8 +1,22 @@
package com.player.musicoo.util package com.player.musicoo.util
import android.util.Log
object LogTag { object LogTag {
const val VO_ACT_LOG = "vo-act—log" const val VO_ACT_LOG = "vo-act—log"
const val VO_FRAGMENT_LOG = "vo-fragment-log" const val VO_FRAGMENT_LOG = "vo-fragment-log"
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"
fun LogD(tag: String, message: String) {
Log.d(tag, message)
}
fun LogE(tag: String, message: String) {
Log.e(tag, message)
}
fun LogI(tag: String, message: String) {
Log.i(tag, message)
}
} }

View File

@ -11,8 +11,8 @@ import android.util.AttributeSet
import android.view.View import android.view.View
class CustomProgressBar(context: Context, attrs: AttributeSet?) : View(context, attrs) { class CustomProgressBar(context: Context, attrs: AttributeSet?) : View(context, attrs) {
private var progress = 0 // 当前进度 private var progress = 0f // 当前进度
private val maxProgress = 100 // 最大进度 private val maxProgress = 100f // 最大进度
private val progressBarHeight = 20f // 进度条高度 private val progressBarHeight = 20f // 进度条高度
private val cornerRadius = 10f // 圆角半径 private val cornerRadius = 10f // 圆角半径
private val backgroundColor = Color.parseColor("#26FFFFFF") private val backgroundColor = Color.parseColor("#26FFFFFF")
@ -39,7 +39,7 @@ class CustomProgressBar(context: Context, attrs: AttributeSet?) : View(context,
// 计算进度条的宽度 // 计算进度条的宽度
val progressBarWidth = (width * progress.toFloat() / maxProgress).toInt() val progressBarWidth = (width * progress / maxProgress)
// 创建颜色渐变对象 // 创建颜色渐变对象
val gradient = LinearGradient(0f, 0f, width.toFloat(), 0f, intArrayOf(startColor, middleColor, endColor), null, Shader.TileMode.CLAMP) val gradient = LinearGradient(0f, 0f, width.toFloat(), 0f, intArrayOf(startColor, middleColor, endColor), null, Shader.TileMode.CLAMP)
@ -50,12 +50,12 @@ class CustomProgressBar(context: Context, attrs: AttributeSet?) : View(context,
canvas.drawRoundRect(rect, cornerRadius, cornerRadius, paintTow) canvas.drawRoundRect(rect, cornerRadius, cornerRadius, paintTow)
} }
fun getProgress():Int{ fun getProgress():Float{
return progress return progress
} }
// 设置进度 // 设置进度
fun setProgress(progress: Int) { fun setProgress(progress: Float) {
this.progress = progress this.progress = progress
invalidate() // 请求重绘 invalidate() // 请求重绘
} }

View File

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

View File

@ -117,7 +117,10 @@
android:id="@+id/loading_view" android:id="@+id/loading_view"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true" /> android:layout_centerInParent="true"
android:indeterminateTint="@color/green"
android:progressBackgroundTint="@color/green"
android:progressTint="@color/green" />
<TextView <TextView
android:id="@+id/playback_error_layout" android:id="@+id/playback_error_layout"

View File

@ -10,12 +10,19 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" /> android:layout_height="0dp" />
<FrameLayout <LinearLayout
android:id="@+id/frame_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_above="@+id/bottom_layout" android:layout_above="@+id/bottom_layout"
android:layout_below="@+id/view" /> android:layout_below="@+id/view"
android:orientation="vertical">
<FrameLayout
android:id="@+id/frame_layout"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/bottom_layout" android:id="@+id/bottom_layout"
@ -29,8 +36,8 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="12dp" android:layout_marginStart="12dp"
android:visibility="gone"
android:layout_marginEnd="12dp" android:layout_marginEnd="12dp"
android:visibility="visible"
app:hl_cornerRadius="36dp" app:hl_cornerRadius="36dp"
app:hl_layoutBackground="#FF80F988" app:hl_layoutBackground="#FF80F988"
app:hl_shadowColor="#40040604" app:hl_shadowColor="#40040604"
@ -39,76 +46,87 @@
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="78dp" android:layout_height="68dp"
android:layout_alignParentBottom="true" android:layout_alignParentBottom="true"
android:gravity="center_vertical" android:gravity="center_vertical"
android:paddingStart="20dp" android:paddingStart="20dp"
android:paddingEnd="20dp"> android:paddingEnd="20dp">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<androidx.cardview.widget.CardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:elevation="0dp"
app:cardCornerRadius="24dp"
app:cardElevation="0dp">
<ImageView
android:id="@+id/audio_img"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@mipmap/breathe" />
</androidx.cardview.widget.CardView>
<com.player.musicoo.view.CircularProgressBar
android:id="@+id/progressBar"
android:layout_width="54dp"
android:layout_height="54dp"
android:layout_centerInParent="true" />
</RelativeLayout>
<LinearLayout <LinearLayout
android:id="@+id/goDetailsBtn"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_weight="1" android:layout_weight="1"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="vertical"> android:orientation="horizontal">
<com.player.musicoo.view.MarqueeTextView <RelativeLayout
android:id="@+id/name"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content">
android:maxLines="1"
android:ellipsize="marquee"
android:fontFamily="@font/medium_font"
android:text="@string/app_name"
android:textColor="@color/black"
android:textSize="16dp" />
<com.player.musicoo.view.MarqueeTextView <androidx.cardview.widget.CardView
android:id="@+id/desc" android:layout_width="wrap_content"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:layout_centerInParent="true"
android:maxLines="1" android:elevation="0dp"
android:text="@string/app_name" app:cardCornerRadius="24dp"
android:textColor="@color/black_60" app:cardElevation="0dp">
android:textSize="12dp" />
<ImageView
android:id="@+id/audio_img"
android:layout_width="48dp"
android:scaleType="centerCrop"
android:layout_height="48dp"
android:src="@mipmap/musicoo_logo_img" />
</androidx.cardview.widget.CardView>
<com.player.musicoo.view.CircularProgressBar
android:id="@+id/progressBar"
android:layout_width="54dp"
android:layout_height="54dp"
android:layout_centerInParent="true" />
</RelativeLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_weight="1"
android:gravity="center_vertical"
android:orientation="vertical">
<com.player.musicoo.view.MarqueeTextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:fontFamily="@font/medium_font"
android:maxLines="1"
android:text="@string/app_name"
android:textColor="@color/black"
android:textSize="16dp" />
<com.player.musicoo.view.MarqueeTextView
android:id="@+id/desc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:text="@string/app_name"
android:textColor="@color/black_60"
android:textSize="12dp" />
</LinearLayout>
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/alarm_clock_btn" android:id="@+id/alarm_clock_btn"
android:layout_width="42dp" android:layout_width="42dp"
android:visibility="gone"
android:layout_height="42dp" android:layout_height="42dp"
android:gravity="center"> android:gravity="center"
android:visibility="gone">
<ImageView <ImageView
android:layout_width="34dp" android:layout_width="34dp"

View File

@ -73,6 +73,9 @@
<ProgressBar <ProgressBar
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:indeterminateTint="@color/green"
android:progressBackgroundTint="@color/green"
android:progressTint="@color/green"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />
</LinearLayout> </LinearLayout>

View File

@ -16,42 +16,105 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" /> android:layout_height="0dp" />
<androidx.core.widget.NestedScrollView <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_below="@+id/view" android:layout_below="@+id/view"
android:layout_marginTop="-2dp"> android:orientation="vertical">
<LinearLayout <LinearLayout
android:id="@+id/loadingLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:orientation="vertical"> android:gravity="center"
android:visibility="gone">
<LinearLayout <ProgressBar
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:indeterminateTint="@color/green"
android:orientation="horizontal"> android:progressBackgroundTint="@color/green"
android:progressTint="@color/green"
<TextView android:layout_height="wrap_content" />
android:layout_width="match_parent"
android:layout_height="50dp"
android:fontFamily="@font/medium_font"
android:gravity="center"
android:text="@string/app_name"
android:textColor="@color/white"
android:textSize="20dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/content_layout"
android:layout_width="match_parent"
android:layout_marginStart="16dp"
android:layout_height="wrap_content"
android:orientation="vertical" />
</LinearLayout> </LinearLayout>
</androidx.core.widget.NestedScrollView> <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>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="-2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="match_parent"
android:layout_height="50dp"
android:fontFamily="@font/medium_font"
android:gravity="center"
android:text="@string/app_name"
android:textColor="@color/white"
android:textSize="20dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/content_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:orientation="vertical" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>
</RelativeLayout> </RelativeLayout>

View File

@ -19,4 +19,7 @@
<string name="expand">EXPAND</string> <string name="expand">EXPAND</string>
<string name="description">Description</string> <string name="description">Description</string>
<string name="playback_error">An unknown playback error has occurred</string> <string name="playback_error">An unknown playback error has occurred</string>
<string name="exit_main_text">Press again to exit</string>
<string name="content_loading_failed">Content loading failed, click to try again.</string>
<string name="try_again">Try Again</string>
</resources> </resources>

View File

@ -1,6 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="Base.Theme.Musicoo" parent="Theme.Material3.DayNight.NoActionBar"> <style name="Base.Theme.Musicoo" parent="Theme.Material3.Dark.NoActionBar">
<!-- Customize your light theme here. --> <!-- Customize your light theme here. -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> --> <!-- <item name="colorPrimary">@color/my_light_primary</item> -->
<item name="android:progressTint">@color/green</item> <item name="android:progressTint">@color/green</item>