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>
|
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…
Reference in new issue