main
Zeno 1 year ago
parent 4e21832f74
commit b06a2110ce

@ -0,0 +1,7 @@
*.iml
.gradle
/local.properties
/.idea
.DS_Store
/build
/captures

@ -0,0 +1,2 @@
/build
google-services.json

@ -0,0 +1,172 @@
import java.util.Properties
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin)
alias(libs.plugins.ksp)
alias(libs.plugins.hilt)
if (File("app/google-services.json").exists()) {
println("enable gms in app plugins")
alias(libs.plugins.gms)
alias(libs.plugins.crashlytics)
}
id("kotlin-kapt")
id("kotlin-parcelize")
id("auto-register")
}
android {
namespace = "me.wcy.music"
compileSdk = libs.versions.compileSdk.get().toInt()
defaultConfig {
applicationId = "me.wcy.music"
minSdk = libs.versions.minSdk.get().toInt()
targetSdk = libs.versions.targetSdk.get().toInt()
versionCode = libs.versions.versionCode.get().toInt()
versionName = libs.versions.versionName.get()
multiDexEnabled = true
ndk {
abiFilters.apply {
add("armeabi-v7a")
add("arm64-v8a")
add("x86_64")
}
}
applicationVariants.all {
outputs.all {
if (this is com.android.build.gradle.internal.api.ApkVariantOutputImpl) {
this.outputFileName = "AAmusic.apk"
}
}
}
}
buildFeatures {
viewBinding = true
}
signingConfigs {
register("release") {
enableV1Signing = true
enableV2Signing = true
storeFile = file("wangchenyan.keystore")
storePassword = getLocalValue("STORE_PASSWORD")
keyAlias = getLocalValue("KEY_ALIAS")
keyPassword = getLocalValue("KEY_PASSWORD")
}
}
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
signingConfig = signingConfigs.getByName("release")
}
}
compileOptions {
sourceCompatibility = JavaVersion.valueOf(libs.versions.java.get())
targetCompatibility = JavaVersion.valueOf(libs.versions.java.get())
}
}
fun getLocalValue(key: String): String {
return getLocalValue(key, false)
}
fun getLocalValue(key: String, quot: Boolean): String {
val properties = Properties()
properties.load(project.rootProject.file("local.properties").inputStream())
var value = if (properties.containsKey(key)) {
properties[key].toString()
} else {
""
}
if (quot) {
value = "\"" + value + "\""
}
return value
}
kapt {
// For hilt: Allow references to generated code
correctErrorTypes = true
}
ksp {
arg("moduleName", project.name)
// crouter 默认 scheme
arg("defaultScheme", "app")
// crouter 默认 host
arg("defaultHost", "music")
arg("room.schemaLocation", "$projectDir/schemas")
}
autoregister {
registerInfo = listOf(
// crouter 注解收集
mapOf(
"scanInterface" to "me.wcy.router.annotation.RouteLoader",
"codeInsertToClassName" to "me.wcy.router.RouteSet",
"codeInsertToMethodName" to "init",
"registerMethodName" to "register",
"include" to listOf("me/wcy/router/annotation/loader/.*")
)
)
}
dependencies {
implementation(libs.appcompat)
implementation(libs.constraintlayout)
implementation(libs.material)
implementation(libs.media3.exoplayer)
implementation(libs.media3.datasource.okhttp)
implementation(libs.media3.session)
implementation(libs.media3.ui)
implementation(libs.preference)
implementation(libs.flexbox)
implementation(libs.firebase.crashlytics.buildtools)
implementation(libs.support.annotations)
implementation(libs.annotation)
implementation(libs.support.v4)
implementation(libs.legacy.support.v4)
implementation(libs.support.v13)
implementation(libs.legacy.support.v13)
implementation(libs.appcompat.v7)
implementation(libs.support.vector.drawable)
implementation(libs.vectordrawable)
implementation(libs.design)
implementation(libs.gridlayout.v7)
implementation(libs.gridlayout)
implementation(libs.kotlin.reflect)
implementation(libs.error.prone.annotations)
ksp(libs.room.compiler)
implementation(libs.room)
kapt(libs.hilt.compiler)
implementation(libs.hilt)
if (File("${project.projectDir}/google-services.json").exists()) {
println("enable gms in app dependencies")
implementation(libs.crashlytics)
implementation(libs.analytics)
}
implementation(libs.common)
ksp(libs.crouter.compiler)
implementation(libs.crouter.api)
implementation(libs.lrcview)
implementation(libs.loggingInterceptor)
implementation(libs.zbar)
implementation(libs.blurry)
implementation(libs.banner)
}

Binary file not shown.

@ -0,0 +1,20 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "me.wcy.music",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 2030001,
"versionName": "2.3.0-beta01",
"outputFile": "AAmusic.apk"
}
],
"elementType": "File"
}

