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