@ -0,0 +1,134 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "4ee3f3ec308e5eb7aa82deecaf89bd50",
"entities": [
{
"tableName": "play_list",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL, `song_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `artist` TEXT NOT NULL, `artist_id` INTEGER NOT NULL, `album` TEXT NOT NULL, `album_id` INTEGER NOT NULL, `album_cover` TEXT NOT NULL, `duration` INTEGER NOT NULL, `path` TEXT NOT NULL, `file_name` TEXT NOT NULL, `file_size` INTEGER NOT NULL, `unique_id` TEXT NOT NULL, PRIMARY KEY(`unique_id`))",
"fields": [
{
"fieldPath": "type",
"columnName": "type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "songId",
"columnName": "song_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "artist",
"columnName": "artist",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "artistId",
"columnName": "artist_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "album",
"columnName": "album",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "albumId",
"columnName": "album_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "albumCover",
"columnName": "album_cover",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "fileName",
"columnName": "file_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "fileSize",
"columnName": "file_size",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "uniqueId",
"columnName": "unique_id",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"unique_id"
]
},
"indices": [
{
"name": "index_play_list_title",
"unique": false,
"columnNames": [
"title"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_play_list_title` ON `${TABLE_NAME}` (`title`)"
},
{
"name": "index_play_list_artist",
"unique": false,
"columnNames": [
"artist"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_play_list_artist` ON `${TABLE_NAME}` (`artist`)"
},
{
"name": "index_play_list_album",
"unique": false,
"columnNames": [
"album"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_play_list_album` ON `${TABLE_NAME}` (`album`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4ee3f3ec308e5eb7aa82deecaf89bd50')"
]
}
}

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<application
android:name=".MusicApplication"
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@drawable/ic_launcher_round"
android:theme="@style/AppTheme">
<service
android:name=".service.MusicService"
android:exported="true"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService" />
</intent-filter>
</service>
<receiver
android:name=".download.DownloadReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.DOWNLOAD_COMPLETE" />
</intent-filter>
</receiver>
<activity
android:name=".main.MainActivity"
android:exported="true"
android:label="@string/app_name"
android:launchMode="singleTop"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".common.MusicFragmentContainerActivity"
android:screenOrientation="portrait" />
<activity
android:name=".main.SettingsActivity"
android:label="@string/menu_setting"
android:screenOrientation="portrait" />
<activity
android:name=".main.AboutActivity"
android:label="@string/menu_about"
android:screenOrientation="portrait" />
<activity
android:name="me.wcy.music.main.playing.PlayingActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:theme="@style/AppTheme.Popup" />
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

@ -0,0 +1,94 @@
package me.wcy.music
import android.app.Application
import android.content.ComponentName
import android.content.Intent
import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken
import com.blankj.utilcode.util.ActivityUtils
import com.google.common.util.concurrent.MoreExecutors
import dagger.hilt.android.HiltAndroidApp
import me.wcy.music.account.service.UserService
import me.wcy.music.common.DarkModeService
import me.wcy.music.common.MusicFragmentContainerActivity
import me.wcy.music.service.MusicService
import me.wcy.music.service.PlayServiceModule
import me.wcy.music.service.likesong.LikeSongProcessor
import me.wcy.router.CRouter
import me.wcy.router.RouterClient
import top.wangchenyan.common.CommonApp
import top.wangchenyan.common.ext.findActivity
import javax.inject.Inject
/**
* 自定义Application
* Created by wcy on 2015/11/27.
*/
@HiltAndroidApp
class MusicApplication : Application() {
@Inject
lateinit var userService: UserService
@Inject
lateinit var darkModeService: DarkModeService
@Inject
lateinit var likeSongProcessor: LikeSongProcessor
override fun onCreate() {
super.onCreate()
CommonApp.init {
test = BuildConfig.DEBUG
isDarkMode = { darkModeService.isDarkMode() }
titleLayoutConfig {
isStatusBarDarkFontWhenAuto = { darkModeService.isDarkMode().not() }
textColorAuto = { R.color.common_text_h1_color }
textColorBlack = { R.color.common_text_h1_color }
isTitleCenter = false
}
imageLoaderConfig {
placeholderAvatar = R.drawable.ic_launcher_round
}
apiConfig({}) {
codeJsonNames = listOf("code")
msgJsonNames = listOf("message", "msg")
dataJsonNames = listOf("data", "result")
successCode = 200
}
}
initCRouter()
darkModeService.init()
likeSongProcessor.init()
val sessionToken =
SessionToken(this, ComponentName(this, MusicService::class.java))
val mediaControllerFuture = MediaController.Builder(this, sessionToken).buildAsync()
mediaControllerFuture.addListener({
val player = mediaControllerFuture.get()
PlayServiceModule.setPlayer(player)
}, MoreExecutors.directExecutor())
}
private fun initCRouter() {
CRouter.setRouterClient(
RouterClient.Builder()
.baseUrl("app://music")
.loginProvider { context, callback ->
var activity = context.findActivity()
if (activity == null) {
activity = ActivityUtils.getTopActivity()
}
if (activity != null) {
userService.checkLogin(activity) {
callback()
}
}
}
.fragmentContainerIntentProvider {
Intent(it, MusicFragmentContainerActivity::class.java)
}
.build()
)
}
}

@ -0,0 +1,72 @@
package me.wcy.music.account
import top.wangchenyan.common.net.NetResult
import top.wangchenyan.common.net.gson.GsonConverterFactory
import top.wangchenyan.common.utils.GsonUtils
import top.wangchenyan.common.utils.ServerTime
import me.wcy.music.account.bean.LoginResultData
import me.wcy.music.account.bean.LoginStatusData
import me.wcy.music.account.bean.QrCodeData
import me.wcy.music.account.bean.QrCodeKeyData
import me.wcy.music.account.bean.SendCodeResult
import me.wcy.music.net.HttpClient
import me.wcy.music.storage.preference.ConfigPreferences
import retrofit2.Retrofit
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Query
/**
*
*/
interface AccountApi {
@GET("captcha/sent")
suspend fun sendPhoneCode(
@Query("phone") phone: String,
@Query("timestamp") timestamp: Long = ServerTime.currentTimeMillis()
): SendCodeResult
@GET("login/cellphone")
suspend fun phoneLogin(
@Query("phone") phone: String,
@Query("captcha") captcha: String,
@Query("timestamp") timestamp: Long = ServerTime.currentTimeMillis()
): LoginResultData
@GET("login/qr/key")
suspend fun getQrCodeKey(
@Query("timestamp") timestamp: Long = ServerTime.currentTimeMillis()
): NetResult<QrCodeKeyData>
@GET("login/qr/create")
suspend fun getLoginQrCode(
@Query("key") key: String,
@Query("timestamp") timestamp: Long = ServerTime.currentTimeMillis()
): NetResult<QrCodeData>
@GET("login/qr/check")
suspend fun checkLoginStatus(
@Query("key") key: String,
@Query("timestamp") timestamp: Long = ServerTime.currentTimeMillis(),
@Query("noCookie") noCookie: Boolean = true
): LoginResultData
@POST("login/status")
suspend fun getLoginStatus(
@Query("timestamp") timestamp: Long = ServerTime.currentTimeMillis()
): LoginStatusData
companion object {
private val api: AccountApi by lazy {
val retrofit = Retrofit.Builder()
.baseUrl(ConfigPreferences.apiDomain)
.addConverterFactory(GsonConverterFactory.create(GsonUtils.gson, true))
.client(HttpClient.okHttpClient)
.build()
retrofit.create(AccountApi::class.java)
}
fun get(): AccountApi = api
}
}

@ -0,0 +1,16 @@
package me.wcy.music.account
import top.wangchenyan.common.CommonApp
import top.wangchenyan.common.storage.IPreferencesFile
import top.wangchenyan.common.storage.PreferencesFile
import me.wcy.music.account.bean.ProfileData
import me.wcy.music.consts.PreferenceName
/**
*
*/
object AccountPreference :
IPreferencesFile by PreferencesFile(CommonApp.app, PreferenceName.ACCOUNT, false) {
var cookie by IPreferencesFile.StringProperty("cookie", "")
var profile by IPreferencesFile.ObjectProperty("profile", ProfileData::class.java)
}

@ -0,0 +1,30 @@
package me.wcy.music.account.bean
import com.google.gson.annotations.SerializedName
data class LoginResultData(
@SerializedName("code")
val code: Int = 0,
@SerializedName("message")
val message: String = "",
@SerializedName("nickname")
val nickname: String = "",
@SerializedName("avatarUrl")
val avatarUrl: String = "",
@SerializedName("cookie")
val cookie: String = ""
) {
companion object {
// 二维码不存在或已过期
const val STATUS_INVALID = 800
// 等待扫码
const val STATUS_NOT_SCAN = 801
// 授权中
const val STATUS_SCANNING = 802
// 授权登陆成功,包含 cookie
const val STATUS_SUCCESS = 803
}
}

@ -0,0 +1,46 @@
package me.wcy.music.account.bean
import com.google.gson.annotations.SerializedName
data class LoginStatusData(
@SerializedName("data")
val `data`: Data = Data()
) {
data class Data(
@SerializedName("code")
val code: Int = 0,
@SerializedName("account")
val account: Account = Account(),
@SerializedName("profile")
val profile: ProfileData? = null
) {
data class Account(
@SerializedName("id")
val id: Int = 0,
@SerializedName("userName")
val userName: String = "",
@SerializedName("type")
val type: Int = 0,
@SerializedName("status")
val status: Int = 0,
@SerializedName("whitelistAuthority")
val whitelistAuthority: Int = 0,
@SerializedName("createTime")
val createTime: Long = 0,
@SerializedName("tokenVersion")
val tokenVersion: Int = 0,
@SerializedName("ban")
val ban: Int = 0,
@SerializedName("baoyueVersion")
val baoyueVersion: Int = 0,
@SerializedName("donateVersion")
val donateVersion: Int = 0,
@SerializedName("vipType")
val vipType: Int = 0,
@SerializedName("anonimousUser")
val anonimousUser: Boolean = false,
@SerializedName("paidFee")
val paidFee: Boolean = false
)
}
}

@ -0,0 +1,71 @@
package me.wcy.music.account.bean
import com.google.gson.annotations.SerializedName
/**
*
*/
data class ProfileData(
@SerializedName("userId")
val userId: Long = 0,
@SerializedName("userType")
val userType: Int = 0,
@SerializedName("nickname")
val nickname: String = "",
@SerializedName("avatarImgId")
val avatarImgId: Long = 0,
@SerializedName("avatarUrl")
val avatarUrl: String = "",
@SerializedName("backgroundImgId")
val backgroundImgId: Long = 0,
@SerializedName("backgroundUrl")
val backgroundUrl: String = "",
@SerializedName("signature")
val signature: String = "",
@SerializedName("createTime")
val createTime: Long = 0,
@SerializedName("userName")
val userName: String = "",
@SerializedName("accountType")
val accountType: Int = 0,
@SerializedName("shortUserName")
val shortUserName: String = "",
@SerializedName("birthday")
val birthday: Long = 0,
@SerializedName("authority")
val authority: Int = 0,
@SerializedName("gender")
val gender: Int = 0,
@SerializedName("accountStatus")
val accountStatus: Int = 0,
@SerializedName("province")
val province: Int = 0,
@SerializedName("city")
val city: Int = 0,
@SerializedName("authStatus")
val authStatus: Int = 0,
@SerializedName("defaultAvatar")
val defaultAvatar: Boolean = false,
@SerializedName("djStatus")
val djStatus: Int = 0,
@SerializedName("locationStatus")
val locationStatus: Int = 0,
@SerializedName("vipType")
val vipType: Int = 0,
@SerializedName("followed")
val followed: Boolean = false,
@SerializedName("mutual")
val mutual: Boolean = false,
@SerializedName("authenticated")
val authenticated: Boolean = false,
@SerializedName("lastLoginTime")
val lastLoginTime: Long = 0,
@SerializedName("lastLoginIP")
val lastLoginIP: String = "",
@SerializedName("viptypeVersion")
val viptypeVersion: Long = 0,
@SerializedName("authenticationTypes")
val authenticationTypes: Int = 0,
@SerializedName("anchor")
val anchor: Boolean = false
)

@ -0,0 +1,8 @@
package me.wcy.music.account.bean
import com.google.gson.annotations.SerializedName
data class QrCodeData(
@SerializedName("qrurl")
val qrurl: String = ""
)

@ -0,0 +1,10 @@
package me.wcy.music.account.bean
import com.google.gson.annotations.SerializedName
data class QrCodeKeyData(
@SerializedName("code")
val code: Int = 0,
@SerializedName("unikey")
val unikey: String = ""
)

@ -0,0 +1,10 @@
package me.wcy.music.account.bean
import com.google.gson.annotations.SerializedName
data class SendCodeResult(
@SerializedName("code")
val code: Int = 0,
@SerializedName("message")
val message: String = "",
)

@ -0,0 +1,60 @@
package me.wcy.music.account.login
import android.view.View
import top.wangchenyan.common.ext.viewBindings
import me.wcy.music.common.BaseMusicFragment
import me.wcy.music.consts.RoutePath
import me.wcy.music.databinding.FragmentLoginRouteBinding
import me.wcy.router.CRouter
import me.wcy.router.RouteResultListener
import me.wcy.router.annotation.Route
/**
*
*/
@Route(RoutePath.LOGIN)
class LoginRouteFragment : BaseMusicFragment() {
private val viewBinding by viewBindings<FragmentLoginRouteBinding>()
override fun getRootView(): View {
return viewBinding.root
}
override fun isLazy(): Boolean {
return false
}
private val routeResultListener: RouteResultListener = {
if (it.isSuccess()) {
setResultAndFinish()
} else if (it.resultCode == RESULT_SWITCH_QRCODE) {
startQrCode()
} else if (it.resultCode == RESULT_SWITCH_PHONE) {
startPhone()
} else {
finish()
}
}
override fun onLazyCreate() {
super.onLazyCreate()
startPhone()
}
private fun startPhone() {
CRouter.with(requireActivity())
.url(RoutePath.PHONE_LOGIN)
.startForResult(routeResultListener)
}
private fun startQrCode() {
CRouter.with(requireActivity())
.url(RoutePath.QRCODE_LOGIN)
.startForResult(routeResultListener)
}
companion object {
const val RESULT_SWITCH_QRCODE = 100
const val RESULT_SWITCH_PHONE = 200
}
}

@ -0,0 +1,115 @@
package me.wcy.music.account.login.phone
import android.view.View
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import top.wangchenyan.common.ext.viewBindings
import top.wangchenyan.common.utils.ToastUtils
import me.wcy.music.account.login.LoginRouteFragment
import me.wcy.music.account.service.UserService
import me.wcy.music.common.BaseMusicFragment
import me.wcy.music.consts.RoutePath
import me.wcy.music.databinding.FragmentPhoneLoginBinding
import me.wcy.router.annotation.Route
import javax.inject.Inject
/**
*
*/
@Route(RoutePath.PHONE_LOGIN)
@AndroidEntryPoint
class PhoneLoginFragment : BaseMusicFragment() {
private val viewBinding by viewBindings<FragmentPhoneLoginBinding>()
private val viewModel by viewModels<PhoneLoginViewModel>()
@Inject
lateinit var userService: UserService
override fun getRootView(): View {
return viewBinding.root
}
override fun onLazyCreate() {
super.onLazyCreate()
initView()
initDataObserver()
}
private fun initView() {
val updateLoginBtnState = {
viewBinding.btnLogin.isEnabled =
viewBinding.etPhone.length() > 0 && viewBinding.etPhoneCode.length() > 0
}
viewBinding.etPhone.doAfterTextChanged {
updateLoginBtnState()
}
viewBinding.etPhoneCode.doAfterTextChanged {
updateLoginBtnState()
}
viewBinding.tvSendCode.setOnClickListener {
val phone = viewBinding.etPhone.text?.toString()
if (phone.isNullOrEmpty()) {
ToastUtils.show("请输入手机号")
return@setOnClickListener
}
lifecycleScope.launch {
viewBinding.tvSendCode.isEnabled = false
val res = viewModel.sendPhoneCode(phone)
if (res.isSuccess().not()) {
viewBinding.tvSendCode.isEnabled = true
ToastUtils.show(res.msg)
}
}
}
viewBinding.btnLogin.setOnClickListener {
val phone = viewBinding.etPhone.text?.toString()
if (phone.isNullOrEmpty()) {
ToastUtils.show("请输入手机号")
return@setOnClickListener
}
val code = viewBinding.etPhoneCode.text?.toString()
if (code.isNullOrEmpty()) {
ToastUtils.show("请输入手机验证码")
return@setOnClickListener
}
lifecycleScope.launch {
showLoading(false)
val res = viewModel.phoneLogin(phone, code)
dismissLoading()
if (res.isSuccess()) {
ToastUtils.show("登录成功")
setResultAndFinish()
} else {
ToastUtils.show(res.msg.orEmpty().ifEmpty {
"登录失败,请更新服务端版本或稍后重试"
})
}
}
}
viewBinding.tvQrcodeLogin.setOnClickListener {
activity?.apply {
setResult(LoginRouteFragment.RESULT_SWITCH_QRCODE)
finish()
}
}
}
private fun initDataObserver() {
lifecycleScope.launch {
viewModel.sendPhoneCodeCountdown.collectLatest { sendPhoneCodeCountdown ->
if (sendPhoneCodeCountdown > 0) {
viewBinding.tvSendCode.isEnabled = false
viewBinding.tvSendCode.text = "${sendPhoneCodeCountdown}秒后重发"
} else {
viewBinding.tvSendCode.isEnabled = true
viewBinding.tvSendCode.text = "获取验证码"
}
}
}
}
}

@ -0,0 +1,79 @@
package me.wcy.music.account.login.phone
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import top.wangchenyan.common.ext.toUnMutable
import top.wangchenyan.common.model.CommonResult
import me.wcy.music.account.AccountApi
import me.wcy.music.account.service.UserService
import me.wcy.music.net.NetUtils
import javax.inject.Inject
/**
*
*/
@HiltViewModel
class PhoneLoginViewModel @Inject constructor(
private val userService: UserService
) : ViewModel() {
private val _sendPhoneCodeCountdown = MutableStateFlow(0)
val sendPhoneCodeCountdown = _sendPhoneCodeCountdown.toUnMutable()
suspend fun sendPhoneCode(phone: String): CommonResult<Unit> {
if (_sendPhoneCodeCountdown.value > 0) {
return CommonResult.fail()
}
val res = kotlin.runCatching {
AccountApi.get().sendPhoneCode(phone)
}
return if (res.isSuccess) {
val data = res.getOrThrow()
if (data.code == 200) {
viewModelScope.launch {
_sendPhoneCodeCountdown.value = 30
repeat(Int.MAX_VALUE) {
delay(1000)
_sendPhoneCodeCountdown.value = _sendPhoneCodeCountdown.value - 1
if (_sendPhoneCodeCountdown.value == 0) {
return@launch
}
}
}
CommonResult.success(Unit)
} else {
CommonResult.fail(data.code, data.message)
}
} else {
NetUtils.parseErrorResponse(res.exceptionOrNull())
}
}
suspend fun phoneLogin(phone: String, code: String): CommonResult<Unit> {
val loginRes = kotlin.runCatching {
AccountApi.get().phoneLogin(phone, code)
}
return if (loginRes.isSuccess) {
val data = loginRes.getOrNull()
if (data?.code == 200) {
val getProfileRes = userService.login(data.cookie)
if (getProfileRes.isSuccessWithData()) {
CommonResult.success(Unit)
} else {
CommonResult.fail(getProfileRes.code, getProfileRes.msg)
}
} else {
CommonResult.fail(data?.code ?: -1, data?.message)
}
} else {
var result = NetUtils.parseErrorResponse(loginRes.exceptionOrNull())
if (result.code == -462) {
result = result.copy(msg = "登录失败,请更新服务端版本或稍后重试")
}
return result
}
}
}

@ -0,0 +1,119 @@
package me.wcy.music.account.login.qrcode
import android.view.View
import androidx.core.view.isVisible
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import top.wangchenyan.common.ext.viewBindings
import me.wcy.music.account.bean.LoginResultData
import me.wcy.music.account.login.LoginRouteFragment
import me.wcy.music.account.service.UserService
import me.wcy.music.common.BaseMusicFragment
import me.wcy.music.consts.RoutePath
import me.wcy.music.databinding.FragmentQrcodeLoginBinding
import me.wcy.router.annotation.Route
import javax.inject.Inject
/**
*
*/
@Route(RoutePath.QRCODE_LOGIN)
@AndroidEntryPoint
class QrcodeLoginFragment : BaseMusicFragment() {
private val viewBinding by viewBindings<FragmentQrcodeLoginBinding>()
private val viewModel by viewModels<QrcodeLoginViewModel>()
@Inject
lateinit var userService: UserService
override fun getRootView(): View {
return viewBinding.root
}
override fun onLazyCreate() {
super.onLazyCreate()
viewBinding.tvPhoneLogin.setOnClickListener {
activity?.apply {
setResult(LoginRouteFragment.RESULT_SWITCH_PHONE)
finish()
}
}
loadQrCode()
lifecycleScope.launch {
viewModel.loginStatus.collectLatest { status ->
viewBinding.tvStatus.setOnClickListener(null)
if (status == null) {
viewBinding.tvStatus.isVisible = true
viewBinding.tvStatus.text = "加载中…"
} else {
when (status.code) {
LoginResultData.STATUS_NOT_SCAN -> {
viewBinding.tvStatus.isVisible = false
}
LoginResultData.STATUS_SCANNING -> {
viewBinding.tvStatus.isVisible = true
viewBinding.tvStatus.text = "${status.nickname}」授权中"
}
LoginResultData.STATUS_SUCCESS -> {
viewBinding.tvStatus.isVisible = true
viewBinding.tvStatus.text = status.message
getProfile(status.cookie)
}
LoginResultData.STATUS_INVALID -> {
viewBinding.tvStatus.isVisible = true
viewBinding.tvStatus.text = "二维码已失效,点击刷新"
viewBinding.tvStatus.setOnClickListener {
loadQrCode()
}
}
else -> {
viewBinding.tvStatus.isVisible = true
viewBinding.tvStatus.text =
status.message.ifEmpty { "二维码错误,点击刷新" }
viewBinding.tvStatus.setOnClickListener {
loadQrCode()
}
}
}
}
}
}
lifecycleScope.launch {
viewModel.qrCode.collectLatest { qrCode ->
viewBinding.ivQrCode.setImageBitmap(qrCode)
}
}
}
private fun loadQrCode() {
lifecycleScope.launch {
viewModel.getLoginQrCode()
}
}
private fun getProfile(cookie: String) {
lifecycleScope.launch {
val res = userService.login(cookie)
if (res.isSuccessWithData()) {
setResultAndFinish()
} else {
viewBinding.tvStatus.isVisible = true
viewBinding.tvStatus.text = "登录失败,点击重试"
viewBinding.tvStatus.setOnClickListener {
loadQrCode()
}
}
}
}
}

@ -0,0 +1,78 @@
package me.wcy.music.account.login.qrcode
import android.graphics.Bitmap
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import cn.bertsir.zbar.utils.QRUtils
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import top.wangchenyan.common.ext.toUnMutable
import top.wangchenyan.common.net.apiCall
import me.wcy.music.account.AccountApi
import me.wcy.music.account.bean.LoginResultData
import me.wcy.music.account.service.UserService
import javax.inject.Inject
/**
*
*/
@HiltViewModel
class QrcodeLoginViewModel @Inject constructor(
private val userService: UserService
) : ViewModel() {
private var qrCodeKey = ""
private val _qrCode = MutableStateFlow<Bitmap?>(null)
val qrCode = _qrCode
private val _loginStatus = MutableStateFlow<LoginResultData?>(null)
val loginStatus = _loginStatus.toUnMutable()
private var job: Job? = null
fun getLoginQrCode() {
job?.cancel()
job = viewModelScope.launch(Dispatchers.Default) {
qrCodeKey = ""
_qrCode.value = null
_loginStatus.value = null
val getKeyRes = apiCall {
AccountApi.get().getQrCodeKey()
}
if (getKeyRes.isSuccessWithData().not()) {
_loginStatus.value = LoginResultData(-1)
return@launch
}
val keyData = getKeyRes.getDataOrThrow()
qrCodeKey = keyData.unikey
val getQrCodeRes = apiCall {
AccountApi.get().getLoginQrCode(qrCodeKey)
}
if (getQrCodeRes.isSuccessWithData().not()) {
_loginStatus.value = LoginResultData(-1)
return@launch
}
val qrCodeData = getQrCodeRes.getDataOrThrow()
_qrCode.value = QRUtils.getInstance().createQRCode(qrCodeData.qrurl)
while (true) {
kotlin.runCatching {
AccountApi.get().checkLoginStatus(qrCodeKey)
}.onSuccess { status ->
_loginStatus.value = status
if (status.code == LoginResultData.STATUS_NOT_SCAN
|| status.code == LoginResultData.STATUS_SCANNING
) {
delay(3000)
} else {
return@launch
}
}.onFailure {
_loginStatus.value = LoginResultData(-1, it.message ?: "")
return@launch
}
}
}
}
}

@ -0,0 +1,30 @@
package me.wcy.music.account.service
import android.app.Activity
import kotlinx.coroutines.flow.StateFlow
import top.wangchenyan.common.model.CommonResult
import me.wcy.music.account.bean.ProfileData
/**
*
*/
interface UserService {
val profile: StateFlow<ProfileData?>
fun getCookie(): String
fun isLogin(): Boolean
fun getUserId(): Long
suspend fun login(cookie: String): CommonResult<ProfileData>
suspend fun logout()
fun checkLogin(
activity: Activity,
showDialog: Boolean = true,
onCancel: (() -> Unit)? = null,
onLogin: (() -> Unit)? = null
)
}

@ -0,0 +1,104 @@
package me.wcy.music.account.service
import android.app.Activity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.withContext
import me.wcy.music.account.AccountApi
import me.wcy.music.account.AccountPreference
import me.wcy.music.account.bean.ProfileData
import me.wcy.music.consts.RoutePath
import me.wcy.music.net.NetCache
import me.wcy.router.CRouter
import top.wangchenyan.common.ext.showConfirmDialog
import top.wangchenyan.common.ext.toUnMutable
import top.wangchenyan.common.model.CommonResult
import javax.inject.Inject
import javax.inject.Singleton
/**
*
*/
@Singleton
class UserServiceImpl @Inject constructor() : UserService {
private val _profile = MutableStateFlow(AccountPreference.profile)
override val profile = _profile.toUnMutable()
override fun getCookie(): String {
return AccountPreference.cookie
}
override fun isLogin(): Boolean {
return profile.value != null
}
override fun getUserId(): Long {
return _profile.value?.userId ?: 0
}
override suspend fun login(cookie: String): CommonResult<ProfileData> {
AccountPreference.cookie = cookie
val res = kotlin.runCatching {
AccountApi.get().getLoginStatus()
}
return if (res.isSuccess) {
val loginStatusData = res.getOrThrow()
val status = loginStatusData.data.account.status
if (status == 0
&& loginStatusData.data.profile != null
) {
val profileData = loginStatusData.data.profile
_profile.value = profileData
AccountPreference.profile = profileData
CommonResult.success(profileData)
} else {
AccountPreference.cookie = ""
CommonResult.fail(status, msg = "login fail")
}
} else {
AccountPreference.cookie = ""
CommonResult.fail(msg = res.exceptionOrNull()?.message)
}
}
override suspend fun logout() {
withContext(Dispatchers.IO) {
AccountPreference.clear()
NetCache.userCache.clear()
}
_profile.value = null
}
override fun checkLogin(
activity: Activity,
showDialog: Boolean,
onCancel: (() -> Unit)?,
onLogin: (() -> Unit)?
) {
if (isLogin()) {
onLogin?.invoke()
return
}
val startLogin = {
CRouter.with(activity).url(RoutePath.LOGIN).startForResult {
if (it.isSuccess()) {
onLogin?.invoke()
}
}
}
if (showDialog.not()) {
startLogin()
return
}
activity.showConfirmDialog(
title = "未登录",
message = "请先登录",
confirmButton = "去登录",
onCancelClick = {
onCancel?.invoke()
}
) {
startLogin()
}
}
}

@ -0,0 +1,32 @@
package me.wcy.music.account.service
import android.app.Application
import dagger.Binds
import dagger.Module
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import me.wcy.music.ext.accessEntryPoint
/**
*
*/
@Module
@InstallIn(SingletonComponent::class)
abstract class UserServiceModule {
@Binds
abstract fun bindUserService(userServiceImpl: UserServiceImpl): UserService
companion object {
fun Application.userService(): UserService {
return accessEntryPoint<UserServiceEntryPoint>().userService()
}
}
@EntryPoint
@InstallIn(SingletonComponent::class)
interface UserServiceEntryPoint {
fun userService(): UserService
}
}

@ -0,0 +1,94 @@
package me.wcy.music.common
import android.content.Context
import androidx.core.text.buildSpannedString
import com.blankj.utilcode.util.AppUtils
import com.blankj.utilcode.util.RegexUtils
import kotlinx.coroutines.launch
import me.wcy.music.R
import me.wcy.music.account.AccountPreference
import me.wcy.music.databinding.DialogApiDomainBinding
import me.wcy.music.net.NetCache
import me.wcy.music.storage.preference.ConfigPreferences
import top.wangchenyan.common.CommonApp
import top.wangchenyan.common.ext.getColorEx
import top.wangchenyan.common.ext.setLink
import top.wangchenyan.common.ext.showConfirmDialog
import top.wangchenyan.common.ext.showSingleDialog
import top.wangchenyan.common.ext.toast
import top.wangchenyan.common.utils.LaunchUtils
import top.wangchenyan.common.widget.CustomSpan.appendStyle
import top.wangchenyan.common.widget.dialog.CenterDialog
import top.wangchenyan.common.widget.dialog.CenterDialogBuilder
/**
*
*/
class ApiDomainDialog(private val context: Context) {
fun show() {
CenterDialogBuilder(context)
.title("请输入云音乐API域名")
.contentViewBinding { dialog: CenterDialog, viewBinding: DialogApiDomainBinding ->
viewBinding.tvDoc.setLink()
viewBinding.tvDoc.text = buildSpannedString {
append("点击查看")
appendStyle(
"云音乐API文档",
color = context.getColorEx(R.color.common_theme_color)
) {
LaunchUtils.launchBrowser(
context,
"https://binaryify.github.io/NeteaseCloudMusicApi"
)
}
}
if (ConfigPreferences.apiDomain.isNotEmpty()) {
viewBinding.etInput.hint = ConfigPreferences.apiDomain
}
}
.buttonText(
context.getString(R.string.common_confirm),
context.getString(R.string.common_cancel)
)
.onButtonClickListener { dialog, which ->
if (which == 0) {
val domain =
dialog.getContentViewBinding<DialogApiDomainBinding>()?.etInput?.text?.toString()
if (RegexUtils.isURL(domain).not()) {
toast("请输入正确的域名")
} else if (domain!!.endsWith("/").not()) {
toast("域名需要以'/'结尾")
} else {
ConfigPreferences.apiDomain = domain
AccountPreference.clear()
CommonApp.appScope.launch {
NetCache.userCache.clear()
}
dialog.dismiss()
context.showSingleDialog("设置成功,重启后生效") {
AppUtils.relaunchApp(true)
}
}
} else {
dialog.dismiss()
}
}
.isAutoClose(false)
.build()
.show()
}
companion object {
fun checkApiDomain(context: Context): Boolean {
return if (ConfigPreferences.apiDomain.isEmpty()) {
context.showConfirmDialog("请先设置云音乐API域名") {
ApiDomainDialog(context).show()
}
false
} else {
true
}
}
}
}

@ -0,0 +1,23 @@
package me.wcy.music.common
import com.kingja.loadsir.callback.Callback
import me.wcy.music.widget.loadsir.SoundWaveLoadingCallback
import top.wangchenyan.common.ui.activity.BaseActivity
/**
*
*/
abstract class BaseMusicActivity : BaseActivity() {
override fun getLoadingCallback(): Callback {
return SoundWaveLoadingCallback()
}
override fun showLoadSirLoading() {
loadService?.showCallback(SoundWaveLoadingCallback::class.java)
}
companion object {
private const val TAG = "BaseMusicActivity"
}
}

@ -0,0 +1,19 @@
package me.wcy.music.common
import com.kingja.loadsir.callback.Callback
import top.wangchenyan.common.ui.fragment.BaseFragment
import me.wcy.music.widget.loadsir.SoundWaveLoadingCallback
/**
*
*/
abstract class BaseMusicFragment : BaseFragment() {
override fun getLoadingCallback(): Callback {
return SoundWaveLoadingCallback()
}
override fun showLoadSirLoading() {
loadService?.showCallback(SoundWaveLoadingCallback::class.java)
}
}

@ -0,0 +1,19 @@
package me.wcy.music.common
import com.kingja.loadsir.callback.Callback
import top.wangchenyan.common.ui.fragment.BaseRefreshFragment
import me.wcy.music.widget.loadsir.SoundWaveLoadingCallback
/**
*
*/
abstract class BaseMusicRefreshFragment<T> : BaseRefreshFragment<T>() {
override fun getLoadingCallback(): Callback {
return SoundWaveLoadingCallback()
}
override fun showLoadSirLoading() {
loadService?.showCallback(SoundWaveLoadingCallback::class.java)
}
}

@ -0,0 +1,67 @@
package me.wcy.music.common
import android.content.res.Configuration
import androidx.appcompat.app.AppCompatDelegate
import com.blankj.utilcode.util.ActivityUtils
import me.wcy.music.storage.preference.ConfigPreferences
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class DarkModeService @Inject constructor() {
fun init() {
setDarkModeInternal(DarkMode.fromValue(ConfigPreferences.darkMode))
}
fun setDarkMode(mode: DarkMode) {
if (mode.value != ConfigPreferences.darkMode) {
ConfigPreferences.darkMode = mode.value
setDarkModeInternal(mode)
}
}
private fun setDarkModeInternal(mode: DarkMode) {
AppCompatDelegate.setDefaultNightMode(mode.systemValue)
}
fun isDarkMode(): Boolean {
val context = ActivityUtils.getTopActivity() ?: return false
val nightModeFlags =
context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
when (nightModeFlags) {
Configuration.UI_MODE_NIGHT_NO -> {
return false
}
Configuration.UI_MODE_NIGHT_YES -> {
return true
}
}
return false
}
sealed class DarkMode(val value: String, val systemValue: Int) {
object Auto : DarkMode("0", AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
object Light : DarkMode("1", AppCompatDelegate.MODE_NIGHT_NO)
object Dark : DarkMode("2", AppCompatDelegate.MODE_NIGHT_YES)
override fun hashCode(): Int {
return value.hashCode()
}
override fun equals(other: Any?): Boolean {
return other is DarkMode && other.value == this.value
}
companion object {
fun fromValue(value: String): DarkMode {
return when (value) {
"1" -> Light
"2" -> Dark
else -> Auto
}
}
}
}
}

@ -0,0 +1,10 @@
package me.wcy.music.common
import dagger.hilt.android.AndroidEntryPoint
import top.wangchenyan.common.ui.activity.FragmentContainerActivity
/**
*
*/
@AndroidEntryPoint
class MusicFragmentContainerActivity : FragmentContainerActivity()

@ -0,0 +1,8 @@
package me.wcy.music.common
/**
*
*/
interface OnItemClickListener<T> {
fun onItemClick(item: T, position: Int)
}

@ -0,0 +1,9 @@
package me.wcy.music.common
/**
*
*/
interface OnItemClickListener2<T> {
fun onItemClick(item: T, position: Int)
fun onMoreClick(item: T, position: Int)
}

@ -0,0 +1,19 @@
package me.wcy.music.common
import com.kingja.loadsir.callback.Callback
import top.wangchenyan.common.ui.fragment.SimpleRefreshFragment
import me.wcy.music.widget.loadsir.SoundWaveLoadingCallback
/**
*
*/
abstract class SimpleMusicRefreshFragment<T> : SimpleRefreshFragment<T>() {
override fun getLoadingCallback(): Callback {
return SoundWaveLoadingCallback()
}
override fun showLoadSirLoading() {
loadService?.showCallback(SoundWaveLoadingCallback::class.java)
}
}

@ -0,0 +1,32 @@
package me.wcy.music.common.bean
import com.google.gson.annotations.SerializedName
import me.wcy.music.utils.MusicUtils.asLargeCover
import me.wcy.music.utils.MusicUtils.asSmallCover
/**
*
*/
data class AlbumData(
@SerializedName("id")
val id: Long = 0,
@SerializedName("name")
val name: String = "",
@Deprecated("Please use resized url")
@SerializedName("picUrl")
val picUrl: String = "",
@SerializedName("tns")
val tns: List<Any> = listOf(),
@SerializedName("pic_str")
val picStr: String = "",
@SerializedName("pic")
val pic: Long = 0
) {
fun getSmallCover(): String {
return picUrl.asSmallCover()
}
fun getLargeCover(): String {
return picUrl.asLargeCover()
}
}

@ -0,0 +1,17 @@
package me.wcy.music.common.bean
import com.google.gson.annotations.SerializedName
/**
*
*/
data class ArtistData(
@SerializedName("id")
val id: Long = 0,
@SerializedName("name")
val name: String = "",
@SerializedName("tns")
val tns: List<Any> = listOf(),
@SerializedName("alias")
val alias: List<Any> = listOf()
)

@ -0,0 +1,12 @@
package me.wcy.music.common.bean
import com.google.gson.annotations.SerializedName
data class LrcData(
@SerializedName("version")
val version: Int = 0,
@SerializedName("lyric")
val lyric: String = ""
) {
fun isValid() = lyric.isNotEmpty()
}

@ -0,0 +1,13 @@
package me.wcy.music.common.bean
import com.google.gson.annotations.SerializedName
/**
*
*/
data class LrcDataWrap(
@SerializedName("code")
val code: Int = -1,
@SerializedName("lrc")
val lrc: LrcData = LrcData()
)

@ -0,0 +1,17 @@
package me.wcy.music.common.bean
import com.google.gson.annotations.SerializedName
/**
*
*/
data class OriginSongSimpleData(
@SerializedName("songId")
val songId: Int = 0,
@SerializedName("name")
val name: String = "",
@SerializedName("artists")
val artists: List<ArtistData> = listOf(),
@SerializedName("albumMeta")
val albumMeta: AlbumData = AlbumData()
)

@ -0,0 +1,51 @@
package me.wcy.music.common.bean
import com.google.gson.annotations.SerializedName
import me.wcy.music.account.bean.ProfileData
import me.wcy.music.utils.MusicUtils.asLargeCover
import me.wcy.music.utils.MusicUtils.asSmallCover
data class PlaylistData(
@SerializedName("id")
val id: Long = 0,
@SerializedName("name")
val name: String = "",
@Deprecated("Please use resized url")
@SerializedName("coverImgUrl", alternate = ["picUrl"])
val coverImgUrl: String = "",
@SerializedName("creator")
val creator: ProfileData = ProfileData(),
@SerializedName("subscribed")
val subscribed: Boolean = false,
@SerializedName("trackCount")
val trackCount: Int = 0,
@SerializedName("userId")
val userId: Long = 0,
@SerializedName("playCount", alternate = ["playcount"])
val playCount: Long = 0,
@SerializedName("bookCount")
val bookCount: Long = 0,
@SerializedName("specialType")
val specialType: Int = 0,
@SerializedName("description")
val description: String = "",
@SerializedName("tags")
val tags: List<String> = emptyList(),
@SerializedName("highQuality")
val highQuality: Boolean = false,
@SerializedName("updateFrequency")
val updateFrequency: String = "",
@SerializedName("ToplistType")
val toplistType: String = "",
) {
@SerializedName("_songList")
var songList: List<SongData> = emptyList()
fun getSmallCover(): String {
return coverImgUrl.asSmallCover()
}
fun getLargeCover(): String {
return coverImgUrl.asLargeCover()
}
}

@ -0,0 +1,19 @@
package me.wcy.music.common.bean
import com.google.gson.annotations.SerializedName
/**
*
*/
data class QualityData(
@SerializedName("br")
val br: Int = 0,
@SerializedName("fid")
val fid: Int = 0,
@SerializedName("size")
val size: Int = 0,
@SerializedName("vd")
val vd: Int = 0,
@SerializedName("sr")
val sr: Int = 0
)

@ -0,0 +1,84 @@
package me.wcy.music.common.bean
import com.google.gson.annotations.SerializedName
data class SongData(
@SerializedName("id")
val id: Long = 0,
@SerializedName("name")
val name: String = "",
@SerializedName("pst")
val pst: Int = 0,
@SerializedName("t")
val t: Int = 0,
@SerializedName("ar")
val ar: List<ArtistData> = listOf(),
@SerializedName("pop")
val pop: Int = 0,
@SerializedName("st")
val st: Int = 0,
@SerializedName("rt")
val rt: String = "",
@SerializedName("fee")
val fee: Int = 0,
@SerializedName("v")
val v: Int = 0,
@SerializedName("cf")
val cf: String = "",
@SerializedName("al")
val al: AlbumData = AlbumData(),
@SerializedName("dt")
val dt: Long = 0,
@SerializedName("h")
val h: QualityData = QualityData(),
@SerializedName("m")
val m: QualityData = QualityData(),
@SerializedName("l")
val l: QualityData = QualityData(),
@SerializedName("sq")
val sq: QualityData = QualityData(),
@SerializedName("hr")
val hr: QualityData = QualityData(),
@SerializedName("cd")
val cd: String = "",
@SerializedName("no")
val no: Int = 0,
@SerializedName("ftype")
val ftype: Int = 0,
@SerializedName("djId")
val djId: Int = 0,
@SerializedName("copyright")
val copyright: Int = 0,
@SerializedName("s_id")
val sId: Int = 0,
@SerializedName("mark")
val mark: Int = 0,
@SerializedName("originCoverType")
val originCoverType: Int = 0,
@SerializedName("originSongSimpleData")
val originSongSimpleData: OriginSongSimpleData? = null,
@SerializedName("resourceState")
val resourceState: Boolean = false,
@SerializedName("version")
val version: Int = 0,
@SerializedName("single")
val single: Int = 0,
@SerializedName("rtype")
val rtype: Int = 0,
@SerializedName("mst")
val mst: Int = 0,
@SerializedName("cp")
val cp: Int = 0,
@SerializedName("mv")
val mv: Int = 0,
@SerializedName("publishTime")
val publishTime: Int = 0,
@SerializedName("reason")
val reason: String = "",
@SerializedName("tns")
val tns: List<String> = listOf(),
@SerializedName("recommendReason")
val recommendReason: String = "",
@SerializedName("alg")
val alg: String = ""
)

@ -0,0 +1,44 @@
package me.wcy.music.common.bean
import com.google.gson.annotations.SerializedName
data class SongUrlData(
@SerializedName("id")
val id: Long = 0,
@SerializedName("url")
val url: String = "",
@SerializedName("br")
val br: Int = 0,
@SerializedName("size")
val size: Int = 0,
@SerializedName("md5")
val md5: String = "",
@SerializedName("code")
val code: Int = 0,
@SerializedName("expi")
val expi: Int = 0,
@SerializedName("type")
val type: String = "",
@SerializedName("gain")
val gain: Double = 0.0,
@SerializedName("peak")
val peak: Int = 0,
@SerializedName("fee")
val fee: Int = 0,
@SerializedName("payed")
val payed: Int = 0,
@SerializedName("flag")
val flag: Int = 0,
@SerializedName("canExtend")
val canExtend: Boolean = false,
@SerializedName("level")
val level: String = "",
@SerializedName("encodeType")
val encodeType: String = "",
@SerializedName("urlSource")
val urlSource: Int = 0,
@SerializedName("rightSource")
val rightSource: Int = 0,
@SerializedName("time")
val time: Int = 0
)

@ -0,0 +1,20 @@
package me.wcy.music.common.dialog.songmenu
import android.view.View
/**
*
*/
interface MenuItem {
val name: String
fun onClick(view: View)
}
data class SimpleMenuItem(
override val name: String,
val onClick: (View) -> Unit = {}
) : MenuItem {
override fun onClick(view: View) {
onClick.invoke(view)
}
}

@ -0,0 +1,84 @@
package me.wcy.music.common.dialog.songmenu
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import com.blankj.utilcode.util.SizeUtils
import top.wangchenyan.common.widget.dialog.BottomDialog
import top.wangchenyan.common.widget.dialog.BottomDialogBuilder
import me.wcy.music.common.bean.SongData
import me.wcy.music.databinding.DialogSongMoreMenuBinding
import me.wcy.music.databinding.ItemSongMoreMenuBinding
import me.wcy.music.storage.db.entity.SongEntity
import me.wcy.music.utils.ImageUtils.loadCover
import me.wcy.music.utils.getSimpleArtist
/**
*
*/
class SongMoreMenuDialog {
private val context: Context
private var songEntity: SongEntity? = null
private var songData: SongData? = null
private val items = mutableListOf<MenuItem>()
constructor(context: Context, songEntity: SongEntity) {
this.context = context
this.songEntity = songEntity
}
constructor(context: Context, songData: SongData) {
this.context = context
this.songData = songData
}
fun setItems(items: List<MenuItem>) = apply {
this.items.apply {
clear()
addAll(items)
}
}
fun show() {
BottomDialogBuilder(context)
.contentViewBinding { dialog: BottomDialog, viewBinding: DialogSongMoreMenuBinding ->
bindSongInfo(viewBinding)
bindMenus(dialog, viewBinding)
}
.cancelable(true)
.build()
.show()
}
@SuppressLint("SetTextI18n")
private fun bindSongInfo(viewBinding: DialogSongMoreMenuBinding) {
val songEntity = songEntity
val songData = songData
if (songEntity != null) {
viewBinding.ivCover.loadCover(songEntity.getSmallCover(), SizeUtils.dp2px(4f))
viewBinding.tvTitle.text = "歌曲: ${songEntity.title}"
viewBinding.tvArtist.text = songEntity.artist
} else if (songData != null) {
viewBinding.ivCover.loadCover(songData.al.getSmallCover(), SizeUtils.dp2px(4f))
viewBinding.tvTitle.text = "歌曲: ${songData.name}"
viewBinding.tvArtist.text = songData.getSimpleArtist()
}
}
private fun bindMenus(dialog: BottomDialog, viewBinding: DialogSongMoreMenuBinding) {
viewBinding.menuContainer.removeAllViews()
items.forEach { item ->
ItemSongMoreMenuBinding.inflate(
LayoutInflater.from(context),
viewBinding.menuContainer,
true
).apply {
root.text = item.name
root.setOnClickListener {
dialog.dismiss()
item.onClick(it)
}
}
}
}
}

@ -0,0 +1,18 @@
package me.wcy.music.common.dialog.songmenu.items
import android.view.View
import me.wcy.music.common.bean.SongData
import me.wcy.music.common.dialog.songmenu.MenuItem
import top.wangchenyan.common.ext.toast
/**
*
*/
class AlbumMenuItem(private val songData: SongData) : MenuItem {
override val name: String
get() = "专辑: ${songData.al.name}"
override fun onClick(view: View) {
toast("敬请期待")
}
}

@ -0,0 +1,19 @@
package me.wcy.music.common.dialog.songmenu.items
import android.view.View
import me.wcy.music.common.bean.SongData
import me.wcy.music.common.dialog.songmenu.MenuItem
import me.wcy.music.utils.getSimpleArtist
import top.wangchenyan.common.ext.toast
/**
*
*/
class ArtistMenuItem(private val songData: SongData) : MenuItem {
override val name: String
get() = "歌手: ${songData.getSimpleArtist()}"
override fun onClick(view: View) {
toast("敬请期待")
}
}

@ -0,0 +1,28 @@
package me.wcy.music.common.dialog.songmenu.items
import android.view.View
import androidx.fragment.app.FragmentActivity
import kotlinx.coroutines.CoroutineScope
import me.wcy.music.common.bean.SongData
import me.wcy.music.common.dialog.songmenu.MenuItem
import me.wcy.music.mine.collect.song.CollectSongFragment
import top.wangchenyan.common.ext.findActivity
/**
*
*/
class CollectMenuItem(
private val scope: CoroutineScope,
private val songData: SongData
) : MenuItem {
override val name: String
get() = "收藏到歌单"
override fun onClick(view: View) {
val activity = view.context.findActivity()
if (activity is FragmentActivity) {
CollectSongFragment.newInstance(songData.id)
.show(activity.supportFragmentManager, CollectSongFragment.TAG)
}
}
}

@ -0,0 +1,18 @@
package me.wcy.music.common.dialog.songmenu.items
import android.view.View
import me.wcy.music.common.bean.SongData
import me.wcy.music.common.dialog.songmenu.MenuItem
import top.wangchenyan.common.ext.toast
/**
*
*/
class CommentMenuItem(private val songData: SongData) : MenuItem {
override val name: String
get() = "评论"
override fun onClick(view: View) {
toast("敬请期待")
}
}

@ -0,0 +1,49 @@
package me.wcy.music.common.dialog.songmenu.items
import android.view.View
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import me.wcy.music.account.service.UserServiceModule.Companion.userService
import me.wcy.music.common.bean.PlaylistData
import me.wcy.music.common.bean.SongData
import me.wcy.music.common.dialog.songmenu.MenuItem
import me.wcy.music.mine.MineApi
import top.wangchenyan.common.ext.findActivity
import top.wangchenyan.common.ext.showConfirmDialog
import top.wangchenyan.common.ext.toast
import top.wangchenyan.common.ui.activity.BaseActivity
/**
*
*/
class DeletePlaylistSongMenuItem(
private val playlistData: PlaylistData,
private val songData: SongData,
private val onDelete: (songData: SongData) -> Unit,
) : MenuItem {
override val name: String
get() = "删除"
override fun onClick(view: View) {
val activity = view.context.findActivity() as? BaseActivity
activity ?: return
if (activity.application.userService().isLogin().not()) return
activity.showConfirmDialog(message = "确定将所选音乐从列表删除?") {
activity.lifecycleScope.launch {
val result = runCatching {
MineApi.get().collectSong(playlistData.id, songData.id.toString(), "del")
}
if (result.isSuccess) {
val body = result.getOrThrow().body
if (body.code == 200) {
onDelete.invoke(songData)
} else {
toast(body.message)
}
} else {
toast(result.exceptionOrNull()?.message)
}
}
}
}
}

@ -0,0 +1,10 @@
package me.wcy.music.consts
/**
*
*/
object Consts {
const val PAGE_COUNT = 20
const val PAGE_COUNT_GRID = 21
const val SEARCH_HISTORY_COUNT = 20
}

@ -0,0 +1,35 @@
package me.wcy.music.consts
import top.wangchenyan.common.CommonApp
import java.io.File
/**
*
*/
object FilePath {
val httpCache: String
get() = "http".assembleExternalCachePath()
val logRootDir: String
get() = "log".assembleExternalFilePath()
val lrcDir: String
get() = "lrc".assembleExternalFilePath().mkdirs()
fun getLogPath(type: String): String {
return logRootDir + File.separator + type
}
private fun String.assembleExternalCachePath(): String {
return "${CommonApp.app.externalCacheDir}${File.separator}music_$this"
}
private fun String.assembleExternalFilePath(): String {
return CommonApp.app.getExternalFilesDir("music_$this")?.path ?: ""
}
private fun String.mkdirs() = apply {
val file = File(this)
if (!file.exists()) {
file.mkdirs()
}
}
}

@ -0,0 +1,14 @@
package me.wcy.music.consts
/**
*
*/
object PreferenceName {
val ACCOUNT = "account".assemble()
val CONFIG = "config".assemble()
val SEARCH = "search".assemble()
private fun String.assemble(): String {
return "music_$this"
}
}

@ -0,0 +1,17 @@
package me.wcy.music.consts
/**
*
*/
object RoutePath {
const val LOGIN = "/login"
const val PHONE_LOGIN = "/login/phone"
const val QRCODE_LOGIN = "/login/qrcode"
const val LOCAL_SONG = "/local_music"
const val RECOMMEND_SONG = "/recommend_song"
const val SEARCH = "/search"
const val PLAYLIST_DETAIL = "/playlist/detail"
const val PLAYLIST_SQUARE = "/playlist/square"
const val PLAYING = "/playing"
const val RANKING = "/ranking"
}

@ -0,0 +1,113 @@
package me.wcy.music.discover
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import me.wcy.music.common.bean.LrcDataWrap
import me.wcy.music.common.bean.SongData
import me.wcy.music.common.bean.SongUrlData
import me.wcy.music.discover.banner.BannerListData
import me.wcy.music.discover.playlist.detail.bean.PlaylistDetailData
import me.wcy.music.discover.playlist.detail.bean.SongListData
import me.wcy.music.discover.playlist.square.bean.PlaylistListData
import me.wcy.music.discover.playlist.square.bean.PlaylistTagListData
import me.wcy.music.discover.recommend.song.bean.RecommendSongListData
import me.wcy.music.net.HttpClient
import me.wcy.music.storage.preference.ConfigPreferences
import retrofit2.Retrofit
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Query
import top.wangchenyan.common.net.NetResult
import top.wangchenyan.common.net.gson.GsonConverterFactory
import top.wangchenyan.common.utils.GsonUtils
/**
*
*/
interface DiscoverApi {
@POST("recommend/songs")
suspend fun getRecommendSongs(): NetResult<RecommendSongListData>
@POST("recommend/resource")
suspend fun getRecommendPlaylists(): PlaylistListData
@POST("song/url/v1")
suspend fun getSongUrl(
@Query("id") id: Long,
@Query("level") level: String,
): NetResult<List<SongUrlData>>
@POST("lyric")
suspend fun getLrc(
@Query("id") id: Long,
): LrcDataWrap
@POST("playlist/detail")
suspend fun getPlaylistDetail(
@Query("id") id: Long,
): PlaylistDetailData
@POST("playlist/track/all")
suspend fun getPlaylistSongList(
@Query("id") id: Long,
@Query("limit") limit: Int? = null,
@Query("offset") offset: Int? = null,
@Query("timestamp") timestamp: Long? = null
): SongListData
@POST("playlist/hot")
suspend fun getPlaylistTagList(): PlaylistTagListData
@POST("top/playlist")
suspend fun getPlaylistList(
@Query("cat") cat: String,
@Query("limit") limit: Int,
@Query("offset") offset: Int,
): PlaylistListData
@POST("toplist")
suspend fun getRankingList(): PlaylistListData
@GET("banner?type=2")
suspend fun getBannerList(): BannerListData
companion object {
private const val SONG_LIST_LIMIT = 800
private val api: DiscoverApi by lazy {
val retrofit = Retrofit.Builder()
.baseUrl(ConfigPreferences.apiDomain)
.addConverterFactory(GsonConverterFactory.create(GsonUtils.gson, true))
.client(HttpClient.okHttpClient)
.build()
retrofit.create(DiscoverApi::class.java)
}
fun get(): DiscoverApi = api
suspend fun getFullPlaylistSongList(id: Long, timestamp: Long? = null): SongListData {
return withContext(Dispatchers.IO) {
var offset = 0
val list = mutableListOf<SongData>()
while (true) {
val songList = get().getPlaylistSongList(
id,
limit = SONG_LIST_LIMIT,
offset = offset,
timestamp = timestamp
)
if (songList.code != 200) {
throw Exception("code = ${songList.code}")
}
if (songList.songs.isEmpty()) {
break
}
list.addAll(songList.songs)
offset = list.size
}
return@withContext SongListData(200, list)
}
}
}
}

@ -0,0 +1,39 @@
package me.wcy.music.discover.banner
import com.google.gson.annotations.SerializedName
import me.wcy.music.common.bean.SongData
data class BannerData(
@SerializedName("pic")
val pic: String = "",
@SerializedName("targetId")
val targetId: Long = 0,
@SerializedName("targetType")
val targetType: Int = 0,
@SerializedName("titleColor")
val titleColor: String = "",
@SerializedName("typeTitle")
val typeTitle: String = "",
@SerializedName("url")
val url: String = "",
@SerializedName("exclusive")
val exclusive: Boolean = false,
@SerializedName("encodeId")
val encodeId: String = "",
@SerializedName("song")
val song: SongData? = null,
@SerializedName("bannerId")
val bannerId: String = "",
@SerializedName("alg")
val alg: String = "",
@SerializedName("scm")
val scm: String = "",
@SerializedName("requestId")
val requestId: String = "",
@SerializedName("showAdTag")
val showAdTag: Boolean = false,
@SerializedName("s_ctrp")
val sCtrp: String = "",
@SerializedName("bannerBizType")
val bannerBizType: String = ""
)

@ -0,0 +1,13 @@
package me.wcy.music.discover.banner
import com.google.gson.annotations.SerializedName
/**
*
*/
data class BannerListData(
@SerializedName("code")
val code: Int = 0,
@SerializedName("banners")
val banners: List<BannerData> = emptyList(),
)

@ -0,0 +1,296 @@
package me.wcy.music.discover.home
import android.view.View
import androidx.core.view.isVisible
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import com.blankj.utilcode.util.ScreenUtils
import com.blankj.utilcode.util.SizeUtils
import com.youth.banner.adapter.BannerImageAdapter
import com.youth.banner.config.IndicatorConfig
import com.youth.banner.holder.BannerImageHolder
import com.youth.banner.indicator.CircleIndicator
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import me.wcy.music.R
import me.wcy.music.account.service.UserService
import me.wcy.music.common.ApiDomainDialog
import me.wcy.music.common.BaseMusicFragment
import me.wcy.music.common.bean.PlaylistData
import me.wcy.music.consts.RoutePath
import me.wcy.music.databinding.FragmentDiscoverBinding
import me.wcy.music.discover.DiscoverApi
import me.wcy.music.discover.banner.BannerData
import me.wcy.music.discover.home.viewmodel.DiscoverViewModel
import me.wcy.music.discover.playlist.square.item.PlaylistItemBinder
import me.wcy.music.discover.ranking.discover.item.DiscoverRankingItemBinder
import me.wcy.music.main.MainActivity
import me.wcy.music.service.PlayerController
import me.wcy.music.storage.preference.ConfigPreferences
import me.wcy.music.utils.toMediaItem
import me.wcy.radapter3.RAdapter
import me.wcy.router.CRouter
import top.wangchenyan.common.ext.load
import top.wangchenyan.common.ext.toast
import top.wangchenyan.common.ext.viewBindings
import top.wangchenyan.common.utils.LaunchUtils
import top.wangchenyan.common.widget.decoration.SpacingDecoration
import javax.inject.Inject
/**
*
*/
@AndroidEntryPoint
class DiscoverFragment : BaseMusicFragment() {
private val viewBinding by viewBindings<FragmentDiscoverBinding>()
private val viewModel by viewModels<DiscoverViewModel>()
private val recommendPlaylistAdapter by lazy {
RAdapter<PlaylistData>()
}
private val rankingListAdapter by lazy {
RAdapter<PlaylistData>()
}
@Inject
lateinit var userService: UserService
@Inject
lateinit var playerController: PlayerController
override fun getRootView(): View {
return viewBinding.root
}
override fun isUseLoadSir(): Boolean {
return true
}
override fun getLoadSirTarget(): View {
return viewBinding.content
}
override fun onReload() {
super.onReload()
checkApiDomain(true)
}
override fun onLazyCreate() {
super.onLazyCreate()
initTitle()
initBanner()
initTopButton()
initRecommendPlaylist()
initRankingList()
checkApiDomain(false)
}
private fun initTitle() {
getTitleLayout()?.run {
addImageMenu(
R.drawable.ic_menu,
isDayNight = true,
isLeft = true
).setOnClickListener {
val activity = requireActivity()
if (activity is MainActivity) {
activity.openDrawer()
}
}
}
getTitleLayout()?.getContentView()?.setOnClickListener {
if (ApiDomainDialog.checkApiDomain(requireContext())) {
CRouter.with(requireActivity()).url(RoutePath.SEARCH).start()
}
}
}
private fun initBanner() {
viewBinding.banner.addBannerLifecycleObserver(this)
.setIndicator(CircleIndicator(requireContext()))
.setIndicatorGravity(IndicatorConfig.Direction.LEFT)
.setIndicatorMargins(IndicatorConfig.Margins().apply {
leftMargin = SizeUtils.dp2px(28f)
})
.setAdapter(object : BannerImageAdapter<BannerData>(emptyList()) {
override fun onBindView(
holder: BannerImageHolder?,
data: BannerData?,
position: Int,
size: Int
) {
holder?.imageView?.apply {
val padding = SizeUtils.dp2px(16f)
setPadding(padding, 0, padding, 0)
load(data?.pic ?: "", SizeUtils.dp2px(12f))
setOnClickListener {
data ?: return@setOnClickListener
if (data.song != null) {
playerController.addAndPlay(data.song.toMediaItem())
CRouter.with(context).url(RoutePath.PLAYING).start()
} else if (data.url.isNotEmpty()) {
LaunchUtils.launchBrowser(requireContext(), data.url)
} else if (data.targetId > 0) {
CRouter.with(requireActivity())
.url(RoutePath.PLAYLIST_DETAIL)
.extra("id", data.targetId)
.start()
}
}
}
}
})
lifecycleScope.launch {
viewModel.bannerList.collectLatest {
viewBinding.banner.isVisible = it.isNotEmpty()
viewBinding.bannerPlaceholder.isVisible = it.isEmpty()
if (it.isNotEmpty()) {
viewBinding.banner.setDatas(it)
}
}
}
}
private fun initTopButton() {
viewBinding.btnRecommendSong.setOnClickListener {
CRouter.with(requireActivity()).url(RoutePath.RECOMMEND_SONG).start()
}
viewBinding.btnPrivateFm.setOnClickListener {
toast("敬请期待")
}
viewBinding.btnRecommendPlaylist.setOnClickListener {
CRouter.with(requireActivity())
.url(RoutePath.PLAYLIST_SQUARE)
.start()
}
viewBinding.btnRank.setOnClickListener {
CRouter.with(requireActivity()).url(RoutePath.RANKING).start()
}
}
private fun initRecommendPlaylist() {
viewBinding.tvRecommendPlaylist.setOnClickListener {
CRouter.with(requireActivity())
.url(RoutePath.PLAYLIST_SQUARE)
.start()
}
val itemWidth = (ScreenUtils.getAppScreenWidth() - SizeUtils.dp2px(20f)) / 3
recommendPlaylistAdapter.register(PlaylistItemBinder(itemWidth, true, object :
PlaylistItemBinder.OnItemClickListener {
override fun onItemClick(item: PlaylistData) {
CRouter.with(requireActivity())
.url(RoutePath.PLAYLIST_DETAIL)
.extra("id", item.id)
.start()
}
override fun onPlayClick(item: PlaylistData) {
playPlaylist(item, 0)
}
}))
viewBinding.rvRecommendPlaylist.adapter = recommendPlaylistAdapter
viewBinding.rvRecommendPlaylist.layoutManager =
LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
viewBinding.rvRecommendPlaylist.addItemDecoration(
SpacingDecoration(SizeUtils.dp2px(10f))
)
val updateVisibility = {
if (userService.isLogin() && viewModel.recommendPlaylist.value.isNotEmpty()) {
viewBinding.tvRecommendPlaylist.isVisible = true
viewBinding.rvRecommendPlaylist.isVisible = true
} else {
viewBinding.tvRecommendPlaylist.isVisible = false
viewBinding.rvRecommendPlaylist.isVisible = false
}
}
lifecycleScope.launch {
userService.profile.collectLatest {
updateVisibility()
}
}
lifecycleScope.launch {
viewModel.recommendPlaylist.collectLatest { recommendPlaylist ->
updateVisibility()
recommendPlaylistAdapter.refresh(recommendPlaylist)
}
}
}
private fun initRankingList() {
viewBinding.tvRankingList.setOnClickListener {
CRouter.with(requireActivity())
.url(RoutePath.RANKING)
.start()
}
rankingListAdapter.register(DiscoverRankingItemBinder(object :
DiscoverRankingItemBinder.OnItemClickListener {
override fun onItemClick(item: PlaylistData, position: Int) {
CRouter.with(requireActivity())
.url(RoutePath.PLAYLIST_DETAIL)
.extra("id", item.id)
.start()
}
override fun onSongClick(item: PlaylistData, songPosition: Int) {
playPlaylist(item, songPosition)
}
}))
viewBinding.vpRankingList.apply {
val recyclerView = getChildAt(0) as RecyclerView
recyclerView.apply {
setPadding(SizeUtils.dp2px(16f), 0, SizeUtils.dp2px(16f), 0)
clipToPadding = false
}
orientation = ViewPager2.ORIENTATION_HORIZONTAL
adapter = rankingListAdapter
}
viewModel.rankingList.observe(this) { rankingList ->
rankingList ?: return@observe
if (viewModel.rankingList.value?.isNotEmpty() == true) {
viewBinding.tvRankingList.isVisible = true
viewBinding.vpRankingList.isVisible = true
} else {
viewBinding.tvRankingList.isVisible = false
viewBinding.vpRankingList.isVisible = false
}
rankingListAdapter.refresh(rankingList)
}
}
private fun checkApiDomain(isReload: Boolean) {
if (ConfigPreferences.apiDomain.isNotEmpty()) {
showLoadSirSuccess()
} else {
showLoadSirError("请先设置云音乐API域名")
if (isReload) {
ApiDomainDialog.checkApiDomain(requireContext())
}
}
}
private fun playPlaylist(playlistData: PlaylistData, songPosition: Int) {
lifecycleScope.launch {
showLoading()
kotlin.runCatching {
DiscoverApi.getFullPlaylistSongList(playlistData.id)
}.onSuccess { songListData ->
dismissLoading()
if (songListData.code == 200 && songListData.songs.isNotEmpty()) {
val songs = songListData.songs.map { it.toMediaItem() }
playerController.replaceAll(songs, songs.getOrElse(songPosition) { songs[0] })
}
}.onFailure {
dismissLoading()
}
}
}
}

@ -0,0 +1,130 @@
package me.wcy.music.discover.home.viewmodel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import me.wcy.music.account.service.UserService
import me.wcy.music.common.bean.PlaylistData
import me.wcy.music.discover.DiscoverApi
import me.wcy.music.discover.banner.BannerData
import me.wcy.music.net.NetCache
import me.wcy.music.storage.preference.ConfigPreferences
import top.wangchenyan.common.ext.toUnMutable
import javax.inject.Inject
/**
*
*/
@HiltViewModel
class DiscoverViewModel @Inject constructor(
private val userService: UserService
) : ViewModel() {
private val _bannerList = MutableStateFlow<List<BannerData>>(emptyList())
val bannerList = _bannerList.toUnMutable()
private val _recommendPlaylist = MutableStateFlow<List<PlaylistData>>(emptyList())
val recommendPlaylist = _recommendPlaylist.toUnMutable()
private val _rankingList = MutableLiveData<List<PlaylistData>>(emptyList())
val rankingList = _rankingList.toUnMutable()
init {
loadCache()
viewModelScope.launch {
userService.profile.collectLatest { profile ->
if (profile != null && ConfigPreferences.apiDomain.isNotEmpty()) {
loadRecommendPlaylist()
}
}
}
loadBanner()
loadRankingList()
}
private fun loadCache() {
viewModelScope.launch {
val list = NetCache.globalCache.getJsonArray(CACHE_KEY_BANNER, BannerData::class.java)
?: return@launch
_bannerList.value = list
}
if (userService.isLogin()) {
viewModelScope.launch {
val list = NetCache.userCache.getJsonArray(
CACHE_KEY_REC_PLAYLIST,
PlaylistData::class.java
) ?: return@launch
_recommendPlaylist.value = list
}
}
viewModelScope.launch {
val list =
NetCache.globalCache.getJsonArray(CACHE_KEY_RANKING_LIST, PlaylistData::class.java)
?: return@launch
_rankingList.postValue(list)
}
}
private fun loadBanner() {
viewModelScope.launch {
kotlin.runCatching {
DiscoverApi.get().getBannerList()
}.onSuccess {
_bannerList.value = it.banners
NetCache.globalCache.putJson(CACHE_KEY_BANNER, it.banners)
}.onFailure {
}
}
}
private fun loadRecommendPlaylist() {
viewModelScope.launch {
kotlin.runCatching {
DiscoverApi.get().getRecommendPlaylists()
}.onSuccess {
_recommendPlaylist.value = it.playlists
NetCache.userCache.putJson(CACHE_KEY_REC_PLAYLIST, it.playlists)
}.onFailure {
}
}
}
private fun loadRankingList() {
viewModelScope.launch {
kotlin.runCatching {
DiscoverApi.get().getRankingList()
}.onSuccess {
val rankingList = it.playlists.take(5)
val deferredList = mutableListOf<Deferred<*>>()
rankingList.forEach {
val d = async {
val songListRes = kotlin.runCatching {
DiscoverApi.get().getPlaylistSongList(it.id, limit = 3)
}
if (songListRes.getOrNull()?.code == 200) {
it.songList = songListRes.getOrThrow().songs
}
}
deferredList.add(d)
}
deferredList.forEach { d ->
d.await()
}
_rankingList.postValue(rankingList)
NetCache.globalCache.putJson(CACHE_KEY_RANKING_LIST, rankingList)
}.onFailure {
}
}
}
companion object {
const val CACHE_KEY_BANNER = "discover_banner"
const val CACHE_KEY_REC_PLAYLIST = "discover_recommend_playlist"
const val CACHE_KEY_RANKING_LIST = "discover_ranking_list"
}
}

@ -0,0 +1,238 @@
package me.wcy.music.discover.playlist.detail
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import com.blankj.utilcode.util.SizeUtils
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import me.wcy.music.R
import me.wcy.music.account.service.UserService
import me.wcy.music.common.BaseMusicFragment
import me.wcy.music.common.OnItemClickListener2
import me.wcy.music.common.bean.SongData
import me.wcy.music.common.dialog.songmenu.SongMoreMenuDialog
import me.wcy.music.common.dialog.songmenu.items.AlbumMenuItem
import me.wcy.music.common.dialog.songmenu.items.ArtistMenuItem
import me.wcy.music.common.dialog.songmenu.items.CollectMenuItem
import me.wcy.music.common.dialog.songmenu.items.CommentMenuItem
import me.wcy.music.common.dialog.songmenu.items.DeletePlaylistSongMenuItem
import me.wcy.music.consts.RoutePath
import me.wcy.music.databinding.FragmentPlaylistDetailBinding
import me.wcy.music.databinding.ItemPlaylistTagBinding
import me.wcy.music.discover.playlist.detail.item.PlaylistSongItemBinder
import me.wcy.music.discover.playlist.detail.viewmodel.PlaylistViewModel
import me.wcy.music.service.PlayerController
import me.wcy.music.utils.ConvertUtils
import me.wcy.music.utils.ImageUtils.loadCover
import me.wcy.music.utils.toMediaItem
import me.wcy.radapter3.RAdapter
import me.wcy.router.CRouter
import me.wcy.router.annotation.Route
import top.wangchenyan.common.ext.loadAvatar
import top.wangchenyan.common.ext.toast
import top.wangchenyan.common.ext.viewBindings
import top.wangchenyan.common.utils.StatusBarUtils
import javax.inject.Inject
/**
*
*/
@Route(RoutePath.PLAYLIST_DETAIL)
@AndroidEntryPoint
class PlaylistDetailFragment : BaseMusicFragment() {
private val viewBinding by viewBindings<FragmentPlaylistDetailBinding>()
private val viewModel by viewModels<PlaylistViewModel>()
private val adapter by lazy { RAdapter<SongData>() }
private var collectMenu: View? = null
@Inject
lateinit var userService: UserService
@Inject
lateinit var playerController: PlayerController
override fun getRootView(): View {
return viewBinding.root
}
override fun isUseLoadSir(): Boolean {
return true
}
override fun getLoadSirTarget(): View {
return viewBinding.coordinatorLayout
}
override fun onReload() {
super.onReload()
loadData()
}
override fun onLazyCreate() {
super.onLazyCreate()
val id = getRouteArguments().getLongExtra("id", 0)
val realtimeData = getRouteArguments().getBooleanExtra("realtime_data", false)
val isLike = getRouteArguments().getBooleanExtra("is_like", false)
if (id <= 0) {
finish()
return
}
viewModel.init(id, realtimeData, isLike)
initTitle()
initPlaylistInfo()
initSongList()
loadData()
}
private fun loadData() {
lifecycleScope.launch {
showLoadSirLoading()
val res = viewModel.loadData()
if (res.isSuccess()) {
showLoadSirSuccess()
} else {
showLoadSirError(res.msg)
}
}
}
private fun initTitle() {
collectMenu = getTitleLayout()?.addImageMenu(R.drawable.ic_favorite_selector, false)
collectMenu?.setOnClickListener {
viewModel.playlistData.value ?: return@setOnClickListener
userService.checkLogin(requireActivity()) {
lifecycleScope.launch {
showLoading()
val res = viewModel.collect()
dismissLoading()
if (res.isSuccess()) {
toast("操作成功")
} else {
toast(res.msg)
}
}
}
}
StatusBarUtils.getStatusBarHeight(requireActivity()) {
(viewBinding.titlePlaceholder.layoutParams as ViewGroup.MarginLayoutParams).apply {
topMargin = it
viewBinding.titlePlaceholder.requestLayout()
}
viewBinding.toolbarPlaceholder.layoutParams.height =
requireContext().resources.getDimensionPixelSize(R.dimen.common_title_bar_size) + it
viewBinding.toolbarPlaceholder.requestLayout()
}
viewBinding.appBarLayout.addOnOffsetChangedListener { appBarLayout, verticalOffset ->
getTitleLayout()?.updateScroll(-verticalOffset)
}
lifecycleScope.launch {
userService.profile.collectLatest { profile ->
updateCollectState()
}
}
}
private fun initPlaylistInfo() {
lifecycleScope.launch {
viewModel.playlistData.collectLatest { playlistData ->
playlistData ?: return@collectLatest
getTitleLayout()?.setTitleText(playlistData.name)
updateCollectState()
viewBinding.ivCover.loadCover(playlistData.getSmallCover(), SizeUtils.dp2px(6f))
viewBinding.tvPlayCount.text =
ConvertUtils.formatPlayCount(playlistData.playCount)
viewBinding.tvName.text = playlistData.name
viewBinding.ivCreatorAvatar.loadAvatar(playlistData.creator.avatarUrl)
viewBinding.tvCreatorName.text = playlistData.creator.nickname
viewBinding.tvSongCount.text = "(${playlistData.trackCount})"
viewBinding.flTags.removeAllViews()
playlistData.tags.forEach { tag ->
ItemPlaylistTagBinding.inflate(
LayoutInflater.from(context),
viewBinding.flTags,
true
).apply {
root.text = tag
}
}
viewBinding.tvDesc.text = playlistData.description
}
}
}
private fun initSongList() {
viewBinding.llPlayAll.setOnClickListener {
val songList = viewModel.songList.value.map { it.toMediaItem() }
if (songList.isNotEmpty()) {
playerController.replaceAll(songList, songList.first())
CRouter.with(requireContext()).url(RoutePath.PLAYING).start()
}
}
adapter.register(PlaylistSongItemBinder(object : OnItemClickListener2<SongData> {
override fun onItemClick(item: SongData, position: Int) {
val songList = viewModel.songList.value.map { it.toMediaItem() }
if (songList.isNotEmpty()) {
playerController.replaceAll(songList, songList[position])
CRouter.with(requireContext()).url(RoutePath.PLAYING).start()
}
}
override fun onMoreClick(item: SongData, position: Int) {
val items = mutableListOf(
CollectMenuItem(lifecycleScope, item),
CommentMenuItem(item),
ArtistMenuItem(item),
AlbumMenuItem(item)
)
val playlistData = viewModel.playlistData.value
if (playlistData != null && playlistData.creator.userId == userService.getUserId()) {
items.add(DeletePlaylistSongMenuItem(playlistData, item) {
viewModel.removeSong(it)
})
}
SongMoreMenuDialog(requireActivity(), item)
.setItems(items)
.show()
}
}))
viewBinding.recyclerView.adapter = adapter
lifecycleScope.launch {
viewModel.songList.collectLatest { songList ->
adapter.refresh(songList)
}
}
}
private fun updateCollectState() {
val playlistData = viewModel.playlistData.value
if (playlistData == null) {
collectMenu?.isVisible = false
return
}
if (userService.getUserId() == playlistData.userId) {
collectMenu?.isVisible = false
return
}
collectMenu?.isVisible = true
collectMenu?.isSelected = playlistData.subscribed
}
override fun getNavigationBarColor(): Int {
return R.color.play_bar_bg
}
}

@ -0,0 +1,14 @@
package me.wcy.music.discover.playlist.detail.bean
import com.google.gson.annotations.SerializedName
import me.wcy.music.common.bean.PlaylistData
/**
*
*/
data class PlaylistDetailData(
@SerializedName("code")
val code: Int = 0,
@SerializedName("playlist")
val playlist: PlaylistData = PlaylistData(),
)

@ -0,0 +1,14 @@
package me.wcy.music.discover.playlist.detail.bean
import com.google.gson.annotations.SerializedName
import me.wcy.music.common.bean.SongData
/**
*
*/
data class SongListData(
@SerializedName("code")
val code: Int = 0,
@SerializedName("songs")
val songs: List<SongData> = emptyList()
)

@ -0,0 +1,34 @@
package me.wcy.music.discover.playlist.detail.item
import me.wcy.music.common.OnItemClickListener2
import me.wcy.music.common.bean.SongData
import me.wcy.music.databinding.ItemPlaylistSongBinding
import me.wcy.music.utils.getSimpleArtist
import me.wcy.radapter3.RItemBinder
/**
*
*/
class PlaylistSongItemBinder(private val listener: OnItemClickListener2<SongData>) :
RItemBinder<ItemPlaylistSongBinding, SongData>() {
override fun onBind(viewBinding: ItemPlaylistSongBinding, item: SongData, position: Int) {
viewBinding.root.setOnClickListener {
listener.onItemClick(item, position)
}
viewBinding.ivMore.setOnClickListener {
listener.onMoreClick(item, position)
}
viewBinding.tvIndex.text = (position + 1).toString()
viewBinding.tvTitle.text = item.name
viewBinding.tvSubTitle.text = buildString {
append(item.getSimpleArtist())
append(" - ")
append(item.al.name)
item.originSongSimpleData?.let { originSong ->
append(" | 原唱: ")
append(originSong.artists.joinToString("/") { it.name })
}
}
}
}

@ -0,0 +1,93 @@
package me.wcy.music.discover.playlist.detail.viewmodel
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import me.wcy.music.common.bean.PlaylistData
import me.wcy.music.common.bean.SongData
import me.wcy.music.discover.DiscoverApi
import me.wcy.music.mine.MineApi
import me.wcy.music.service.likesong.LikeSongProcessor
import top.wangchenyan.common.ext.toUnMutable
import top.wangchenyan.common.model.CommonResult
import top.wangchenyan.common.net.apiCall
import top.wangchenyan.common.utils.ServerTime
import javax.inject.Inject
/**
*
*/
@HiltViewModel
class PlaylistViewModel @Inject constructor() : ViewModel() {
@Inject
lateinit var likeSongProcessor: LikeSongProcessor
private val _playlistData = MutableStateFlow<PlaylistData?>(null)
val playlistData = _playlistData.toUnMutable()
private val _songList = MutableStateFlow<List<SongData>>(emptyList())
val songList = _songList.toUnMutable()
private var playlistId = 0L
private var realtimeData = false
private var isLike = false
fun init(playlistId: Long, realtimeData: Boolean, isLike: Boolean) {
this.playlistId = playlistId
this.realtimeData = realtimeData
this.isLike = isLike
}
suspend fun loadData(): CommonResult<Unit> {
val detailRes = kotlin.runCatching {
DiscoverApi.get().getPlaylistDetail(playlistId)
}
val songListRes = kotlin.runCatching {
val timestamp = if (realtimeData) ServerTime.currentTimeMillis() else null
DiscoverApi.getFullPlaylistSongList(playlistId, timestamp = timestamp)
}
return if (detailRes.isSuccess.not() || detailRes.getOrThrow().code != 200) {
CommonResult.fail(msg = detailRes.exceptionOrNull()?.message)
} else if (songListRes.isSuccess.not() || songListRes.getOrThrow().code != 200) {
CommonResult.fail(msg = songListRes.exceptionOrNull()?.message)
} else {
_playlistData.value = detailRes.getOrThrow().playlist
_songList.value = songListRes.getOrThrow().songs
CommonResult.success(Unit)
}
}
suspend fun collect(): CommonResult<Unit> {
val playlistData = _playlistData.value ?: return CommonResult.fail()
if (playlistData.subscribed) {
val res = apiCall {
MineApi.get().collectPlaylist(playlistData.id, 2)
}
return if (res.isSuccess()) {
_playlistData.value = playlistData.copy(subscribed = false)
CommonResult.success(Unit)
} else {
CommonResult.fail(res.code, res.msg)
}
} else {
val res = apiCall {
MineApi.get().collectPlaylist(playlistData.id, 1)
}
return if (res.isSuccess()) {
_playlistData.value = playlistData.copy(subscribed = true)
CommonResult.success(Unit)
} else {
CommonResult.fail(res.code, res.msg)
}
}
}
fun removeSong(songData: SongData) {
val songList = _songList.value.toMutableList()
songList.remove(songData)
_songList.value = songList
if (isLike) {
likeSongProcessor.updateLikeSongList()
}
}
}

@ -0,0 +1,91 @@
package me.wcy.music.discover.playlist.square
import android.os.Bundle
import android.view.View
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import top.wangchenyan.common.ext.viewBindings
import top.wangchenyan.common.widget.pager.TabLayoutPager
import me.wcy.music.R
import me.wcy.music.common.BaseMusicFragment
import me.wcy.music.consts.RoutePath
import me.wcy.music.databinding.FragmentPlaylistSpuareBinding
import me.wcy.music.discover.playlist.square.viewmodel.PlaylistSquareViewModel
import me.wcy.router.annotation.Route
/**
*
*/
@Route(RoutePath.PLAYLIST_SQUARE)
@AndroidEntryPoint
class PlaylistSquareFragment : BaseMusicFragment() {
private val viewBinding by viewBindings<FragmentPlaylistSpuareBinding>()
private val viewModel by viewModels<PlaylistSquareViewModel>()
private var pager: TabLayoutPager? = null
override fun getRootView(): View {
return viewBinding.root
}
override fun isUseLoadSir(): Boolean {
return true
}
override fun getLoadSirTarget(): View {
return viewBinding.content
}
override fun onReload() {
super.onReload()
loadTagList()
}
override fun onLazyCreate() {
super.onLazyCreate()
initTab()
loadTagList()
}
private fun initTab() {
lifecycleScope.launch {
viewModel.tagList.collectLatest { tagList ->
if (tagList.isNotEmpty() && pager == null) {
pager = TabLayoutPager(
lifecycle,
childFragmentManager,
viewBinding.viewPage2,
viewBinding.tabLayout
).apply {
tagList.forEach { tag ->
addFragment(PlaylistTabFragment().apply {
arguments = Bundle().apply {
putString("tag", tag)
}
}, tag)
}
setup()
}
}
}
}
}
private fun loadTagList() {
lifecycleScope.launch {
showLoadSirLoading()
val res = viewModel.loadTagList()
if (res.isSuccess()) {
showLoadSirSuccess()
} else {
showLoadSirError(res.msg)
}
}
}
override fun getNavigationBarColor(): Int {
return R.color.play_bar_bg
}
}

@ -0,0 +1,87 @@
package me.wcy.music.discover.playlist.square
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.blankj.utilcode.util.ScreenUtils
import com.blankj.utilcode.util.SizeUtils
import dagger.hilt.android.AndroidEntryPoint
import top.wangchenyan.common.model.CommonResult
import top.wangchenyan.common.widget.decoration.GridSpacingDecoration
import me.wcy.music.common.SimpleMusicRefreshFragment
import me.wcy.music.common.bean.PlaylistData
import me.wcy.music.consts.Consts
import me.wcy.music.consts.RoutePath
import me.wcy.music.discover.DiscoverApi
import me.wcy.music.discover.playlist.square.item.PlaylistItemBinder
import me.wcy.radapter3.RAdapter
import me.wcy.router.CRouter
/**
*
*/
@AndroidEntryPoint
class PlaylistTabFragment : SimpleMusicRefreshFragment<PlaylistData>() {
private val cat by lazy {
getRouteArguments().getStringExtra("tag") ?: ""
}
override fun isShowTitle(): Boolean {
return false
}
override fun isRefreshEnabled(): Boolean {
return false
}
override fun getLayoutManager(): RecyclerView.LayoutManager {
return GridLayoutManager(requireContext(), 3)
}
override fun initAdapter(adapter: RAdapter<PlaylistData>) {
val itemWidth = (ScreenUtils.getAppScreenWidth() - SizeUtils.dp2px(52f)) / 3
adapter.register(
PlaylistItemBinder(
itemWidth,
false,
object : PlaylistItemBinder.OnItemClickListener {
override fun onItemClick(item: PlaylistData) {
CRouter.with(requireActivity())
.url(RoutePath.PLAYLIST_DETAIL)
.extra("id", item.id)
.start()
}
override fun onPlayClick(item: PlaylistData) {
}
})
)
}
override fun onLazyCreate() {
super.onLazyCreate()
getRecyclerView().apply {
val paddingHorizontal = SizeUtils.dp2px(16f)
val paddingVertical = SizeUtils.dp2px(10f)
setPadding(paddingHorizontal, paddingVertical, paddingHorizontal, paddingVertical)
clipToPadding = false
val spacing = SizeUtils.dp2px(10f)
addItemDecoration(GridSpacingDecoration(spacing, spacing))
}
}
override suspend fun getData(page: Int): CommonResult<List<PlaylistData>> {
val res = kotlin.runCatching {
DiscoverApi.get()
.getPlaylistList(
cat,
Consts.PAGE_COUNT_GRID,
(page - 1) * Consts.PAGE_COUNT_GRID
)
}
return if (res.getOrNull()?.code == 200) {
CommonResult.success(res.getOrThrow().playlists)
} else {
CommonResult.fail(msg = res.exceptionOrNull()?.message)
}
}
}

@ -0,0 +1,14 @@
package me.wcy.music.discover.playlist.square.bean
import com.google.gson.annotations.SerializedName
import me.wcy.music.common.bean.PlaylistData
/**
*
*/
data class PlaylistListData(
@SerializedName("code")
val code: Int = 0,
@SerializedName("playlists", alternate = ["playlist", "recommend", "list"])
val playlists: List<PlaylistData> = emptyList(),
)

@ -0,0 +1,24 @@
package me.wcy.music.discover.playlist.square.bean
import com.google.gson.annotations.SerializedName
data class PlaylistTagData(
@SerializedName("id")
val id: Long = 0,
@SerializedName("name")
val name: String = "",
@SerializedName("activity")
val activity: Boolean = false,
@SerializedName("hot")
val hot: Boolean = false,
@SerializedName("position")
val position: Int = 0,
@SerializedName("category")
val category: Int = 0,
@SerializedName("createTime")
val createTime: Long = 0,
@SerializedName("usedCount")
val usedCount: Long = 0,
@SerializedName("type")
val type: Int = 0
)

@ -0,0 +1,13 @@
package me.wcy.music.discover.playlist.square.bean
import com.google.gson.annotations.SerializedName
/**
*
*/
data class PlaylistTagListData(
@SerializedName("code")
val code: Int = 0,
@SerializedName("tags")
val tags: List<PlaylistTagData> = emptyList(),
)

@ -0,0 +1,45 @@
package me.wcy.music.discover.playlist.square.item
import androidx.core.view.isVisible
import com.blankj.utilcode.util.SizeUtils
import me.wcy.music.common.bean.PlaylistData
import me.wcy.music.databinding.ItemDiscoverPlaylistBinding
import me.wcy.music.utils.ConvertUtils
import me.wcy.music.utils.ImageUtils.loadCover
import me.wcy.radapter3.RItemBinder
/**
*
*/
class PlaylistItemBinder(
private val itemWidth: Int,
private val showPlayButton: Boolean,
private val listener: OnItemClickListener
) : RItemBinder<ItemDiscoverPlaylistBinding, PlaylistData>() {
override fun onBind(
viewBinding: ItemDiscoverPlaylistBinding,
item: PlaylistData,
position: Int
) {
viewBinding.root.setOnClickListener {
listener.onItemClick(item)
}
viewBinding.ivPlay.isVisible = showPlayButton
viewBinding.ivPlay.setOnClickListener {
listener.onPlayClick(item)
}
val lp = viewBinding.ivCover.layoutParams
lp.width = itemWidth
lp.height = itemWidth
viewBinding.ivCover.layoutParams = lp
viewBinding.ivCover.loadCover(item.getSmallCover(), SizeUtils.dp2px(6f))
viewBinding.tvPlayCount.text = ConvertUtils.formatPlayCount(item.playCount)
viewBinding.tvName.text = item.name
}
interface OnItemClickListener {
fun onItemClick(item: PlaylistData)
fun onPlayClick(item: PlaylistData)
}
}

@ -0,0 +1,29 @@
package me.wcy.music.discover.playlist.square.viewmodel
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import me.wcy.music.discover.DiscoverApi
import top.wangchenyan.common.ext.toUnMutable
import top.wangchenyan.common.model.CommonResult
/**
*
*/
class PlaylistSquareViewModel : ViewModel() {
private val _tagList = MutableStateFlow<List<String>>(emptyList())
val tagList = _tagList.toUnMutable()
suspend fun loadTagList(): CommonResult<Unit> {
val res = kotlin.runCatching {
DiscoverApi.get().getPlaylistTagList()
}
return if (res.getOrNull()?.code == 200) {
val list = res.getOrThrow().tags.map { it.name }.toMutableList()
list.add(0, "全部")
_tagList.value = list
CommonResult.success(Unit)
} else {
CommonResult.fail(msg = res.exceptionOrNull()?.message)
}
}
}

@ -0,0 +1,172 @@
package me.wcy.music.discover.ranking
import android.view.View
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.viewbinding.ViewBinding
import com.blankj.utilcode.util.ScreenUtils
import com.blankj.utilcode.util.SizeUtils
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import me.wcy.music.R
import me.wcy.music.common.BaseMusicFragment
import me.wcy.music.common.bean.PlaylistData
import me.wcy.music.consts.RoutePath
import me.wcy.music.databinding.FragmentRankingBinding
import me.wcy.music.discover.DiscoverApi
import me.wcy.music.discover.ranking.item.OfficialRankingItemBinder
import me.wcy.music.discover.ranking.item.RankingTitleItemBinding
import me.wcy.music.discover.ranking.item.SelectedRankingItemBinder
import me.wcy.music.discover.ranking.viewmodel.RankingViewModel
import me.wcy.music.service.PlayerController
import me.wcy.music.utils.toMediaItem
import me.wcy.radapter3.RAdapter
import me.wcy.radapter3.RItemBinder
import me.wcy.radapter3.RTypeMapper
import me.wcy.router.CRouter
import me.wcy.router.annotation.Route
import top.wangchenyan.common.ext.viewBindings
import javax.inject.Inject
/**
*
*/
@Route(RoutePath.RANKING)
@AndroidEntryPoint
class RankingFragment : BaseMusicFragment() {
private val viewBinding by viewBindings<FragmentRankingBinding>()
private val viewModel by viewModels<RankingViewModel>()
private val adapter by lazy { RAdapter<Any>() }
@Inject
lateinit var playerController: PlayerController
override fun getRootView(): View {
return viewBinding.root
}
override fun isUseLoadSir(): Boolean {
return true
}
override fun getLoadSirTarget(): View {
return viewBinding.recyclerView
}
override fun onReload() {
super.onReload()
loadData()
}
override fun onLazyCreate() {
super.onLazyCreate()
initView()
initDataObserver()
loadData()
}
private fun loadData() {
lifecycleScope.launch {
showLoadSirLoading()
val res = viewModel.loadData()
if (res.isSuccess()) {
showLoadSirSuccess()
} else {
showLoadSirError(res.msg)
}
}
}
private fun initView() {
val itemWidth = (ScreenUtils.getAppScreenWidth() - SizeUtils.dp2px(52f)) / 3
adapter.register(PlaylistData::class, object : RTypeMapper<PlaylistData> {
private val officialItemBinder = OfficialRankingItemBinder(object :
OfficialRankingItemBinder.OnItemClickListener {
override fun onItemClick(item: PlaylistData, position: Int) {
openRankingDetail(item)
}
override fun onPlayClick(item: PlaylistData, position: Int) {
playPlaylist(item)
}
})
private val selectedItemBinder = SelectedRankingItemBinder(itemWidth,
object : SelectedRankingItemBinder.OnItemClickListener {
override fun onItemClick(item: PlaylistData, position: Int) {
openRankingDetail(item)
}
override fun onPlayClick(item: PlaylistData, position: Int) {
playPlaylist(item)
}
override fun getFirstSelectedPosition(): Int {
val dataList = viewModel.rankingList.value ?: return -1
return dataList.indexOfFirst { it is PlaylistData && it.toplistType.isEmpty() }
}
})
override fun map(data: PlaylistData): RItemBinder<out ViewBinding, PlaylistData> {
return if (data.toplistType.isNotEmpty()) {
officialItemBinder
} else {
selectedItemBinder
}
}
})
adapter.register(RankingTitleItemBinding())
viewBinding.recyclerView.layoutManager = GridLayoutManager(requireContext(), 3).apply {
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
val dataList = viewModel.rankingList.value ?: return 1
val item = dataList.getOrNull(position) ?: return 1
return if (item is RankingViewModel.TitleData
|| (item is PlaylistData && item.toplistType.isNotEmpty())
) {
3
} else {
1
}
}
}
}
viewBinding.recyclerView.adapter = adapter
}
private fun initDataObserver() {
viewModel.rankingList.observe(this) { rankingList ->
rankingList ?: return@observe
adapter.refresh(rankingList)
}
}
private fun openRankingDetail(item: PlaylistData) {
CRouter.with(requireActivity())
.url(RoutePath.PLAYLIST_DETAIL)
.extra("id", item.id)
.start()
}
private fun playPlaylist(playlistData: PlaylistData) {
lifecycleScope.launch {
showLoading()
kotlin.runCatching {
DiscoverApi.getFullPlaylistSongList(playlistData.id)
}.onSuccess { songListData ->
dismissLoading()
if (songListData.code == 200 && songListData.songs.isNotEmpty()) {
val songs = songListData.songs.map { it.toMediaItem() }
playerController.replaceAll(songs, songs[0])
}
}.onFailure {
dismissLoading()
}
}
}
override fun getNavigationBarColor(): Int {
return R.color.play_bar_bg
}
}

@ -0,0 +1,61 @@
package me.wcy.music.discover.ranking.discover.item
import android.view.LayoutInflater
import androidx.core.view.get
import androidx.core.view.isEmpty
import androidx.core.view.isVisible
import com.blankj.utilcode.util.SizeUtils
import top.wangchenyan.common.ext.context
import me.wcy.music.common.bean.PlaylistData
import me.wcy.music.databinding.ItemDiscoverRankingBinding
import me.wcy.music.databinding.ItemDiscoverRankingSongBinding
import me.wcy.music.utils.ImageUtils.loadCover
import me.wcy.music.utils.getSimpleArtist
import me.wcy.radapter3.RItemBinder
/**
*
*/
class DiscoverRankingItemBinder(private val listener: OnItemClickListener) :
RItemBinder<ItemDiscoverRankingBinding, PlaylistData>() {
override fun onBind(
viewBinding: ItemDiscoverRankingBinding,
item: PlaylistData,
position: Int
) {
viewBinding.root.setOnClickListener {
listener.onItemClick(item, position)
}
viewBinding.tvName.text = item.name
if (viewBinding.llSongContainer.isEmpty()) {
for (i in 0 until 3) {
ItemDiscoverRankingSongBinding.inflate(
LayoutInflater.from(viewBinding.context),
viewBinding.llSongContainer,
true
)
}
}
for (i in 0 until 3) {
val itemBinding = ItemDiscoverRankingSongBinding.bind(viewBinding.llSongContainer[i])
item.songList.getOrNull(i)?.also { songItem ->
itemBinding.root.isVisible = true
itemBinding.root.setOnClickListener {
listener.onSongClick(item, i)
}
itemBinding.ivCover.loadCover(songItem.al.getSmallCover(), SizeUtils.dp2px(4f))
itemBinding.tvRank.text = (i + 1).toString()
itemBinding.tvTitle.text = songItem.name
itemBinding.tvSubTitle.text = songItem.getSimpleArtist()
} ?: {
itemBinding.root.isVisible = false
}
}
}
interface OnItemClickListener {
fun onItemClick(item: PlaylistData, position: Int)
fun onSongClick(item: PlaylistData, songPosition: Int)
}
}

@ -0,0 +1,72 @@
package me.wcy.music.discover.ranking.item
import android.view.LayoutInflater
import androidx.core.text.buildSpannedString
import androidx.core.view.get
import androidx.core.view.isEmpty
import androidx.core.view.isVisible
import com.blankj.utilcode.util.SizeUtils
import top.wangchenyan.common.ext.context
import top.wangchenyan.common.widget.CustomSpan.appendStyle
import me.wcy.music.common.bean.PlaylistData
import me.wcy.music.databinding.ItemOfficialRankingBinding
import me.wcy.music.databinding.ItemOfficialRankingSongBinding
import me.wcy.music.utils.ImageUtils.loadCover
import me.wcy.music.utils.getSimpleArtist
import me.wcy.radapter3.RItemBinder
import kotlin.reflect.KClass
/**
*
*/
class OfficialRankingItemBinder(private val listener: OnItemClickListener) :
RItemBinder<ItemOfficialRankingBinding, PlaylistData>() {
override fun onBind(
viewBinding: ItemOfficialRankingBinding,
item: PlaylistData,
position: Int
) {
viewBinding.root.setOnClickListener {
listener.onItemClick(item, position)
}
viewBinding.ivPlay.setOnClickListener {
listener.onPlayClick(item, position)
}
viewBinding.tvName.text = item.name
viewBinding.tvUpdateTime.text = item.updateFrequency
viewBinding.ivCover.loadCover(item.getSmallCover(), SizeUtils.dp2px(6f))
if (viewBinding.llSongContainer.isEmpty()) {
for (i in 0 until 3) {
ItemOfficialRankingSongBinding.inflate(
LayoutInflater.from(viewBinding.context),
viewBinding.llSongContainer,
true
)
}
}
for (i in 0 until 3) {
val songItem = item.songList.getOrNull(i)
val itemBinding = ItemOfficialRankingSongBinding.bind(viewBinding.llSongContainer[i])
if (songItem == null) {
itemBinding.root.isVisible = false
} else {
itemBinding.root.isVisible = true
itemBinding.tvIndex.text = (i + 1).toString()
itemBinding.tvTitle.text = buildSpannedString {
appendStyle(songItem.name, isBold = true)
append(" - ")
append(songItem.getSimpleArtist())
}
}
}
}
override fun getViewBindingClazz(): KClass<*> {
return ItemOfficialRankingBinding::class
}
interface OnItemClickListener {
fun onItemClick(item: PlaylistData, position: Int)
fun onPlayClick(item: PlaylistData, position: Int)
}
}

@ -0,0 +1,18 @@
package me.wcy.music.discover.ranking.item
import me.wcy.music.databinding.ItemRankingTitleBinding
import me.wcy.music.discover.ranking.viewmodel.RankingViewModel
import me.wcy.radapter3.RItemBinder
/**
*
*/
class RankingTitleItemBinding : RItemBinder<ItemRankingTitleBinding, RankingViewModel.TitleData>() {
override fun onBind(
viewBinding: ItemRankingTitleBinding,
item: RankingViewModel.TitleData,
position: Int
) {
viewBinding.root.text = item.title
}
}

@ -0,0 +1,58 @@
package me.wcy.music.discover.ranking.item
import android.view.Gravity
import android.widget.FrameLayout
import com.blankj.utilcode.util.SizeUtils
import me.wcy.music.common.bean.PlaylistData
import me.wcy.music.databinding.ItemSelectedRankingBinding
import me.wcy.music.utils.ImageUtils.loadCover
import me.wcy.radapter3.RItemBinder
import kotlin.reflect.KClass
/**
*
*/
class SelectedRankingItemBinder(
private val itemWidth: Int,
private val listener: OnItemClickListener
) : RItemBinder<ItemSelectedRankingBinding, PlaylistData>() {
override fun onBind(
viewBinding: ItemSelectedRankingBinding,
item: PlaylistData,
position: Int
) {
viewBinding.content.setOnClickListener {
listener.onItemClick(item, position)
}
viewBinding.ivPlay.setOnClickListener {
listener.onPlayClick(item, position)
}
val selectedPosition = position - listener.getFirstSelectedPosition()
val gravity = when (selectedPosition % 3) {
0 -> Gravity.START
1 -> Gravity.CENTER_HORIZONTAL
2 -> Gravity.END
else -> Gravity.START
}
val lp = viewBinding.content.layoutParams as FrameLayout.LayoutParams
if (lp.width != itemWidth || lp.height != itemWidth || lp.gravity != gravity) {
lp.width = itemWidth
lp.height = itemWidth
lp.gravity = gravity
viewBinding.content.layoutParams = lp
}
viewBinding.tvName.text = item.name
viewBinding.tvUpdateTime.text = item.updateFrequency
viewBinding.ivCover.loadCover(item.getSmallCover(), SizeUtils.dp2px(6f))
}
override fun getViewBindingClazz(): KClass<*> {
return ItemSelectedRankingBinding::class
}
interface OnItemClickListener {
fun onItemClick(item: PlaylistData, position: Int)
fun onPlayClick(item: PlaylistData, position: Int)
fun getFirstSelectedPosition(): Int
}
}

@ -0,0 +1,50 @@
package me.wcy.music.discover.ranking.viewmodel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import me.wcy.music.discover.DiscoverApi
import top.wangchenyan.common.ext.toUnMutable
import top.wangchenyan.common.model.CommonResult
/**
*
*/
class RankingViewModel : ViewModel() {
private val _rankingList = MutableLiveData<List<Any>>()
val rankingList = _rankingList.toUnMutable()
suspend fun loadData(): CommonResult<Unit> {
val rankingListRes = kotlin.runCatching {
DiscoverApi.get().getRankingList()
}
if (rankingListRes.getOrNull()?.code == 200) {
val rankingList = rankingListRes.getOrThrow().playlists
val officialList = rankingList.filter { it.toplistType.isNotEmpty() }
val selectedList = rankingList.filter { it.toplistType.isEmpty() }
val finalList =
listOf(TitleData("官方榜")) + officialList + listOf(TitleData("精选榜")) + selectedList
_rankingList.value = finalList
viewModelScope.launch {
officialList.forEach {
val d = async {
val songListRes = kotlin.runCatching {
DiscoverApi.get().getPlaylistSongList(it.id, limit = 3)
}
if (songListRes.getOrNull()?.code == 200) {
it.songList = songListRes.getOrThrow().songs
_rankingList.value = finalList
}
}
}
}
return CommonResult.success(Unit)
} else {
return CommonResult.fail(msg = rankingListRes.exceptionOrNull()?.message)
}
}
data class TitleData(val title: CharSequence)
}

@ -0,0 +1,109 @@
package me.wcy.music.discover.recommend.song
import android.view.View
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import me.wcy.music.R
import me.wcy.music.common.BaseMusicFragment
import me.wcy.music.common.OnItemClickListener2
import me.wcy.music.common.bean.SongData
import me.wcy.music.common.dialog.songmenu.SongMoreMenuDialog
import me.wcy.music.common.dialog.songmenu.items.AlbumMenuItem
import me.wcy.music.common.dialog.songmenu.items.ArtistMenuItem
import me.wcy.music.common.dialog.songmenu.items.CollectMenuItem
import me.wcy.music.common.dialog.songmenu.items.CommentMenuItem
import me.wcy.music.consts.RoutePath
import me.wcy.music.databinding.FragmentRecommendSongBinding
import me.wcy.music.discover.DiscoverApi
import me.wcy.music.discover.recommend.song.item.RecommendSongItemBinder
import me.wcy.music.service.PlayerController
import me.wcy.music.utils.toMediaItem
import me.wcy.radapter3.RAdapter
import me.wcy.router.CRouter
import me.wcy.router.annotation.Route
import top.wangchenyan.common.ext.viewBindings
import top.wangchenyan.common.net.apiCall
import javax.inject.Inject
/**
*
*/
@Route(RoutePath.RECOMMEND_SONG, needLogin = true)
@AndroidEntryPoint
class RecommendSongFragment : BaseMusicFragment() {
private val viewBinding by viewBindings<FragmentRecommendSongBinding>()
private val adapter by lazy {
RAdapter<SongData>()
}
@Inject
lateinit var playerController: PlayerController
override fun getRootView(): View {
return viewBinding.root
}
override fun isUseLoadSir(): Boolean {
return true
}
override fun getLoadSirTarget(): View {
return viewBinding.content
}
override fun onReload() {
super.onReload()
loadData()
}
override fun onLazyCreate() {
super.onLazyCreate()
adapter.register(RecommendSongItemBinder(object : OnItemClickListener2<SongData> {
override fun onItemClick(item: SongData, position: Int) {
val entityList = adapter.getDataList().map { it.toMediaItem() }
playerController.replaceAll(entityList, entityList[position])
CRouter.with(requireContext()).url(RoutePath.PLAYING).start()
}
override fun onMoreClick(item: SongData, position: Int) {
SongMoreMenuDialog(requireActivity(), item)
.setItems(
listOf(
CollectMenuItem(lifecycleScope, item),
CommentMenuItem(item),
ArtistMenuItem(item),
AlbumMenuItem(item)
)
)
.show()
}
}))
viewBinding.recyclerView.adapter = adapter
viewBinding.tvPlayAll.setOnClickListener {
val entityList = adapter.getDataList().map { it.toMediaItem() }
playerController.replaceAll(entityList, entityList.first())
CRouter.with(requireContext()).url(RoutePath.PLAYING).start()
}
loadData()
}
private fun loadData() {
lifecycleScope.launch {
showLoadSirLoading()
val res = apiCall { DiscoverApi.get().getRecommendSongs() }
if (res.isSuccessWithData()) {
showLoadSirSuccess()
adapter.refresh(res.getDataOrThrow().dailySongs)
} else {
showLoadSirError(res.msg)
}
}
}
override fun getNavigationBarColor(): Int {
return R.color.play_bar_bg
}
}

@ -0,0 +1,12 @@
package me.wcy.music.discover.recommend.song.bean
import com.google.gson.annotations.SerializedName
import me.wcy.music.common.bean.SongData
/**
*
*/
data class RecommendSongListData(
@SerializedName("dailySongs")
val dailySongs: List<SongData> = emptyList()
)

@ -0,0 +1,39 @@
package me.wcy.music.discover.recommend.song.item
import androidx.core.view.isVisible
import com.blankj.utilcode.util.SizeUtils
import me.wcy.music.common.OnItemClickListener2
import me.wcy.music.common.bean.SongData
import me.wcy.music.databinding.ItemRecommendSongBinding
import me.wcy.music.utils.ImageUtils.loadCover
import me.wcy.music.utils.getSimpleArtist
import me.wcy.radapter3.RItemBinder
/**
*
*/
class RecommendSongItemBinder(private val listener: OnItemClickListener2<SongData>) :
RItemBinder<ItemRecommendSongBinding, SongData>() {
override fun onBind(viewBinding: ItemRecommendSongBinding, item: SongData, position: Int) {
viewBinding.root.setOnClickListener {
listener.onItemClick(item, position)
}
viewBinding.ivMore.setOnClickListener {
listener.onMoreClick(item, position)
}
viewBinding.ivCover.loadCover(item.al.getSmallCover(), SizeUtils.dp2px(4f))
viewBinding.tvTitle.text = item.name
viewBinding.tvTag.isVisible = item.recommendReason.isNotEmpty()
viewBinding.tvTag.text = item.recommendReason
viewBinding.tvSubTitle.text = buildString {
append(item.getSimpleArtist())
append(" - ")
append(item.al.name)
item.originSongSimpleData?.let { originSong ->
append(" | 原唱: ")
append(originSong.artists.joinToString("/") { it.name })
}
}
}
}

@ -0,0 +1,10 @@
package me.wcy.music.download
/**
*
*/
class DownloadMusicInfo(
val title: String?,
val musicPath: String,
val coverPath: String?
)

@ -0,0 +1,25 @@
package me.wcy.music.download
import android.app.DownloadManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.LongSparseArray
import top.wangchenyan.common.ext.toast
import me.wcy.music.R
/**
* 下载完成广播接收器
*
*/
class DownloadReceiver : BroadcastReceiver() {
private val mDownloadList = LongSparseArray<DownloadMusicInfo>()
override fun onReceive(context: Context, intent: Intent) {
val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0)
val downloadMusicInfo = mDownloadList.get(id)
if (downloadMusicInfo != null) {
toast(context.getString(R.string.download_success, downloadMusicInfo.title))
}
}
}

@ -0,0 +1,24 @@
package me.wcy.music.ext
import android.app.Application
import android.content.BroadcastReceiver
import android.content.Context
import android.content.IntentFilter
import android.os.Build
import dagger.hilt.android.EntryPointAccessors
/**
*
*/
inline fun <reified T : Any> Application.accessEntryPoint(): T {
return EntryPointAccessors.fromApplication(this, T::class.java)
}
fun Context.registerReceiverCompat(receiver: BroadcastReceiver, filter: IntentFilter) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED)
} else {
registerReceiver(receiver, filter)
}
}

@ -0,0 +1,93 @@
package me.wcy.music.main
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.blankj.utilcode.util.AppUtils
import me.wcy.music.R
import top.wangchenyan.common.ui.activity.BaseActivity
class AboutActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_about)
supportFragmentManager.beginTransaction()
.replace(R.id.fragment_container, AboutFragment())
.commit()
}
class AboutFragment : PreferenceFragmentCompat() {
private val mVersion: Preference by lazy {
findPreference("version")!!
}
private val mShare: Preference by lazy {
findPreference("share")!!
}
private val mStar: Preference by lazy {
findPreference("star")!!
}
private val mWeibo: Preference by lazy {
findPreference("weibo")!!
}
private val mBlog: Preference by lazy {
findPreference("blog")!!
}
private val mGithub: Preference by lazy {
findPreference("github")!!
}
private val api: Preference by lazy {
findPreference("api")!!
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preference_about)
mVersion.summary = AppUtils.getAppVersionName()
mShare.setOnPreferenceClickListener {
share()
true
}
mStar.setOnPreferenceClickListener {
openUrl(getString(R.string.about_project_url))
true
}
mWeibo.setOnPreferenceClickListener {
openUrl(it.summary.toString())
true
}
mBlog.setOnPreferenceClickListener {
openUrl(it.summary.toString())
true
}
mGithub.setOnPreferenceClickListener {
openUrl(it.summary.toString())
true
}
api.setOnPreferenceClickListener {
openUrl("https://github.com/Binaryify/NeteaseCloudMusicApi")
true
}
}
private fun share() {
val intent = Intent(Intent.ACTION_SEND)
intent.type = "text/plain"
intent.putExtra(
Intent.EXTRA_TEXT,
getString(
R.string.share_app,
getString(R.string.app_name),
getString(R.string.about_project_url)
)
)
startActivity(Intent.createChooser(intent, getString(R.string.share)))
}
private fun openUrl(url: String) {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(url)
startActivity(intent)
}
}
}

@ -0,0 +1,218 @@
package me.wcy.music.main
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import androidx.annotation.DrawableRes
import androidx.appcompat.app.AlertDialog
import androidx.core.view.GravityCompat
import androidx.lifecycle.lifecycleScope
import com.google.android.material.navigation.NavigationView
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import me.wcy.music.R
import me.wcy.music.account.service.UserService
import me.wcy.music.common.ApiDomainDialog
import me.wcy.music.common.BaseMusicActivity
import me.wcy.music.consts.RoutePath
import me.wcy.music.databinding.ActivityMainBinding
import me.wcy.music.databinding.NavigationHeaderBinding
import me.wcy.music.databinding.TabItemBinding
import me.wcy.music.service.MusicService
import me.wcy.music.service.PlayServiceModule
import me.wcy.music.service.PlayServiceModule.playerController
import me.wcy.music.utils.QuitTimer
import me.wcy.music.utils.TimeUtils
import me.wcy.router.CRouter
import top.wangchenyan.common.ext.showConfirmDialog
import top.wangchenyan.common.ext.toast
import top.wangchenyan.common.ext.viewBindings
import top.wangchenyan.common.widget.pager.CustomTabPager
import javax.inject.Inject
/**
*
*/
@AndroidEntryPoint
class MainActivity : BaseMusicActivity() {
private val viewBinding by viewBindings<ActivityMainBinding>()
private val quitTimer by lazy {
QuitTimer(onTimerListener)
}
private var timerItem: MenuItem? = null
@Inject
lateinit var userService: UserService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
PlayServiceModule.isPlayerReady.observe(this) { isReady ->
if (isReady) {
setContentView(viewBinding.root)
CustomTabPager(lifecycle, supportFragmentManager, viewBinding.viewPager).apply {
NaviTab.ALL.onEach {
val tabItem = getTabItem(it.icon, it.name)
addFragment(it.newFragment(), tabItem.root)
}
setScrollable(false)
setup()
}
initDrawer()
parseIntent()
}
}
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
setIntent(intent)
parseIntent()
}
private fun initDrawer() {
val navigationHeaderBinding = NavigationHeaderBinding.inflate(
LayoutInflater.from(this),
viewBinding.navigationView,
false
)
viewBinding.navigationView.addHeaderView(navigationHeaderBinding.root)
viewBinding.navigationView.setNavigationItemSelectedListener(onMenuSelectListener)
lifecycleScope.launch {
userService.profile.collectLatest { profile ->
val menuLogout = viewBinding.navigationView.menu.findItem(R.id.action_logout)
menuLogout.isVisible = profile != null
}
}
}
fun openDrawer() {
if (viewBinding.drawerLayout.isDrawerOpen(GravityCompat.START).not()) {
viewBinding.drawerLayout.openDrawer(GravityCompat.START)
}
}
private fun parseIntent() {
val intent = intent
if (intent.hasExtra(MusicService.EXTRA_NOTIFICATION)) {
if (application.playerController().currentSong.value != null) {
CRouter.with(this).url(RoutePath.PLAYING).start()
}
setIntent(Intent())
}
}
private val onMenuSelectListener = object : NavigationView.OnNavigationItemSelectedListener {
override fun onNavigationItemSelected(item: MenuItem): Boolean {
viewBinding.drawerLayout.closeDrawers()
lifecycleScope.launch {
delay(1000)
item.isChecked = false
}
when (item.itemId) {
R.id.action_domain_setting -> {
ApiDomainDialog(this@MainActivity).show()
return true
}
R.id.action_setting -> {
CRouter.with(this@MainActivity).url("/settings").start()
return true
}
R.id.action_timer -> {
timerDialog()
return true
}
R.id.action_logout -> {
logout()
return true
}
R.id.action_exit -> {
exitApp()
return true
}
R.id.action_about -> {
startActivity(Intent(this@MainActivity, AboutActivity::class.java))
return true
}
}
return false
}
}
private val onTimerListener = object : QuitTimer.OnTimerListener {
override fun onTick(remain: Long) {
if (timerItem == null) {
timerItem = viewBinding.navigationView.menu.findItem(R.id.action_timer)
}
val title = getString(R.string.menu_timer)
timerItem?.title = if (remain == 0L) {
title
} else {
TimeUtils.formatTime("$title(mm:ss)", remain)
}
}
override fun onTimeEnd() {
exitApp()
}
}
private fun timerDialog() {
AlertDialog.Builder(this)
.setTitle(R.string.menu_timer)
.setItems(resources.getStringArray(R.array.timer_text)) { dialog: DialogInterface?, which: Int ->
val times = resources.getIntArray(R.array.timer_int)
startTimer(times[which])
}
.show()
}
private fun startTimer(minute: Int) {
quitTimer.start((minute * 60 * 1000).toLong())
if (minute > 0) {
toast(getString(R.string.timer_set, minute.toString()))
} else {
toast(R.string.timer_cancel)
}
}
private fun logout() {
showConfirmDialog(message = "确认退出登录?") {
lifecycleScope.launch {
userService.logout()
}
}
}
private fun exitApp() {
application.playerController().stop()
finish()
}
private fun getTabItem(@DrawableRes icon: Int, text: CharSequence): TabItemBinding {
val binding = TabItemBinding.inflate(layoutInflater, viewBinding.tabBar, true)
binding.ivIcon.setImageResource(icon)
binding.tvTitle.text = text
return binding
}
override fun getNavigationBarColor(): Int {
return R.color.tab_bg
}
override fun onDestroy() {
super.onDestroy()
quitTimer.stop()
}
}

@ -0,0 +1,47 @@
package me.wcy.music.main
import androidx.annotation.DrawableRes
import androidx.fragment.app.Fragment
import me.wcy.music.R
import me.wcy.music.discover.home.DiscoverFragment
import me.wcy.music.mine.home.MineFragment
sealed class NaviTab private constructor(
val id: String,
@DrawableRes
val icon: Int,
val name: String,
val newFragment: () -> Fragment
) {
object Discover : NaviTab(
"discover",
R.drawable.ic_tab_discover,
"发现",
{ DiscoverFragment() }
)
object Mine : NaviTab(
"mine",
R.drawable.ic_tab_mine,
"我的",
{ MineFragment() }
)
fun getPosition(): Int {
return ALL.indexOf(this)
}
companion object {
val ALL: List<NaviTab> = listOf(
Discover, Mine
)
fun findByPosition(position: Int): NaviTab? {
return ALL.getOrNull(position)
}
fun findByName(name: String): NaviTab? {
return ALL.find { it.id == name }
}
}
}

@ -0,0 +1,193 @@
package me.wcy.music.main
import android.content.ActivityNotFoundException
import android.content.Intent
import android.media.audiofx.AudioEffect
import android.os.Bundle
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import dagger.hilt.android.AndroidEntryPoint
import me.wcy.music.R
import me.wcy.music.common.BaseMusicActivity
import me.wcy.music.common.DarkModeService
import me.wcy.music.consts.PreferenceName
import me.wcy.music.service.PlayerController
import me.wcy.music.storage.preference.ConfigPreferences
import me.wcy.music.utils.MusicUtils
import me.wcy.router.annotation.Route
import top.wangchenyan.common.ext.toast
import javax.inject.Inject
@Route("/settings")
@AndroidEntryPoint
class SettingsActivity : BaseMusicActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
val fragment = SettingsFragment()
supportFragmentManager.beginTransaction()
.replace(R.id.fragment_container, fragment)
.commitAllowingStateLoss()
}
@AndroidEntryPoint
class SettingsFragment : PreferenceFragmentCompat() {
private val darkMode: Preference by lazy {
findPreference(getString(R.string.setting_key_dark_mode))!!
}
private val playSoundQuality: Preference by lazy {
findPreference(getString(R.string.setting_key_play_sound_quality))!!
}
private val soundEffect: Preference by lazy {
findPreference(getString(R.string.setting_key_sound_effect))!!
}
private val downloadSoundQuality: Preference by lazy {
findPreference(getString(R.string.setting_key_download_sound_quality))!!
}
private val filterSize: Preference by lazy {
findPreference(getString(R.string.setting_key_filter_size))!!
}
private val filterTime: Preference by lazy {
findPreference(getString(R.string.setting_key_filter_time))!!
}
@Inject
lateinit var playerController: PlayerController
@Inject
lateinit var darkModeService: DarkModeService
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
preferenceManager.sharedPreferencesName = PreferenceName.CONFIG
addPreferencesFromResource(R.xml.preference_setting)
initDarkMode()
initPlaySoundQuality()
initSoundEffect()
initDownloadSoundQuality()
initFilter()
}
private fun initDarkMode() {
darkMode.summary = getSummary(
ConfigPreferences.darkMode,
R.array.dark_mode_entries,
R.array.dark_mode_values
)
darkMode.setOnPreferenceChangeListener { preference, newValue ->
val value = newValue.toString()
filterSize.summary = getSummary(
value,
R.array.dark_mode_entries,
R.array.dark_mode_values
)
val mode = DarkModeService.DarkMode.fromValue(value)
darkModeService.setDarkMode(mode)
true
}
}
private fun initPlaySoundQuality() {
playSoundQuality.summary = getSummary(
ConfigPreferences.playSoundQuality,
R.array.sound_quality_entries,
R.array.sound_quality_entry_values
)
playSoundQuality.setOnPreferenceChangeListener { preference, newValue ->
val value = newValue.toString()
playSoundQuality.summary = getSummary(
value,
R.array.sound_quality_entries,
R.array.sound_quality_entry_values
)
true
}
}
private fun initSoundEffect() {
soundEffect.setOnPreferenceClickListener {
startEqualizer()
true
}
}
private fun initDownloadSoundQuality() {
downloadSoundQuality.summary = getSummary(
ConfigPreferences.downloadSoundQuality,
R.array.sound_quality_entries,
R.array.sound_quality_entry_values
)
downloadSoundQuality.setOnPreferenceChangeListener { preference, newValue ->
val value = newValue.toString()
downloadSoundQuality.summary = getSummary(
value,
R.array.sound_quality_entries,
R.array.sound_quality_entry_values
)
true
}
}
private fun initFilter() {
filterSize.summary = getSummary(
ConfigPreferences.filterSize,
R.array.filter_size_entries,
R.array.filter_size_entry_values
)
filterSize.setOnPreferenceChangeListener { preference, newValue ->
val value = newValue.toString()
filterSize.summary = getSummary(
value,
R.array.filter_size_entries,
R.array.filter_size_entry_values
)
true
}
filterTime.summary = getSummary(
ConfigPreferences.filterTime,
R.array.filter_time_entries,
R.array.filter_time_entry_values
)
filterTime.setOnPreferenceChangeListener { preference, newValue ->
val value = newValue.toString()
filterTime.summary = getSummary(
value,
R.array.filter_time_entries,
R.array.filter_time_entry_values
)
true
}
}
private fun startEqualizer() {
if (MusicUtils.isAudioControlPanelAvailable(requireContext())) {
val intent = Intent()
val packageName = requireContext().packageName
intent.action = AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL
intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, packageName)
intent.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC)
intent.putExtra(
AudioEffect.EXTRA_AUDIO_SESSION,
playerController.getAudioSessionId()
)
try {
startActivity(intent)
} catch (e: ActivityNotFoundException) {
e.printStackTrace()
toast(R.string.device_not_support)
}
} else {
toast(R.string.device_not_support)
}
}
private fun getSummary(value: String, entries: Int, values: Int): String {
val entryArray = resources.getStringArray(entries)
val valueArray = resources.getStringArray(values)
val index = valueArray.indexOf(value).coerceAtLeast(0)
return entryArray[index]
}
}
}

@ -0,0 +1,393 @@
package me.wcy.music.main.playing
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.BitmapFactory
import android.media.AudioManager
import android.os.Bundle
import android.text.format.DateUtils
import android.util.Log
import android.widget.SeekBar
import android.widget.SeekBar.OnSeekBarChangeListener
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.MediaItem
import com.gyf.immersionbar.ImmersionBar
import dagger.hilt.android.AndroidEntryPoint
import jp.wasabeef.blurry.Blurry
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import me.wcy.lrcview.LrcView
import me.wcy.music.R
import me.wcy.music.common.BaseMusicActivity
import me.wcy.music.consts.RoutePath
import me.wcy.music.databinding.ActivityPlayingBinding
import me.wcy.music.discover.DiscoverApi
import me.wcy.music.ext.registerReceiverCompat
import me.wcy.music.main.playlist.CurrentPlaylistFragment
import me.wcy.music.service.PlayMode
import me.wcy.music.service.PlayerController
import me.wcy.music.service.likesong.LikeSongProcessor
import me.wcy.music.storage.LrcCache
import me.wcy.music.storage.preference.ConfigPreferences
import me.wcy.music.utils.TimeUtils
import me.wcy.music.utils.getDuration
import me.wcy.music.utils.getSongId
import me.wcy.music.utils.isLocal
import me.wcy.router.annotation.Route
import top.wangchenyan.common.ext.toast
import top.wangchenyan.common.ext.viewBindings
import top.wangchenyan.common.net.apiCall
import top.wangchenyan.common.utils.LaunchUtils
import top.wangchenyan.common.utils.StatusBarUtils
import top.wangchenyan.common.utils.image.ImageUtils
import java.io.File
import javax.inject.Inject
import kotlin.math.abs
/**
*
*/
@Route(RoutePath.PLAYING)
@AndroidEntryPoint
class PlayingActivity : BaseMusicActivity() {
private val viewBinding by viewBindings<ActivityPlayingBinding>()
@Inject
lateinit var playerController: PlayerController
@Inject
lateinit var likeSongProcessor: LikeSongProcessor
private val audioManager by lazy {
getSystemService(Context.AUDIO_SERVICE) as AudioManager
}
private val defaultCoverBitmap by lazy {
BitmapFactory.decodeResource(resources, R.drawable.bg_playing_default_cover)
}
private var loadLrcJob: Job? = null
private var lastProgress = 0
private var isDraggingProgress = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(viewBinding.root)
initTitle()
initVolume()
initCover()
initLrc()
initActions()
initPlayControl()
initData()
switchCoverLrc(true)
}
override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
if (StatusBarUtils.isSupportStatusBarTransparent()) {
ImmersionBar.with(this)
.transparentNavigationBar()
.navigationBarDarkIcon(false)
.init()
}
}
private fun initTitle() {
viewBinding.ivClose.setOnClickListener {
onBackPressed()
}
}
private fun initVolume() {
viewBinding.sbVolume.max = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
viewBinding.sbVolume.progress = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
val filter = IntentFilter(VOLUME_CHANGED_ACTION)
registerReceiverCompat(volumeReceiver, filter)
}
private fun initCover() {
val playState = playerController.playState.value
viewBinding.albumCoverView.initNeedle(playState.isPlaying)
viewBinding.clAlbumCover.setOnClickListener {
switchCoverLrc(false)
}
}
private fun initLrc() {
viewBinding.lrcView.setDraggable(true) { view, time ->
val playState = playerController.playState.value
if (playState.isPlaying || playState.isPausing) {
playerController.seekTo(time.toInt())
if (playState.isPausing) {
playerController.playPause()
}
return@setDraggable true
}
return@setDraggable false
}
viewBinding.lrcView.setOnTapListener { view: LrcView?, x: Float, y: Float ->
switchCoverLrc(true)
}
}
private fun initActions() {
viewBinding.ivLike.setOnClickListener {
lifecycleScope.launch {
val song = playerController.currentSong.value ?: return@launch
val res = likeSongProcessor.like(this@PlayingActivity, song.getSongId())
if (res.isSuccess()) {
updateOnlineActionsState(song)
} else {
toast(res.msg)
}
}
}
viewBinding.ivDownload.setOnClickListener {
lifecycleScope.launch {
val song = playerController.currentSong.value ?: return@launch
val res = apiCall {
DiscoverApi.get()
.getSongUrl(song.getSongId(), ConfigPreferences.downloadSoundQuality)
}
if (res.isSuccessWithData() && res.getDataOrThrow().isNotEmpty()) {
val url = res.getDataOrThrow().first().url
LaunchUtils.launchBrowser(this@PlayingActivity, url)
} else {
toast(res.msg)
}
}
}
}
private fun initPlayControl() {
lifecycleScope.launch {
playerController.playMode.collectLatest { playMode ->
viewBinding.ivMode.setImageLevel(playMode.value)
}
}
val lp = viewBinding.navigationBarPlaceholder.layoutParams
lp.height = ImmersionBar.getNavigationBarHeight(this)
viewBinding.navigationBarPlaceholder.layoutParams = lp
viewBinding.ivMode.setOnClickListener {
switchPlayMode()
}
viewBinding.ivPlay.setOnClickListener {
playerController.playPause()
}
viewBinding.ivPrev.setOnClickListener {
playerController.prev()
}
viewBinding.ivNext.setOnClickListener {
playerController.next()
}
viewBinding.ivPlaylist.setOnClickListener {
CurrentPlaylistFragment.newInstance()
.show(supportFragmentManager, CurrentPlaylistFragment.TAG)
}
viewBinding.sbProgress.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
if (abs(progress - lastProgress) >= DateUtils.SECOND_IN_MILLIS) {
viewBinding.tvCurrentTime.text = TimeUtils.formatMs(progress.toLong())
lastProgress = progress
}
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {
isDraggingProgress = true
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
seekBar ?: return
isDraggingProgress = false
val playState = playerController.playState.value
if (playState.isPlaying || playState.isPausing) {
val progress = seekBar.progress
playerController.seekTo(progress)
if (viewBinding.lrcView.hasLrc()) {
viewBinding.lrcView.updateTime(progress.toLong())
}
} else {
seekBar.progress = 0
}
}
})
viewBinding.sbVolume.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
seekBar ?: return
audioManager.setStreamVolume(
AudioManager.STREAM_MUSIC,
seekBar.progress,
AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE
)
}
})
}
private fun initData() {
playerController.currentSong.observe(this) { song ->
if (song != null) {
viewBinding.tvTitle.text = song.mediaMetadata.title
viewBinding.tvArtist.text = song.mediaMetadata.artist
viewBinding.sbProgress.max = song.mediaMetadata.getDuration().toInt()
viewBinding.sbProgress.progress = playerController.playProgress.value.toInt()
viewBinding.sbProgress.secondaryProgress = 0
lastProgress = 0
viewBinding.tvCurrentTime.text =
TimeUtils.formatMs(playerController.playProgress.value)
viewBinding.tvTotalTime.text = TimeUtils.formatMs(song.mediaMetadata.getDuration())
updateCover(song)
updateLrc(song)
viewBinding.albumCoverView.reset()
val playState = playerController.playState.value
if (playState.isPlaying || playState.isPreparing) {
viewBinding.ivPlay.isSelected = true
viewBinding.albumCoverView.start()
} else {
viewBinding.ivPlay.isSelected = false
viewBinding.albumCoverView.pause()
}
updateOnlineActionsState(song)
} else {
finish()
}
}
lifecycleScope.launch {
playerController.playState.collectLatest { playState ->
if (playState.isPlaying) {
viewBinding.ivPlay.isSelected = true
viewBinding.albumCoverView.start()
} else {
viewBinding.ivPlay.isSelected = false
viewBinding.albumCoverView.pause()
}
}
}
lifecycleScope.launch {
playerController.playProgress.collectLatest { progress ->
if (isDraggingProgress.not()) {
viewBinding.sbProgress.progress = progress.toInt()
}
if (viewBinding.lrcView.hasLrc()) {
viewBinding.lrcView.updateTime(progress)
}
}
}
lifecycleScope.launch {
playerController.bufferingPercent.collectLatest { percent ->
viewBinding.sbProgress.secondaryProgress =
viewBinding.sbProgress.max * percent / 100
}
}
}
private fun updateCover(song: MediaItem) {
viewBinding.albumCoverView.setCoverBitmap(defaultCoverBitmap)
viewBinding.ivPlayingBg.setImageResource(R.drawable.bg_playing_default)
ImageUtils.loadBitmap(song.mediaMetadata.artworkUri.toString()) {
if (it.isSuccessWithData()) {
val bitmap = it.getDataOrThrow()
viewBinding.albumCoverView.setCoverBitmap(bitmap)
Blurry.with(this).sampling(10).from(bitmap).into(viewBinding.ivPlayingBg)
}
}
}
private fun updateLrc(song: MediaItem) {
loadLrcJob?.cancel()
loadLrcJob = null
val lrcPath = LrcCache.getLrcFilePath(song)
if (lrcPath?.isNotEmpty() == true) {
loadLrc(lrcPath)
return
}
viewBinding.lrcView.loadLrc("")
if (song.isLocal()) {
setLrcLabel("暂无歌词")
} else {
setLrcLabel("歌词加载中…")
loadLrcJob = lifecycleScope.launch {
kotlin.runCatching {
val lrcWrap = DiscoverApi.get().getLrc(song.getSongId())
if (lrcWrap.code == 200 && lrcWrap.lrc.isValid()) {
lrcWrap.lrc
} else {
throw IllegalStateException("lrc is invalid")
}
}.onSuccess {
val file = LrcCache.saveLrcFile(song, it.lyric)
loadLrc(file.path)
}.onFailure {
Log.e(TAG, "load lrc error", it)
setLrcLabel("歌词加载失败")
}
}
}
}
private fun loadLrc(path: String) {
val file = File(path)
viewBinding.lrcView.loadLrc(file)
}
private fun setLrcLabel(label: String) {
viewBinding.lrcView.setLabel(label)
}
private fun switchCoverLrc(showCover: Boolean) {
viewBinding.clAlbumCover.isVisible = showCover
viewBinding.lrcLayout.isVisible = showCover.not()
}
private fun switchPlayMode() {
val mode = when (playerController.playMode.value) {
PlayMode.Loop -> PlayMode.Shuffle
PlayMode.Shuffle -> PlayMode.Single
PlayMode.Single -> PlayMode.Loop
}
toast(mode.nameRes)
playerController.setPlayMode(mode)
}
private fun updateOnlineActionsState(song: MediaItem) {
viewBinding.llActions.isVisible = song.isLocal().not()
viewBinding.ivLike.isSelected = likeSongProcessor.isLiked(song.getSongId())
}
override fun getNavigationBarColor(): Int {
return R.color.black
}
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(volumeReceiver)
}
private val volumeReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
viewBinding.sbVolume.progress = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
}
}
companion object {
private const val TAG = "PlayingActivity"
private const val VOLUME_CHANGED_ACTION = "android.media.VOLUME_CHANGED_ACTION"
}
}

@ -0,0 +1,145 @@
package me.wcy.music.main.playlist
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.text.buildSpannedString
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.MediaItem
import androidx.recyclerview.widget.LinearLayoutManager
import com.blankj.utilcode.util.ActivityUtils
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import me.wcy.music.R
import me.wcy.music.common.OnItemClickListener2
import me.wcy.music.databinding.FragmentCurrentPlaylistBinding
import me.wcy.music.main.playing.PlayingActivity
import me.wcy.music.service.PlayMode
import me.wcy.music.service.PlayerController
import me.wcy.radapter3.RAdapter
import top.wangchenyan.common.ext.getColorEx
import top.wangchenyan.common.ext.showConfirmDialog
import top.wangchenyan.common.ext.viewBindings
import top.wangchenyan.common.widget.CustomSpan.appendStyle
import javax.inject.Inject
/**
*
*/
@AndroidEntryPoint
class CurrentPlaylistFragment : BottomSheetDialogFragment() {
private val viewBinding by viewBindings<FragmentCurrentPlaylistBinding>()
private val adapter by lazy { RAdapter<MediaItem>() }
private val layoutManager by lazy { LinearLayoutManager(requireContext()) }
@Inject
lateinit var playerController: PlayerController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NORMAL, R.style.BottomSheetDialogTheme)
}
override fun getTheme(): Int {
return R.style.BottomSheetDialogTheme
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return viewBinding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initView()
initData()
}
private fun initView() {
viewBinding.llPlayMode.setOnClickListener {
switchPlayMode()
}
viewBinding.btnClear.setOnClickListener {
showConfirmDialog(message = "确认清空播放列表?") {
playerController.clearPlaylist()
dismissAllowingStateLoss()
ActivityUtils.finishActivity(PlayingActivity::class.java)
}
}
adapter.register(
CurrentPlaylistItemBinder(
playerController,
object : OnItemClickListener2<MediaItem> {
override fun onItemClick(item: MediaItem, position: Int) {
playerController.play(item.mediaId)
}
override fun onMoreClick(item: MediaItem, position: Int) {
playerController.delete(item)
}
})
)
viewBinding.recyclerView.layoutManager = layoutManager
viewBinding.recyclerView.adapter = adapter
}
private fun initData() {
lifecycleScope.launch {
playerController.playMode.collectLatest { playMode ->
viewBinding.ivMode.setImageLevel(playMode.value)
viewBinding.tvPlayMode.setText(playMode.nameRes)
}
}
playerController.playlist.observe(this) { playlist ->
playlist ?: return@observe
val size = playlist.size
viewBinding.tvTitle.text = buildSpannedString {
append("当前播放")
if (size > 0) {
appendStyle(
"($size)",
color = context.getColorEx(R.color.common_text_h2_color),
isBold = true
)
}
}
adapter.refresh(playlist)
}
playerController.currentSong.observe(this) { song ->
adapter.notifyDataSetChanged()
val playlist = playerController.playlist.value
if (playlist?.isNotEmpty() == true && song != null) {
val index = playlist.indexOfFirst { it.mediaId == song.mediaId }
if (index == 0) {
layoutManager.scrollToPosition(index)
} else if (index > 0) {
layoutManager.scrollToPosition(index - 1)
}
}
}
}
private fun switchPlayMode() {
val mode = when (playerController.playMode.value) {
PlayMode.Loop -> PlayMode.Shuffle
PlayMode.Shuffle -> PlayMode.Single
PlayMode.Single -> PlayMode.Loop
}
playerController.setPlayMode(mode)
}
companion object {
const val TAG = "CurrentPlaylistFragment"
fun newInstance(): CurrentPlaylistFragment {
return CurrentPlaylistFragment()
}
}
}

@ -0,0 +1,30 @@
package me.wcy.music.main.playlist
import android.annotation.SuppressLint
import androidx.media3.common.MediaItem
import me.wcy.music.common.OnItemClickListener2
import me.wcy.music.databinding.ItemCurrentPlaylistBinding
import me.wcy.music.service.PlayerController
import me.wcy.radapter3.RItemBinder
/**
*
*/
class CurrentPlaylistItemBinder(
private val playerController: PlayerController,
private val listener: OnItemClickListener2<MediaItem>
) :
RItemBinder<ItemCurrentPlaylistBinding, MediaItem>() {
@SuppressLint("SetTextI18n")
override fun onBind(viewBinding: ItemCurrentPlaylistBinding, item: MediaItem, position: Int) {
viewBinding.root.isSelected = (playerController.currentSong.value == item)
viewBinding.root.setOnClickListener {
listener.onItemClick(item, position)
}
viewBinding.tvTitle.text = item.mediaMetadata.title
viewBinding.tvArtist.text = " · ${item.mediaMetadata.artist}"
viewBinding.ivDelete.setOnClickListener {
listener.onMoreClick(item, position)
}
}
}

@ -0,0 +1,87 @@
package me.wcy.music.mine
import me.wcy.music.discover.playlist.square.bean.PlaylistListData
import me.wcy.music.mine.collect.song.bean.CollectSongResult
import me.wcy.music.net.HttpClient
import me.wcy.music.service.likesong.bean.LikeSongListData
import me.wcy.music.storage.preference.ConfigPreferences
import retrofit2.Retrofit
import retrofit2.http.POST
import retrofit2.http.Query
import top.wangchenyan.common.net.NetResult
import top.wangchenyan.common.net.gson.GsonConverterFactory
import top.wangchenyan.common.utils.GsonUtils
import top.wangchenyan.common.utils.ServerTime
/**
*
*/
interface MineApi {
@POST("user/playlist")
suspend fun getUserPlaylist(
@Query("uid") uid: Long,
@Query("limit") limit: Int = 1000,
@Query("timestamp") timestamp: Long = ServerTime.currentTimeMillis()
): PlaylistListData
/**
* 收藏/取消收藏歌单
* @param id 歌单 id
* @param t 类型,1:收藏,2:取消收藏
*/
@POST("playlist/subscribe")
suspend fun collectPlaylist(
@Query("id") id: Long,
@Query("t") t: Int,
@Query("timestamp") timestamp: Long = ServerTime.currentTimeMillis()
): NetResult<Any>
/**
* 对歌单添加歌曲
* @param op 从歌单增加单曲为 add, 删除为 del
* @param pid 歌单 id
* @param tracks 歌曲 id,可多个,用逗号隔开
*/
@POST("playlist/tracks")
suspend fun collectSong(
@Query("pid") pid: Long,
@Query("tracks") tracks: String,
@Query("op") op: String = "add",
@Query("timestamp") timestamp: Long = ServerTime.currentTimeMillis()
): CollectSongResult
/**
* 喜欢音乐
* @param id 歌曲 id
* @param like 默认为 true 即喜欢 , 若传 false, 则取消喜欢
*/
@POST("like")
suspend fun likeSong(
@Query("id") id: Long,
@Query("like") like: Boolean = true,
@Query("timestamp") timestamp: Long = ServerTime.currentTimeMillis()
): NetResult<Any>
/**
* 喜欢音乐列表
*/
@POST("likelist")
suspend fun getMyLikeSongList(
@Query("uid") uid: Long,
@Query("timestamp") timestamp: Long = ServerTime.currentTimeMillis()
): LikeSongListData
companion object {
private val api: MineApi by lazy {
val retrofit = Retrofit.Builder()
.baseUrl(ConfigPreferences.apiDomain)
.addConverterFactory(GsonConverterFactory.create(GsonUtils.gson, true))
.client(HttpClient.okHttpClient)
.build()
retrofit.create(MineApi::class.java)
}
fun get(): MineApi = api
}
}

@ -0,0 +1,115 @@
package me.wcy.music.mine.collect.song
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import com.blankj.utilcode.util.SizeUtils
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import me.wcy.music.R
import me.wcy.music.common.bean.PlaylistData
import me.wcy.music.databinding.FragmentCollectSongBinding
import me.wcy.music.mine.playlist.UserPlaylistItemBinder
import me.wcy.radapter3.RAdapter
import top.wangchenyan.common.ext.toast
import top.wangchenyan.common.ext.viewBindings
import top.wangchenyan.common.widget.decoration.SpacingDecoration
/**
*
*/
@AndroidEntryPoint
class CollectSongFragment : BottomSheetDialogFragment() {
private val viewBinding by viewBindings<FragmentCollectSongBinding>()
private val viewModel: CollectSongViewModel by viewModels()
private val adapter by lazy { RAdapter<PlaylistData>() }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NORMAL, R.style.BottomSheetDialogTheme)
}
override fun getTheme(): Int {
return R.style.BottomSheetDialogTheme
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return viewBinding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val songId = arguments?.getLong("song_id") ?: 0
if (songId <= 0) {
toast("参数错误")
dismissAllowingStateLoss()
return
}
viewModel.songId = songId
initView()
initData()
lifecycleScope.launch {
viewModel.getMyPlayList()
}
}
private fun initView() {
adapter.register(
UserPlaylistItemBinder(
true,
object : UserPlaylistItemBinder.OnItemClickListener {
override fun onItemClick(item: PlaylistData) {
collectSong(item.id)
}
override fun onMoreClick(item: PlaylistData) {
}
})
)
val spacingDecoration = SpacingDecoration(SizeUtils.dp2px(10f))
viewBinding.recyclerView.addItemDecoration(spacingDecoration)
viewBinding.recyclerView.adapter = adapter
}
private fun initData() {
lifecycleScope.launch {
viewModel.myPlaylists.collectLatest {
adapter.refresh(it)
}
}
}
private fun collectSong(pid: Long) {
lifecycleScope.launch {
val res = viewModel.collectSong(pid)
if (res.isSuccess()) {
toast("操作成功")
dismissAllowingStateLoss()
} else {
toast(res.msg)
}
}
}
companion object {
const val TAG = "CollectSongFragment"
fun newInstance(songId: Long): CollectSongFragment {
return CollectSongFragment().apply {
arguments = bundleOf("song_id" to songId)
}
}
}
}

@ -0,0 +1,55 @@
package me.wcy.music.mine.collect.song
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import me.wcy.music.account.service.UserService
import me.wcy.music.common.bean.PlaylistData
import me.wcy.music.mine.MineApi
import top.wangchenyan.common.model.CommonResult
import javax.inject.Inject
/**
*
*/
@HiltViewModel
class CollectSongViewModel @Inject constructor() : ViewModel() {
private val _myPlaylists = MutableStateFlow<List<PlaylistData>>(emptyList())
val myPlaylists = _myPlaylists
var songId: Long = 0
@Inject
lateinit var userService: UserService
suspend fun getMyPlayList(): CommonResult<List<PlaylistData>> {
val uid = userService.profile.value?.userId ?: 0
val res = kotlin.runCatching {
MineApi.get().getUserPlaylist(uid)
}
val playlistData = res.getOrNull()
return if (playlistData?.code == 200) {
val list = playlistData.playlists.filter { it.userId == uid }
_myPlaylists.value = list
CommonResult.success(list)
} else {
CommonResult.fail(playlistData?.code ?: -1)
}
}
suspend fun collectSong(pid: Long): CommonResult<Unit> {
val res = kotlin.runCatching {
MineApi.get().collectSong(pid, songId.toString())
}
return if (res.isSuccess) {
val body = res.getOrThrow().body
if (body.code == 200) {
CommonResult.success(Unit)
} else {
CommonResult.fail(body.code, body.message)
}
} else {
CommonResult.fail(msg = res.exceptionOrNull()?.message)
}
}
}

@ -0,0 +1,20 @@
package me.wcy.music.mine.collect.song.bean
import com.google.gson.annotations.SerializedName
/**
*
*/
data class CollectSongResult(
@SerializedName("status")
val status: Int = 0,
@SerializedName("body")
val body: Body = Body(),
) {
data class Body(
@SerializedName("code")
val code: Int = 0,
@SerializedName("message")
val message: String = "",
)
}

@ -0,0 +1,186 @@
package me.wcy.music.mine.home
import android.annotation.SuppressLint
import android.view.View
import androidx.core.view.isVisible
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import com.blankj.utilcode.util.SizeUtils
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import me.wcy.music.R
import me.wcy.music.account.service.UserService
import me.wcy.music.common.ApiDomainDialog
import me.wcy.music.common.BaseMusicFragment
import me.wcy.music.common.bean.PlaylistData
import me.wcy.music.consts.RoutePath
import me.wcy.music.databinding.FragmentMineBinding
import me.wcy.music.main.MainActivity
import me.wcy.music.mine.home.viewmodel.MineViewModel
import me.wcy.music.mine.playlist.UserPlaylistItemBinder
import me.wcy.radapter3.RAdapter
import me.wcy.router.CRouter
import top.wangchenyan.common.ext.loadAvatar
import top.wangchenyan.common.ext.toast
import top.wangchenyan.common.ext.viewBindings
import top.wangchenyan.common.widget.decoration.SpacingDecoration
import top.wangchenyan.common.widget.dialog.BottomItemsDialogBuilder
import javax.inject.Inject
/**
*
*/
@AndroidEntryPoint
class MineFragment : BaseMusicFragment() {
private val viewBinding by viewBindings<FragmentMineBinding>()
private val viewModel by viewModels<MineViewModel>()
@Inject
lateinit var userService: UserService
override fun getRootView(): View {
return viewBinding.root
}
override fun onLazyCreate() {
super.onLazyCreate()
initTitle()
initProfile()
initLocalMusic()
initPlaylist()
viewModel.updatePlaylistFromCache()
}
override fun onResume() {
super.onResume()
viewModel.updatePlaylist()
}
private fun initTitle() {
getTitleLayout()?.run {
addImageMenu(
R.drawable.ic_menu,
isDayNight = true,
isLeft = true
).setOnClickListener {
val activity = requireActivity()
if (activity is MainActivity) {
activity.openDrawer()
}
}
addImageMenu(
R.drawable.ic_menu_search,
isDayNight = true,
isLeft = false
).setOnClickListener {
if (ApiDomainDialog.checkApiDomain(requireContext())) {
CRouter.with(requireActivity()).url(RoutePath.SEARCH).start()
}
}
}
}
private fun initProfile() {
lifecycleScope.launch {
userService.profile.collectLatest { profile ->
viewBinding.ivAvatar.loadAvatar(profile?.avatarUrl)
viewBinding.tvNickName.text = profile?.nickname
viewBinding.flProfile.setOnClickListener {
if (ApiDomainDialog.checkApiDomain(requireActivity())) {
if (userService.isLogin().not()) {
CRouter.with(requireActivity())
.url(RoutePath.LOGIN)
.start()
}
}
}
}
}
}
private fun initLocalMusic() {
viewBinding.localMusic.setOnClickListener {
CRouter.with().url(RoutePath.LOCAL_SONG).start()
}
}
@SuppressLint("SetTextI18n")
private fun initPlaylist() {
val likePlaylistAdapter = RAdapter<PlaylistData>().apply {
register(UserPlaylistItemBinder(true, ItemClickListener(true, isLike = true)))
}
val myPlaylistAdapter = RAdapter<PlaylistData>().apply {
register(UserPlaylistItemBinder(true, ItemClickListener(true, isLike = false)))
}
val collectPlaylistAdapter = RAdapter<PlaylistData>().apply {
register(UserPlaylistItemBinder(false, ItemClickListener(false, isLike = false)))
}
val spacingDecoration = SpacingDecoration(SizeUtils.dp2px(10f))
viewBinding.rvLikePlaylist.adapter = likePlaylistAdapter
viewBinding.rvMyPlaylist.addItemDecoration(spacingDecoration)
viewBinding.rvMyPlaylist.adapter = myPlaylistAdapter
viewBinding.rvCollectPlaylist.addItemDecoration(spacingDecoration)
viewBinding.rvCollectPlaylist.adapter = collectPlaylistAdapter
val updateVisible = {
viewBinding.llLikePlaylist.isVisible = viewModel.likePlaylist.value != null
viewBinding.llMyPlaylist.isVisible = viewModel.myPlaylists.value.isNotEmpty()
viewBinding.llCollectPlaylist.isVisible = viewModel.collectPlaylists.value.isNotEmpty()
}
lifecycleScope.launch {
viewModel.likePlaylist.collectLatest { likePlaylist ->
updateVisible()
if (likePlaylist != null) {
likePlaylistAdapter.refresh(listOf(likePlaylist))
}
}
}
lifecycleScope.launch {
viewModel.myPlaylists.collectLatest { myPlaylists ->
updateVisible()
viewBinding.tvMyPlaylist.text = "创建歌单(${myPlaylists.size})"
myPlaylistAdapter.refresh(myPlaylists)
}
}
lifecycleScope.launch {
viewModel.collectPlaylists.collectLatest { collectPlaylists ->
updateVisible()
viewBinding.tvCollectPlaylist.text = "收藏歌单(${collectPlaylists.size})"
collectPlaylistAdapter.refresh(collectPlaylists)
}
}
}
inner class ItemClickListener(private val isMine: Boolean, private val isLike: Boolean) :
UserPlaylistItemBinder.OnItemClickListener {
override fun onItemClick(item: PlaylistData) {
CRouter.with(requireActivity())
.url(RoutePath.PLAYLIST_DETAIL)
.extra("id", item.id)
.extra("realtime_data", isMine)
.extra("is_like", isLike)
.start()
}
override fun onMoreClick(item: PlaylistData) {
BottomItemsDialogBuilder(requireActivity())
.items(listOf("删除"))
.onClickListener { dialog, which ->
lifecycleScope.launch {
showLoading()
val res = viewModel.removeCollect(item.id)
dismissLoading()
if (res.isSuccess().not()) {
toast(res.msg)
}
}
}
.build()
.show()
}
}
}

@ -0,0 +1,105 @@
package me.wcy.music.mine.home.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import me.wcy.music.account.service.UserService
import me.wcy.music.common.bean.PlaylistData
import me.wcy.music.mine.MineApi
import me.wcy.music.net.NetCache
import top.wangchenyan.common.ext.toUnMutable
import top.wangchenyan.common.model.CommonResult
import top.wangchenyan.common.net.apiCall
import javax.inject.Inject
/**
*
*/
@HiltViewModel
class MineViewModel @Inject constructor() : ViewModel() {
private val _likePlaylist = MutableStateFlow<PlaylistData?>(null)
val likePlaylist = _likePlaylist.toUnMutable()
private val _myPlaylists = MutableStateFlow<List<PlaylistData>>(emptyList())
val myPlaylists = _myPlaylists
private val _collectPlaylists = MutableStateFlow<List<PlaylistData>>(emptyList())
val collectPlaylists = _collectPlaylists
@Inject
lateinit var userService: UserService
private var updateJob: Job? = null
init {
viewModelScope.launch {
userService.profile.collectLatest { profile ->
if (profile != null) {
updatePlaylist(profile.userId)
} else {
_likePlaylist.value = null
_myPlaylists.value = emptyList()
_collectPlaylists.value = emptyList()
}
}
}
}
fun updatePlaylistFromCache() {
viewModelScope.launch {
if (userService.isLogin()) {
val uid = userService.profile.value?.userId ?: return@launch
val cacheList = NetCache.userCache.getJsonArray(CACHE_KEY, PlaylistData::class.java)
?: return@launch
notifyPlaylist(uid, cacheList)
}
}
}
fun updatePlaylist() {
if (userService.isLogin()) {
val uid = userService.profile.value?.userId ?: return
updatePlaylist(uid)
}
}
private fun updatePlaylist(uid: Long) {
updateJob?.cancel()
updateJob = viewModelScope.launch {
val res = kotlin.runCatching {
MineApi.get().getUserPlaylist(uid)
}
if (res.getOrNull()?.code == 200) {
val list = res.getOrThrow().playlists
notifyPlaylist(uid, list)
NetCache.userCache.putJson(CACHE_KEY, list)
}
}
}
private fun notifyPlaylist(uid: Long, list: List<PlaylistData>) {
val mineList = list.filter { it.userId == uid }
_likePlaylist.value = mineList.firstOrNull()
_myPlaylists.value = mineList.takeLast((mineList.size - 1).coerceAtLeast(0))
_collectPlaylists.value = list.filter { it.userId != uid }
}
suspend fun removeCollect(id: Long): CommonResult<Unit> {
val res = apiCall { MineApi.get().collectPlaylist(id, 2) }
return if (res.isSuccess()) {
val list = _collectPlaylists.value
_collectPlaylists.value = list.toMutableList().apply {
removeAll { it.id == id }
}
CommonResult.success(Unit)
} else {
CommonResult.fail(res.code, res.msg)
}
}
companion object {
private const val CACHE_KEY = "my_playlist"
}
}

@ -0,0 +1,123 @@
package me.wcy.music.mine.local
import android.view.View
import androidx.lifecycle.lifecycleScope
import com.blankj.utilcode.util.ConvertUtils
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.wcy.music.R
import me.wcy.music.common.BaseMusicFragment
import me.wcy.music.common.OnItemClickListener2
import me.wcy.music.common.dialog.songmenu.SimpleMenuItem
import me.wcy.music.common.dialog.songmenu.SongMoreMenuDialog
import me.wcy.music.consts.RoutePath
import me.wcy.music.databinding.FragmentLocalMusicBinding
import me.wcy.music.service.PlayerController
import me.wcy.music.storage.db.entity.SongEntity
import me.wcy.music.utils.TimeUtils
import me.wcy.music.utils.toMediaItem
import me.wcy.radapter3.RAdapter
import me.wcy.router.CRouter
import me.wcy.router.annotation.Route
import top.wangchenyan.common.ext.viewBindings
import top.wangchenyan.common.permission.Permissioner
import javax.inject.Inject
/**
*
*/
@Route(RoutePath.LOCAL_SONG)
@AndroidEntryPoint
class LocalMusicFragment : BaseMusicFragment() {
private val viewBinding by viewBindings<FragmentLocalMusicBinding>()
private val localMusicLoader by lazy {
LocalMusicLoader()
}
private val adapter by lazy {
RAdapter<SongEntity>()
}
@Inject
lateinit var playerController: PlayerController
override fun getRootView(): View {
return viewBinding.root
}
override fun isUseLoadSir(): Boolean {
return true
}
override fun getLoadSirTarget(): View {
return viewBinding.content
}
override fun onReload() {
super.onReload()
loadData()
}
override fun onLazyCreate() {
super.onLazyCreate()
adapter.register(LocalSongItemBinder(object : OnItemClickListener2<SongEntity> {
override fun onItemClick(item: SongEntity, position: Int) {
val mediaList = adapter.getDataList().map { it.toMediaItem() }
playerController.replaceAll(mediaList, mediaList[position])
CRouter.with(requireContext()).url(RoutePath.PLAYING).start()
}
override fun onMoreClick(item: SongEntity, position: Int) {
SongMoreMenuDialog(requireActivity(), item)
.setItems(
listOf(
SimpleMenuItem("文件名称: ${item.fileName}"),
SimpleMenuItem("播放时长: ${TimeUtils.formatMs(item.duration)}"),
SimpleMenuItem(
"文件大小: ${ConvertUtils.byte2FitMemorySize(item.fileSize)}"
),
SimpleMenuItem("文件路径: ${item.path}")
)
)
.show()
}
}))
viewBinding.recyclerView.adapter = adapter
viewBinding.tvPlayAll.setOnClickListener {
val mediaList = adapter.getDataList().map { it.toMediaItem() }
playerController.replaceAll(mediaList, mediaList.first())
CRouter.with(requireContext()).url(RoutePath.PLAYING).start()
}
loadData()
}
private fun loadData() {
showLoadSirLoading()
Permissioner.requestStoragePermission(requireContext()) { granted, shouldRationale ->
if (granted) {
lifecycleScope.launch {
val songList = withContext(Dispatchers.Default) {
localMusicLoader.load(requireContext())
}
if (songList.isNotEmpty()) {
showLoadSirSuccess()
viewBinding.tvPlayAll.text = "播放全部(${songList.size})"
adapter.refresh(songList)
} else {
showLoadSirEmpty(getString(R.string.no_local_music))
}
}
} else {
showLoadSirError(getString(R.string.no_permission_storage))
}
}
}
override fun getNavigationBarColor(): Int {
return R.color.play_bar_bg
}
}

@ -0,0 +1,103 @@
package me.wcy.music.mine.local
import android.content.ContentUris
import android.content.Context
import android.net.Uri
import android.provider.MediaStore
import me.wcy.music.storage.db.entity.SongEntity
import me.wcy.music.storage.preference.ConfigPreferences
/**
*
*/
class LocalMusicLoader {
private val projection = arrayOf(
MediaStore.Audio.Media._ID,
MediaStore.Audio.Media.IS_MUSIC,
MediaStore.Audio.Media.TITLE,
MediaStore.Audio.Media.ARTIST,
MediaStore.Audio.Media.ALBUM,
MediaStore.Audio.Media.ALBUM_ID,
MediaStore.Audio.Media.DATA,
MediaStore.Audio.Media.DISPLAY_NAME,
MediaStore.Audio.Media.SIZE,
MediaStore.Audio.Media.DURATION,
)
private val sortOrder = "${MediaStore.Audio.Media.DATE_MODIFIED} DESC"
fun load(context: Context): List<SongEntity> {
val result = mutableListOf<SongEntity>()
val query = context.contentResolver.query(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
projection,
null,
null,
sortOrder
)
query?.use { cursor ->
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID)
val titleColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE)
val artistColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST)
val albumColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM)
val albumIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID)
val dataColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA)
val displayNameColumn =
cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME)
val isMusicColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.IS_MUSIC)
val durationColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION)
val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE)
val filterTime = ConfigPreferences.filterTime.toLong() * 1000
val filterSize = ConfigPreferences.filterSize.toLong() * 1024
while (cursor.moveToNext()) {
val isMusic = cursor.getInt(isMusicColumn)
if (isMusic == 0) {
continue
}
val duration = cursor.getLong(durationColumn)
if (duration < filterTime) {
continue
}
val fileSize = cursor.getLong(sizeColumn)
if (fileSize < filterSize) {
continue
}
val id = cursor.getLong(idColumn)
val title = cursor.getString(titleColumn)
val artist = cursor.getString(artistColumn)
val album = cursor.getString(albumColumn)
val albumId = cursor.getLong(albumIdColumn)
val albumCover = ContentUris.withAppendedId(
MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
albumId
)
val uri =
Uri.withAppendedPath(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id.toString())
val path = cursor.getString(dataColumn)
val fileName = cursor.getString(displayNameColumn)
val entity = SongEntity(
type = SongEntity.LOCAL,
songId = id,
title = title,
artist = artist,
album = album,
albumId = albumId,
albumCover = albumCover.toString(),
duration = duration,
path = path,
fileName = fileName,
fileSize = fileSize,
)
result.add(entity)
}
}
return result
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save