function adapt
@@ -1,4 +1,5 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.androidApplication)
|
||||
@@ -9,31 +10,108 @@ plugins {
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_11)
|
||||
freeCompilerArgs.add("-Xexpect-actual-classes")
|
||||
}
|
||||
}
|
||||
|
||||
val appVersionCode = 20001
|
||||
val appVersionName = "2.0.0.01"
|
||||
|
||||
val taskNames = gradle.startParameter.taskNames
|
||||
val isApkRelease = taskNames.any { it.contains("assembleRelease", ignoreCase = true) }
|
||||
val isAabRelease = taskNames.any { it.contains("bundleRelease", ignoreCase = true) }
|
||||
|
||||
val keystoreProperties = Properties()
|
||||
if (isApkRelease || isAabRelease) {
|
||||
val keystorePropertiesFile = file("key.properties")
|
||||
if (!keystorePropertiesFile.exists()) {
|
||||
error("release 模式需要在 androidApp 目录下创建 key.properties(可参考 key.properties.example)")
|
||||
}
|
||||
keystorePropertiesFile.inputStream().use { keystoreProperties.load(it) }
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "top.zhushenwudi.llmp"
|
||||
namespace = "com.zhushenwudi.lovelivemusicplayer"
|
||||
compileSdk = libs.versions.android.compileSdk.get().toInt()
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "top.zhushenwudi.llmp"
|
||||
applicationId = "com.zhushenwudi.lovelivemusicplayer"
|
||||
minSdk = libs.versions.android.minSdk.get().toInt()
|
||||
targetSdk = libs.versions.android.targetSdk.get().toInt()
|
||||
versionCode = 1
|
||||
versionName = "0.2.0-phaseb"
|
||||
versionCode = appVersionCode
|
||||
versionName = appVersionName
|
||||
}
|
||||
|
||||
splits {
|
||||
abi {
|
||||
isEnable = isApkRelease
|
||||
reset()
|
||||
include("armeabi-v7a", "arm64-v8a")
|
||||
isUniversalApk = false
|
||||
}
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
if (isApkRelease || isAabRelease) {
|
||||
create("release") {
|
||||
keyAlias = keystoreProperties.getProperty("keyAlias")
|
||||
keyPassword = keystoreProperties.getProperty("storePassword")
|
||||
storePassword = keystoreProperties.getProperty("keyPassword")
|
||||
val storePath = if (isAabRelease) {
|
||||
keystoreProperties.getProperty("aabStoreFile")
|
||||
} else {
|
||||
keystoreProperties.getProperty("apkStoreFile")
|
||||
}
|
||||
storeFile = file(storePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
getByName("debug") {
|
||||
isMinifyEnabled = false
|
||||
isDebuggable = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro",
|
||||
)
|
||||
}
|
||||
getByName("release") {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
if (isApkRelease || isAabRelease) {
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
}
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro",
|
||||
)
|
||||
// ABI 分包开启时不能再写 ndk.abiFilters,否则 AGP 会报冲突。
|
||||
if (!isApkRelease) {
|
||||
ndk {
|
||||
//noinspection ChromeOsAbiSupport
|
||||
abiFilters += listOf("armeabi-v7a", "arm64-v8a")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
}
|
||||
dex {
|
||||
useLegacyPackaging = true
|
||||
}
|
||||
jniLibs {
|
||||
useLegacyPackaging = true
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
getByName("release") {
|
||||
isMinifyEnabled = false
|
||||
}
|
||||
lint {
|
||||
abortOnError = false
|
||||
checkReleaseBuilds = false
|
||||
disable += "InvalidPackage"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
@@ -42,6 +120,27 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
base.archivesName.set("LLMP_v$appVersionName")
|
||||
|
||||
afterEvaluate {
|
||||
tasks.findByName("assembleRelease")?.doLast {
|
||||
val apkRoot = layout.buildDirectory.dir("outputs/apk/release").get().asFile
|
||||
if (!apkRoot.exists()) return@doLast
|
||||
apkRoot.walkTopDown().maxDepth(2).filter { it.isFile && it.extension == "apk" }.forEach { apk ->
|
||||
val shortAbi = when {
|
||||
"arm64-v8a" in apk.name -> "v8a"
|
||||
"armeabi-v7a" in apk.name -> "v7a"
|
||||
else -> null
|
||||
} ?: return@forEach
|
||||
val target = apk.resolveSibling("LLMP_v${appVersionName}_${shortAbi}.apk")
|
||||
if (apk.canonicalFile != target.canonicalFile) {
|
||||
apk.copyTo(target, overwrite = true)
|
||||
apk.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.composeApp)
|
||||
implementation(projects.shared)
|
||||
@@ -57,10 +156,12 @@ dependencies {
|
||||
implementation(libs.zxing.android.embedded)
|
||||
implementation(libs.androidx.lifecycle.service)
|
||||
implementation(libs.ktor.client.okhttp)
|
||||
implementation(files("libs/open_sdk_3.5.19_r9483ffc7_lite.jar"))
|
||||
|
||||
implementation(libs.compose.runtime)
|
||||
implementation(libs.compose.foundation)
|
||||
implementation(libs.compose.material3)
|
||||
implementation(libs.compose.material.icons.extended)
|
||||
implementation(libs.compose.ui)
|
||||
implementation(libs.compose.ui.tooling.preview)
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# 复制为 androidApp/key.properties 后填写。assembleRelease / bundleRelease 会读这个文件。
|
||||
# 字段名和赋值方式和原项目 android/app/build.gradle 保持一致(含 key/store 密码对调)。
|
||||
keyAlias=
|
||||
storePassword=
|
||||
keyPassword=
|
||||
apkStoreFile=signature/apk.jks
|
||||
aabStoreFile=signature/aab.jks
|
||||
@@ -0,0 +1,39 @@
|
||||
-keepattributes *Annotation*,InnerClasses,Signature,Exceptions,SourceFile,LineNumberTable
|
||||
|
||||
-keep class com.zhushenwudi.lovelivemusicplayer.** { *; }
|
||||
-keep public class * extends android.app.Activity
|
||||
-keep public class * extends android.app.Service
|
||||
|
||||
-keep class com.tencent.** { *; }
|
||||
-dontwarn com.tencent.**
|
||||
|
||||
-keep class com.umeng.** { *; }
|
||||
-keep class org.repackage.** { *; }
|
||||
-keepclassmembers class * {
|
||||
public <init> (org.json.JSONObject);
|
||||
}
|
||||
-keepclassmembers enum * {
|
||||
public static **[] values();
|
||||
public static ** valueOf(java.lang.String);
|
||||
}
|
||||
|
||||
-keep class androidx.media3.** { *; }
|
||||
-keep class androidx.startup.** { *; }
|
||||
-keep class androidx.work.** { *; }
|
||||
-keep class androidx.room.** { *; }
|
||||
-keep class * extends androidx.room.RoomDatabase
|
||||
-keep class * extends androidx.room.RoomDatabase_Impl
|
||||
-keep class * implements androidx.room.RoomDatabase
|
||||
-keep class androidx.work.impl.WorkDatabase { *; }
|
||||
-keep class androidx.work.impl.WorkDatabase_Impl { *; }
|
||||
-keep class * extends androidx.work.impl.WorkDatabase
|
||||
-keepnames class * extends androidx.room.RoomDatabase
|
||||
-keepclassmembers class * extends androidx.room.RoomDatabase {
|
||||
public static ** getInstance(...);
|
||||
}
|
||||
-keep class kotlinx.** { *; }
|
||||
-keep class io.ktor.** { *; }
|
||||
-dontwarn org.slf4j.**
|
||||
-dontwarn okhttp3.internal.Version
|
||||
-dontwarn java.lang.management.**
|
||||
-dontwarn io.ktor.util.debug.**
|
||||
@@ -16,9 +16,16 @@
|
||||
|
||||
<uses-feature android:name="android.hardware.usb.host" android:required="false" />
|
||||
|
||||
<queries>
|
||||
<package android:name="com.tencent.mobileqq" />
|
||||
<package android:name="com.tencent.tim" />
|
||||
<package android:name="com.tencent.minihd.qq" />
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:label="LLMP KMP"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@android:style/Theme.Material.Light.NoActionBar"
|
||||
@@ -29,7 +36,8 @@
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:enableOnBackInvokedCallback="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
@@ -59,9 +67,39 @@
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:theme="@android:style/Theme.Material.Light.NoActionBar" />
|
||||
|
||||
<activity
|
||||
android:name="com.tencent.tauth.AuthActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:noHistory="true"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="tencent102039234" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="com.tencent.connect.common.AssistActivity"
|
||||
android:configChanges="orientation|keyboardHidden"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||
android:exported="true" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
<service
|
||||
android:name=".player.PlaybackService"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_MEDIA_BROWSER_SERVICE"
|
||||
android:foregroundServiceType="mediaPlayback">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.media3.session.MediaSessionService" />
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
package com.zhushenwudi.lovelivemusicplayer
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.edit
|
||||
import com.zhushenwudi.lovelivemusicplayer.db.DriverFactory
|
||||
import com.zhushenwudi.lovelivemusicplayer.db.createDatabase
|
||||
import com.zhushenwudi.lovelivemusicplayer.native.platform.AndroidShareBridge
|
||||
import com.zhushenwudi.lovelivemusicplayer.native.platform.AndroidDesktopLyricController
|
||||
import com.zhushenwudi.lovelivemusicplayer.native.platform.installAndroidPlatformServices
|
||||
import com.zhushenwudi.lovelivemusicplayer.native.util.AppUtils
|
||||
import com.zhushenwudi.lovelivemusicplayer.platform.AndroidUiHost
|
||||
import com.zhushenwudi.lovelivemusicplayer.platform.PlatformServices
|
||||
import com.zhushenwudi.lovelivemusicplayer.platform.initAndroidFileSystem
|
||||
import com.zhushenwudi.lovelivemusicplayer.player.PlaybackService
|
||||
import com.zhushenwudi.lovelivemusicplayer.player.PlayerController
|
||||
import com.zhushenwudi.lovelivemusicplayer.settings.createAppSettings
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private var player: PlayerController? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
AndroidUiHost.activity = this
|
||||
initAndroidFileSystem(applicationContext)
|
||||
|
||||
if (!AppContainer.isInitialized) {
|
||||
val settings = createAppSettings(applicationContext)
|
||||
val database = createDatabase(DriverFactory(applicationContext))
|
||||
player = PlayerController(applicationContext)
|
||||
AppContainer.init(
|
||||
settings = settings,
|
||||
database = database,
|
||||
player = player!!,
|
||||
)
|
||||
AppContainer.musicTransfer.setPlatform("android")
|
||||
} else {
|
||||
player = AppContainer.player
|
||||
}
|
||||
|
||||
installAndroidPlatformServices(applicationContext)
|
||||
val lyricEnabled = AppContainer.settings.enableDesktopLyric
|
||||
if (!PlatformServices.desktopLyric.pipAutoOpen(lyricEnabled) && lyricEnabled) {
|
||||
AppContainer.settings.enableDesktopLyric = false
|
||||
}
|
||||
PlatformServices.analytics.initIfAgreed(
|
||||
getSharedPreferences("my_prefs", MODE_PRIVATE).getBoolean("key_agreed", false),
|
||||
)
|
||||
|
||||
getSharedPreferences(AppUtils.SP_NAME, MODE_PRIVATE).edit(commit = true) {
|
||||
putBoolean("isShutdown", false)
|
||||
}
|
||||
|
||||
handleIncomingIntent(intent)
|
||||
|
||||
requestNotifyPermission()
|
||||
startPlaybackService()
|
||||
|
||||
setContent { App() }
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
AndroidShareBridge.handleIntentResult(intent)
|
||||
handleIncomingIntent(intent)
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
AndroidShareBridge.handleActivityResult(requestCode, resultCode, data)
|
||||
if (requestCode == AndroidDesktopLyricController.REQUEST_CODE_OVERLAY_PERMISSION) {
|
||||
AndroidDesktopLyricController.onOverlayPermissionResult(Settings.canDrawOverlays(this))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
AndroidUiHost.activity = this
|
||||
AppUtils.getSchemeData(intent) { url ->
|
||||
PlatformServices.deepLink.handle(url)
|
||||
}
|
||||
AppContainer.setAppInBackground(false)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
AppContainer.setAppInBackground(true)
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (AndroidUiHost.activity === this) {
|
||||
AndroidUiHost.activity = null
|
||||
}
|
||||
if (isFinishing) {
|
||||
val stillPlaying = player?.state?.value?.isPlaying == true
|
||||
if (!stillPlaying) {
|
||||
getSharedPreferences(AppUtils.SP_NAME, MODE_PRIVATE).edit(commit = true) {
|
||||
putString("curJpLrc", "")
|
||||
putString("nextJpLrc", "")
|
||||
putBoolean("isPlaying", false)
|
||||
putBoolean("isShutdown", true)
|
||||
}
|
||||
PlatformServices.homeWidget.setShutdown(true)
|
||||
}
|
||||
PlatformServices.desktopLyric.hide()
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun handleIncomingIntent(intent: Intent?) {
|
||||
val data = intent?.data?.toString() ?: return
|
||||
when {
|
||||
data.startsWith(AppUtils.HOME_WIDGET_URI_PREFIX) -> {
|
||||
handleWidgetUri(data)
|
||||
intent.data = null
|
||||
}
|
||||
|
||||
data.startsWith("llmp://") || data.startsWith("llmp:") -> {
|
||||
PlatformServices.deepLink.handle(data)
|
||||
intent.data = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleWidgetUri(uri: String) {
|
||||
when {
|
||||
uri.endsWith("toggle_play") -> player?.toggle()
|
||||
uri.endsWith("next") -> player?.next()
|
||||
uri.endsWith("previous") -> player?.previous()
|
||||
uri.endsWith("toggle_love") -> {
|
||||
val id = player?.state?.value?.current?.musicId ?: return
|
||||
AppContainer.toggleLove(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestNotifyPermission() {
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
|
||||
!= PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
ActivityCompat.requestPermissions(
|
||||
this,
|
||||
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||
100,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startPlaybackService() {
|
||||
PlaybackService.start(this)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package top.zhushenwudi.llmp.native
|
||||
package com.zhushenwudi.lovelivemusicplayer.native
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
@@ -18,21 +18,23 @@ import android.widget.LinearLayout
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import java.lang.ref.WeakReference
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import top.zhushenwudi.llmp.MainActivity
|
||||
import top.zhushenwudi.llmp.R
|
||||
import com.zhushenwudi.lovelivemusicplayer.AppContainer
|
||||
import com.zhushenwudi.lovelivemusicplayer.i18n.I18n
|
||||
import com.zhushenwudi.lovelivemusicplayer.MainActivity
|
||||
import com.zhushenwudi.lovelivemusicplayer.R
|
||||
import java.lang.ref.WeakReference
|
||||
import kotlin.math.abs
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Floating desktop-lyric overlay.
|
||||
* Hosted by [top.zhushenwudi.llmp.player.PlaybackService] so it shares the Media3 FGS notification.
|
||||
* 桌面悬浮歌词叠加层。
|
||||
* 由 [com.zhushenwudi.lovelivemusicplayer.player.PlaybackService] 承载,共享 Media3 FGS 通知。
|
||||
*/
|
||||
class DesktopLyricOverlay(
|
||||
private val context: Context,
|
||||
@@ -40,18 +42,20 @@ class DesktopLyricOverlay(
|
||||
private val onRequestHide: () -> Unit,
|
||||
) {
|
||||
companion object {
|
||||
private const val MSG_HINT = "正在打开应用..."
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
private var activeRef: WeakReference<DesktopLyricOverlay>? = null
|
||||
|
||||
@Volatile private var cachedLine1: String? = null
|
||||
@Volatile private var cachedLine2: String? = null
|
||||
@Volatile private var cachedCurrentLine: Int = -1
|
||||
@Volatile
|
||||
private var cachedLine1: String? = null
|
||||
@Volatile
|
||||
private var cachedLine2: String? = null
|
||||
@Volatile
|
||||
private var cachedCurrentLine: Int = -1
|
||||
|
||||
fun updateLyric(lyricLine1: String?, lyricLine2: String?, currentLine: Int) {
|
||||
cachedLine1 = lyricLine1
|
||||
cachedLine2 = lyricLine2
|
||||
if (lyricLine1 != null) cachedLine1 = lyricLine1
|
||||
if (lyricLine2 != null) cachedLine2 = lyricLine2
|
||||
cachedCurrentLine = currentLine
|
||||
val overlay = activeRef?.get() ?: return
|
||||
if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||
@@ -87,6 +91,7 @@ class DesktopLyricOverlay(
|
||||
fun attach() {
|
||||
if (root != null) return
|
||||
createView()
|
||||
if (root == null) return
|
||||
bindView()
|
||||
activeRef = WeakReference(this)
|
||||
startChromeTimer()
|
||||
@@ -134,7 +139,12 @@ class DesktopLyricOverlay(
|
||||
}
|
||||
windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||
root = LayoutInflater.from(context).inflate(R.layout.view_lyric, null) as RelativeLayout
|
||||
windowManager?.addView(root, wmParams)
|
||||
val added = runCatching { windowManager?.addView(root, wmParams) }
|
||||
if (added.isFailure) {
|
||||
android.util.Log.e("LLMP-Lyric", "add overlay failed", added.exceptionOrNull())
|
||||
root = null
|
||||
windowManager = null
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@@ -178,6 +188,7 @@ class DesktopLyricOverlay(
|
||||
sx = mRawX
|
||||
sy = mRawY
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
isMove = true
|
||||
val params = wmParams ?: return@setOnTouchListener true
|
||||
@@ -187,6 +198,7 @@ class DesktopLyricOverlay(
|
||||
sx = mRawX
|
||||
sy = mRawY
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP -> {
|
||||
val elapsed = System.currentTimeMillis() - mLastTime
|
||||
val mStopX = event.rawX.toInt()
|
||||
@@ -206,11 +218,11 @@ class DesktopLyricOverlay(
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
context.startActivity(intent)
|
||||
Toast.makeText(context, MSG_HINT, Toast.LENGTH_LONG).show()
|
||||
Toast.makeText(context, I18n.t("opening_app"), Toast.LENGTH_LONG).show()
|
||||
onRequestHide()
|
||||
}
|
||||
ivTranslate?.setOnClickListener {
|
||||
NativeEvents.lyricTypeClick()
|
||||
AppContainer.cycleDesktopLyricType()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,13 +230,13 @@ class DesktopLyricOverlay(
|
||||
val lyricLine1 = cachedLine1
|
||||
val lyricLine2 = cachedLine2
|
||||
val currentLine = cachedCurrentLine
|
||||
tvLyricLine1?.text = lyricLine1.orEmpty()
|
||||
tvLyricLine2?.text = lyricLine2.orEmpty()
|
||||
lyricLine1?.let { tvLyricLine1?.text = it }
|
||||
lyricLine2?.let { tvLyricLine2?.text = it }
|
||||
tvLyricLine1?.setTextColor(if (currentLine == 2) Color.LTGRAY else Color.WHITE)
|
||||
tvLyricLine2?.setTextColor(if (currentLine == 1) Color.LTGRAY else Color.WHITE)
|
||||
|
||||
if (currentLine == -1) {
|
||||
tvNoLyric?.text = lyricLine1 ?: "暂无歌词"
|
||||
tvNoLyric?.text = lyricLine1 ?: I18n.t("no_lyrics")
|
||||
tvLyricLine1?.visibility = View.GONE
|
||||
tvLyricLine2?.visibility = View.GONE
|
||||
tvNoLyric?.visibility = View.VISIBLE
|
||||
@@ -1,9 +1,9 @@
|
||||
package top.zhushenwudi.llmp.native
|
||||
package com.zhushenwudi.lovelivemusicplayer.native
|
||||
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import top.zhushenwudi.llmp.platform.DeepLinkPayload
|
||||
import com.zhushenwudi.lovelivemusicplayer.platform.DeepLinkPayload
|
||||
|
||||
object NativeEvents {
|
||||
private val _widgetEvents = MutableSharedFlow<String>(extraBufferCapacity = 8)
|
||||
@@ -1,10 +1,11 @@
|
||||
package top.zhushenwudi.llmp.native
|
||||
package com.zhushenwudi.lovelivemusicplayer.native
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import com.journeyapps.barcodescanner.ScanContract
|
||||
import com.journeyapps.barcodescanner.ScanOptions
|
||||
import top.zhushenwudi.llmp.platform.AndroidUiHost
|
||||
import com.zhushenwudi.lovelivemusicplayer.i18n.I18n
|
||||
import com.zhushenwudi.lovelivemusicplayer.platform.AndroidUiHost
|
||||
|
||||
class QrScanActivity : ComponentActivity() {
|
||||
private val barcodeLauncher = registerForActivityResult(ScanContract()) { result ->
|
||||
@@ -23,7 +24,7 @@ class QrScanActivity : ComponentActivity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
val options = ScanOptions().apply {
|
||||
setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
||||
setPrompt("扫描二维码")
|
||||
setPrompt(I18n.t("scan_qr_prompt"))
|
||||
setBeepEnabled(false)
|
||||
setOrientationLocked(false)
|
||||
setCaptureActivity(com.journeyapps.barcodescanner.CaptureActivity::class.java)
|
||||
@@ -1,7 +1,7 @@
|
||||
package top.zhushenwudi.llmp.native
|
||||
package com.zhushenwudi.lovelivemusicplayer.native
|
||||
|
||||
import android.content.Context
|
||||
import top.zhushenwudi.llmp.platform.AnalyticsHook
|
||||
import com.zhushenwudi.lovelivemusicplayer.platform.AnalyticsHook
|
||||
|
||||
object UmengBridge : AnalyticsHook {
|
||||
private const val UMENG_KEY = "634bd9c688ccdf4b7e4ac67b"
|
||||
@@ -0,0 +1,203 @@
|
||||
package com.zhushenwudi.lovelivemusicplayer.native
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Bundle
|
||||
import android.webkit.WebChromeClient
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebResourceResponse
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.addCallback
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import com.zhushenwudi.lovelivemusicplayer.i18n.tr
|
||||
import com.zhushenwudi.lovelivemusicplayer.platform.WebViewAdBlock
|
||||
import java.io.ByteArrayInputStream
|
||||
|
||||
class WebViewActivity : ComponentActivity() {
|
||||
private var webView: WebView? = null
|
||||
private val canGoBack = mutableStateOf(false)
|
||||
private val canGoForward = mutableStateOf(false)
|
||||
private val pageProgress = mutableIntStateOf(0)
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val url = intent.getStringExtra(EXTRA_URL).orEmpty()
|
||||
val title = intent.getStringExtra(EXTRA_TITLE)
|
||||
title?.let { setTitle(it) }
|
||||
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
val view = webView
|
||||
if (view?.canGoBack() == true) {
|
||||
view.goBack()
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
setContent {
|
||||
val backEnabled by canGoBack
|
||||
val forwardEnabled by canGoForward
|
||||
val progress by pageProgress
|
||||
|
||||
MaterialTheme {
|
||||
Column(Modifier.fillMaxSize()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
AndroidView(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
factory = { context ->
|
||||
WebView(context).apply {
|
||||
settings.javaScriptEnabled = true
|
||||
settings.domStorageEnabled = true
|
||||
webViewClient = AdBlockWebViewClient(::refreshNavigationState)
|
||||
webChromeClient = object : WebChromeClient() {
|
||||
override fun onProgressChanged(view: WebView?, newProgress: Int) {
|
||||
pageProgress.intValue = newProgress
|
||||
}
|
||||
}
|
||||
webView = this
|
||||
if (url.isNotBlank()) loadUrl(url)
|
||||
refreshNavigationState()
|
||||
}
|
||||
},
|
||||
update = { view ->
|
||||
webView = view
|
||||
refreshNavigationState()
|
||||
},
|
||||
)
|
||||
if (progress in 1..99) {
|
||||
LinearProgressIndicator(
|
||||
progress = { progress / 100f },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.TopCenter),
|
||||
)
|
||||
}
|
||||
}
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.navigationBarsPadding(),
|
||||
tonalElevation = 3.dp,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
IconButton(
|
||||
onClick = { webView?.goBack() },
|
||||
enabled = backEnabled,
|
||||
) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = tr("backward"),
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = { webView?.goForward() },
|
||||
enabled = forwardEnabled,
|
||||
) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.ArrowForward,
|
||||
contentDescription = tr("forward"),
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { webView?.reload() }) {
|
||||
Icon(Icons.Default.Refresh, contentDescription = tr("refresh"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshNavigationState() {
|
||||
canGoBack.value = webView?.canGoBack() == true
|
||||
canGoForward.value = webView?.canGoForward() == true
|
||||
}
|
||||
|
||||
private class AdBlockWebViewClient(
|
||||
private val onNavigationChanged: () -> Unit,
|
||||
) : WebViewClient() {
|
||||
override fun shouldOverrideUrlLoading(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?,
|
||||
): Boolean = false
|
||||
|
||||
override fun shouldInterceptRequest(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?,
|
||||
): WebResourceResponse? {
|
||||
val host = request?.url?.host
|
||||
if (!host.isNullOrBlank() && WebViewAdBlock.isBlockedHost(host)) {
|
||||
return WebResourceResponse(
|
||||
"text/plain",
|
||||
"utf-8",
|
||||
ByteArrayInputStream(ByteArray(0)),
|
||||
)
|
||||
}
|
||||
return super.shouldInterceptRequest(view, request)
|
||||
}
|
||||
|
||||
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
|
||||
super.onPageStarted(view, url, favicon)
|
||||
onNavigationChanged()
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
super.onPageFinished(view, url)
|
||||
if (view != null && !url.isNullOrBlank()) {
|
||||
view.evaluateJavascript(WebViewAdBlock.injectionScript(url), null)
|
||||
}
|
||||
onNavigationChanged()
|
||||
}
|
||||
|
||||
override fun doUpdateVisitedHistory(
|
||||
view: WebView?,
|
||||
url: String?,
|
||||
isReload: Boolean,
|
||||
) {
|
||||
super.doUpdateVisitedHistory(view, url, isReload)
|
||||
onNavigationChanged()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_URL = "url"
|
||||
const val EXTRA_TITLE = "title"
|
||||
}
|
||||
}
|
||||
@@ -1,426 +1,455 @@
|
||||
package top.zhushenwudi.llmp.native.home_widget
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.glance.ColorFilter
|
||||
import androidx.glance.GlanceId
|
||||
import androidx.glance.GlanceModifier
|
||||
import androidx.glance.Image
|
||||
import androidx.glance.ImageProvider
|
||||
import androidx.glance.LocalContext
|
||||
import androidx.glance.action.Action
|
||||
import androidx.glance.action.clickable
|
||||
import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.action.actionStartActivity
|
||||
import androidx.glance.appwidget.cornerRadius
|
||||
import androidx.glance.appwidget.provideContent
|
||||
import androidx.glance.background
|
||||
import androidx.glance.layout.Alignment
|
||||
import androidx.glance.layout.Box
|
||||
import androidx.glance.layout.Column
|
||||
import androidx.glance.layout.fillMaxSize
|
||||
import androidx.glance.layout.height
|
||||
import androidx.glance.layout.padding
|
||||
import androidx.glance.layout.size
|
||||
import androidx.glance.layout.width
|
||||
import androidx.glance.text.FontWeight
|
||||
import androidx.glance.text.Text
|
||||
import androidx.glance.text.TextStyle
|
||||
import androidx.glance.unit.ColorProvider
|
||||
import top.zhushenwudi.llmp.MainActivity
|
||||
import top.zhushenwudi.llmp.R
|
||||
import top.zhushenwudi.llmp.native.util.AppUtils
|
||||
import top.zhushenwudi.llmp.native.util.ImageUtil
|
||||
import java.io.File
|
||||
|
||||
abstract class HomeWidgetContent : GlanceAppWidget() {
|
||||
open var size: DpSize = AppUtils.SMALL_SQUARE
|
||||
open var radius: Dp = 6.dp
|
||||
open val cdSize = 80.dp
|
||||
abstract var isWhite: Boolean
|
||||
var isPlaying = true
|
||||
var isShutdown = false
|
||||
private var isFavorite = false
|
||||
private lateinit var sp: SharedPreferences
|
||||
private lateinit var songName: String
|
||||
private lateinit var songArtist: String
|
||||
private lateinit var playText: String
|
||||
private var lyricLine1: String = ""
|
||||
private var lyricLine2: String = ""
|
||||
private var currentLine: Int = -1
|
||||
|
||||
private var musicColor = Color.White
|
||||
private var coverPath: String? = null
|
||||
private var line1Color = Color.White
|
||||
private var line2Color = Color.White
|
||||
|
||||
private fun widgetAction(context: Context, uri: String): Action {
|
||||
val intent = Intent(context, MainActivity::class.java).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
data = Uri.parse(uri)
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
}
|
||||
return actionStartActivity(intent)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GlanceContent(preferences: SharedPreferences) {
|
||||
sp = preferences
|
||||
songName = sp.getString("songName", "") ?: ""
|
||||
songArtist = sp.getString("songArtist", "") ?: ""
|
||||
isFavorite = sp.getBoolean("songFavorite", false)
|
||||
isPlaying = sp.getBoolean("isPlaying", false)
|
||||
playText = sp.getString("playText", "") ?: ""
|
||||
val tempLine1 = sp.getString("lyricLine1", null)
|
||||
val tempLine2 = sp.getString("lyricLine2", null)
|
||||
currentLine = sp.getInt("currentLine", -1)
|
||||
isShutdown = sp.getBoolean("isShutdown", false)
|
||||
coverPath = sp.getString("shareImage", null)
|
||||
val strBgColor = sp.getString("bgColor", "") ?: ""
|
||||
|
||||
coverPath?.let {
|
||||
if (!File(it).exists()) {
|
||||
coverPath = null
|
||||
}
|
||||
}
|
||||
|
||||
if (strBgColor.contains(",")) {
|
||||
val bgColorArr = strBgColor.split(",")
|
||||
musicColor = Color(
|
||||
red = bgColorArr.getOrNull(0)?.toFloatOrNull() ?: 1f,
|
||||
green = bgColorArr.getOrNull(1)?.toFloatOrNull() ?: 1f,
|
||||
blue = bgColorArr.getOrNull(2)?.toFloatOrNull() ?: 1f,
|
||||
)
|
||||
}
|
||||
|
||||
if (isShutdown) {
|
||||
isPlaying = false
|
||||
}
|
||||
|
||||
tempLine1?.let { lyricLine1 = it }
|
||||
tempLine2?.let { lyricLine2 = it }
|
||||
if (currentLine == -1 || currentLine == 1) {
|
||||
line1Color = if (isWhite) Color.Black else Color.White
|
||||
line2Color = Color.LightGray
|
||||
} else {
|
||||
line2Color = if (isWhite) Color.Black else Color.White
|
||||
line1Color = Color.LightGray
|
||||
}
|
||||
|
||||
RenderAppWidget()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderAppWidget() {
|
||||
Box(
|
||||
modifier = GlanceModifier
|
||||
.width(size.width)
|
||||
.height(size.height),
|
||||
) {
|
||||
if (isWhite) {
|
||||
GradientRectangle(color = musicColor)
|
||||
} else {
|
||||
Box(
|
||||
modifier = GlanceModifier
|
||||
.background(Color(red = 49, green = 49, blue = 49))
|
||||
.width(size.width)
|
||||
.height(size.height)
|
||||
.cornerRadius(radius),
|
||||
) {}
|
||||
}
|
||||
|
||||
Box(
|
||||
contentAlignment = Alignment.CenterEnd,
|
||||
modifier = GlanceModifier
|
||||
.width(size.width)
|
||||
.height(size.height)
|
||||
.cornerRadius(radius),
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = GlanceModifier
|
||||
.size(size.height)
|
||||
.cornerRadius(radius),
|
||||
) {
|
||||
RenderCD(isLargePauseWidget = false)
|
||||
RenderCover(isLargePauseWidget = false)
|
||||
RenderPlayController(false)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = GlanceModifier
|
||||
.width(size.width)
|
||||
.height(size.height)
|
||||
.padding(radius),
|
||||
) {
|
||||
Box(
|
||||
modifier = GlanceModifier.defaultWeight(),
|
||||
) {
|
||||
Column {
|
||||
RenderLogo(size = 22.dp, paddingInDp = (-6).dp)
|
||||
if (size.width == AppUtils.HORIZONTAL_RECTANGLE.width) {
|
||||
RenderLyric(isLargePauseWidget = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RenderTextArr(stateFontSize = 9.sp, infoFontSize = 11.sp)
|
||||
}
|
||||
|
||||
RenderFavorite(imageSize = 20.dp, paddingInDp = radius)
|
||||
}
|
||||
}
|
||||
|
||||
open fun calcCdOffsetX(): Dp = cdSize / 2 - 10.dp
|
||||
|
||||
open fun calcCdOffsetY(): Dp = cdSize / 2 - 10.dp
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
@Composable
|
||||
open fun RenderTextArr(
|
||||
stateFontSize: TextUnit,
|
||||
infoFontSize: TextUnit,
|
||||
) {
|
||||
val playTextArr = playText.split(",")
|
||||
var playStateText = playTextArr.getOrElse(1) { "" }
|
||||
if (!isShutdown && isPlaying) {
|
||||
playStateText = playTextArr.getOrElse(0) { playStateText }
|
||||
}
|
||||
Text(
|
||||
text = playStateText,
|
||||
style = TextStyle(
|
||||
fontSize = stateFontSize,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = ColorProvider(Color.Gray),
|
||||
),
|
||||
)
|
||||
|
||||
Text(
|
||||
text = songName,
|
||||
style = TextStyle(
|
||||
fontSize = infoFontSize,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = ColorProvider(if (isWhite) Color.Black else Color.White),
|
||||
),
|
||||
)
|
||||
|
||||
Text(
|
||||
text = songArtist,
|
||||
style = TextStyle(
|
||||
fontSize = infoFontSize,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = ColorProvider(Color.Gray),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
open fun RenderLogo(size: Dp, paddingInDp: Dp) {
|
||||
Image(
|
||||
provider = ImageProvider(R.drawable.logo),
|
||||
contentDescription = "logo",
|
||||
modifier = GlanceModifier
|
||||
.size(size)
|
||||
.padding(start = paddingInDp),
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
@Composable
|
||||
open fun RenderLyric(isLargePauseWidget: Boolean) {
|
||||
val basicWidth = size.width
|
||||
val width = if (isLargePauseWidget) basicWidth - 150.dp else basicWidth - 60.dp
|
||||
Text(
|
||||
text = lyricLine1,
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = ColorProvider(line1Color),
|
||||
),
|
||||
maxLines = 1,
|
||||
modifier = GlanceModifier.width(width),
|
||||
)
|
||||
|
||||
Text(
|
||||
text = lyricLine2,
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = ColorProvider(line2Color),
|
||||
),
|
||||
maxLines = 1,
|
||||
modifier = GlanceModifier.width(width),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
open fun RenderCD(isLargePauseWidget: Boolean) {
|
||||
val context = LocalContext.current
|
||||
val cdDrawable = if (isWhite) R.drawable.cd_white else R.drawable.cd_black
|
||||
val offsetX = calcCdOffsetX()
|
||||
val offsetY = calcCdOffsetY()
|
||||
val startPadding = if (isLargePauseWidget) 0.dp else offsetX
|
||||
val radio = if (isLargePauseWidget) 0.9f else 1f
|
||||
if (isShutdown) {
|
||||
Image(
|
||||
provider = ImageProvider(cdDrawable),
|
||||
contentDescription = "cd",
|
||||
modifier = GlanceModifier
|
||||
.size(size.height * radio)
|
||||
.padding(
|
||||
start = startPadding,
|
||||
top = -offsetY,
|
||||
end = -offsetX,
|
||||
bottom = offsetY,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
Image(
|
||||
provider = ImageProvider(cdDrawable),
|
||||
contentDescription = "cd",
|
||||
modifier = GlanceModifier
|
||||
.size(size.height * radio)
|
||||
.padding(
|
||||
start = startPadding,
|
||||
top = -offsetY,
|
||||
end = -offsetX,
|
||||
bottom = offsetY,
|
||||
)
|
||||
.clickable(
|
||||
rippleOverride = R.drawable.click_ripple,
|
||||
onClick = widgetAction(context, "homeWidgetExample://toggle_play"),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
open fun RenderCover(isLargePauseWidget: Boolean) {
|
||||
val coverSize = cdSize * 0.4f
|
||||
val coverAndCdDiffSize = (cdSize - coverSize) / 2
|
||||
val offsetX = calcCdOffsetX()
|
||||
val offsetY = calcCdOffsetY()
|
||||
val startPadding =
|
||||
if (isLargePauseWidget) coverAndCdDiffSize else offsetX + coverAndCdDiffSize
|
||||
val radio = if (isLargePauseWidget) 0.95f else 1f
|
||||
coverPath?.let {
|
||||
if (File(it).exists()) {
|
||||
Image(
|
||||
provider = ImageProvider(BitmapFactory.decodeFile(it)),
|
||||
contentDescription = "cover",
|
||||
modifier = GlanceModifier
|
||||
.size(size.height * radio)
|
||||
.padding(
|
||||
start = startPadding,
|
||||
top = -offsetY + coverAndCdDiffSize,
|
||||
bottom = offsetY + coverAndCdDiffSize,
|
||||
end = -offsetX + coverAndCdDiffSize,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
@Composable
|
||||
open fun RenderPlayController(isLargePauseWidget: Boolean) {
|
||||
val resId = if (isPlaying) R.drawable.vector_pause else R.drawable.vector_play
|
||||
|
||||
if (isLargePauseWidget) {
|
||||
Image(
|
||||
provider = ImageProvider(resId),
|
||||
contentDescription = "playButton",
|
||||
colorFilter = ColorFilter.tint(colorProvider = ColorProvider(Color.White)),
|
||||
modifier = GlanceModifier.size(20.dp),
|
||||
)
|
||||
} else {
|
||||
val padding =
|
||||
if (size.width == AppUtils.HORIZONTAL_RECTANGLE.width) 26.dp else 23.dp
|
||||
Box(
|
||||
contentAlignment = Alignment.TopEnd,
|
||||
modifier = GlanceModifier
|
||||
.fillMaxSize()
|
||||
.padding(top = padding, end = padding),
|
||||
) {
|
||||
Image(
|
||||
provider = ImageProvider(resId),
|
||||
contentDescription = "playButton",
|
||||
colorFilter = ColorFilter.tint(colorProvider = ColorProvider(Color.White)),
|
||||
modifier = GlanceModifier.size(15.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
open fun RenderFavorite(imageSize: Dp, paddingInDp: Dp) {
|
||||
val context = LocalContext.current
|
||||
Box(
|
||||
contentAlignment = Alignment.BottomEnd,
|
||||
modifier = GlanceModifier
|
||||
.width(size.width)
|
||||
.height(size.height)
|
||||
.padding(paddingInDp),
|
||||
) {
|
||||
val loveIcon =
|
||||
if (isFavorite) R.drawable.widget_fav_click else R.drawable.widget_fav_unclick
|
||||
if (isShutdown) {
|
||||
Image(
|
||||
provider = ImageProvider(loveIcon),
|
||||
contentDescription = "love",
|
||||
modifier = GlanceModifier.size(imageSize),
|
||||
)
|
||||
} else {
|
||||
Image(
|
||||
provider = ImageProvider(loveIcon),
|
||||
contentDescription = "love",
|
||||
modifier = GlanceModifier
|
||||
.size(imageSize)
|
||||
.clickable(
|
||||
rippleOverride = R.drawable.click_ripple,
|
||||
onClick = widgetAction(context, "homeWidgetExample://toggle_love"),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GradientRectangle(color: Color) {
|
||||
val bitmap = ImageUtil.createGradientImage(size.width.value, size.height.value, color)
|
||||
|
||||
Box(
|
||||
modifier = GlanceModifier
|
||||
.width(size.width)
|
||||
.height(size.height)
|
||||
.cornerRadius(radius),
|
||||
) {
|
||||
Image(
|
||||
provider = ImageProvider(bitmap),
|
||||
contentDescription = "bg",
|
||||
modifier = GlanceModifier
|
||||
.width(size.width)
|
||||
.height(size.height),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||||
val preferences = context.getSharedPreferences(AppUtils.SP_NAME, Context.MODE_PRIVATE)
|
||||
provideContent {
|
||||
GlanceContent(preferences = preferences)
|
||||
}
|
||||
}
|
||||
}
|
||||
package com.zhushenwudi.lovelivemusicplayer.native.home_widget
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.net.toUri
|
||||
import androidx.glance.LocalContext
|
||||
import androidx.glance.action.Action
|
||||
import androidx.glance.appwidget.action.actionStartActivity
|
||||
import com.zhushenwudi.lovelivemusicplayer.MainActivity
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.glance.ColorFilter
|
||||
import androidx.glance.GlanceId
|
||||
import androidx.glance.GlanceModifier
|
||||
import androidx.glance.Image
|
||||
import androidx.glance.ImageProvider
|
||||
import androidx.glance.action.clickable
|
||||
import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.cornerRadius
|
||||
import androidx.glance.appwidget.provideContent
|
||||
import androidx.glance.background
|
||||
import androidx.glance.layout.Alignment
|
||||
import androidx.glance.layout.Box
|
||||
import androidx.glance.layout.Column
|
||||
import androidx.glance.layout.Row
|
||||
import androidx.glance.layout.Spacer
|
||||
import androidx.glance.layout.fillMaxSize
|
||||
import androidx.glance.layout.height
|
||||
import androidx.glance.layout.padding
|
||||
import androidx.glance.layout.size
|
||||
import androidx.glance.layout.width
|
||||
import androidx.glance.text.FontWeight
|
||||
import androidx.glance.text.Text
|
||||
import androidx.glance.text.TextStyle
|
||||
import androidx.glance.unit.ColorProvider
|
||||
import com.zhushenwudi.lovelivemusicplayer.R
|
||||
import com.zhushenwudi.lovelivemusicplayer.native.util.AppUtils
|
||||
import com.zhushenwudi.lovelivemusicplayer.native.util.ImageUtil
|
||||
import java.io.File
|
||||
|
||||
abstract class HomeWidgetContent : GlanceAppWidget() {
|
||||
open var size: DpSize = AppUtils.SMALL_SQUARE
|
||||
open var radius: Dp = 6.dp
|
||||
open val cdSize = 80.dp
|
||||
abstract var isWhite: Boolean
|
||||
var isPlaying = true
|
||||
var isShutdown = false
|
||||
private var isFavorite = false
|
||||
private lateinit var sp: SharedPreferences
|
||||
private lateinit var songName: String
|
||||
private lateinit var songArtist: String
|
||||
private lateinit var playText: String
|
||||
|
||||
private var musicColor = Color.White
|
||||
private var coverPath: String? = null
|
||||
|
||||
@Composable
|
||||
private fun widgetAction(context: Context, uri: String): Action {
|
||||
val intent = Intent(context, MainActivity::class.java).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
data = uri.toUri()
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
}
|
||||
return actionStartActivity(intent)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GlanceContent(preferences: SharedPreferences) {
|
||||
sp = preferences
|
||||
songName = sp.getString("songName", "")!!
|
||||
songArtist = sp.getString("songArtist", "")!!
|
||||
isFavorite = sp.getBoolean("songFavorite", false)
|
||||
isPlaying = sp.getBoolean("isPlaying", false)
|
||||
playText = sp.getString("playText", "")!!
|
||||
isShutdown = sp.getBoolean("isShutdown", false)
|
||||
coverPath = sp.getString("shareImage", null)
|
||||
val strBgColor = sp.getString("bgColor", "")!!
|
||||
|
||||
coverPath?.let {
|
||||
if (!File(it).exists()) {
|
||||
coverPath = null
|
||||
}
|
||||
}
|
||||
|
||||
if (strBgColor.contains(",")) {
|
||||
val bgColorArr = strBgColor.split(",")
|
||||
musicColor = Color(
|
||||
red = bgColorArr[0].toDoubleOrNull()?.toInt() ?: 1,
|
||||
green = bgColorArr[1].toDoubleOrNull()?.toInt() ?: 1,
|
||||
blue = bgColorArr[2].toDoubleOrNull()?.toInt() ?: 1
|
||||
)
|
||||
}
|
||||
|
||||
if (isShutdown) {
|
||||
isPlaying = false
|
||||
}
|
||||
|
||||
RenderAppWidget()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderAppWidget() {
|
||||
Box(
|
||||
modifier = GlanceModifier
|
||||
.width(size.width)
|
||||
.height(size.height)
|
||||
) {
|
||||
if (isWhite) {
|
||||
GradientRectangle(color = musicColor)
|
||||
} else {
|
||||
Box(
|
||||
modifier = GlanceModifier
|
||||
.background(Color(red = 49, green = 49, blue = 49))
|
||||
.width(size.width)
|
||||
.height(size.height)
|
||||
.cornerRadius(radius)
|
||||
) {}
|
||||
}
|
||||
|
||||
Box(
|
||||
contentAlignment = Alignment.CenterEnd,
|
||||
modifier = GlanceModifier
|
||||
.width(size.width)
|
||||
.height(size.height)
|
||||
.cornerRadius(radius)
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = GlanceModifier
|
||||
.size(size.height)
|
||||
.cornerRadius(radius)
|
||||
) {
|
||||
RenderCD(isLargePauseWidget = false)
|
||||
RenderCover(isLargePauseWidget = false)
|
||||
RenderPlayController(false)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = GlanceModifier
|
||||
.width(size.width)
|
||||
.height(size.height)
|
||||
.padding(radius)
|
||||
) {
|
||||
Box(
|
||||
modifier = GlanceModifier.defaultWeight()
|
||||
) {
|
||||
RenderLogo(size = 22.dp, paddingInDp = (-6).dp)
|
||||
}
|
||||
|
||||
RenderTextArr(stateFontSize = 9.sp, infoFontSize = 11.sp)
|
||||
}
|
||||
|
||||
RenderFavorite(imageSize = 20.dp, paddingInDp = radius)
|
||||
}
|
||||
}
|
||||
|
||||
open fun calcCdOffsetX(): Dp = cdSize / 2 - 10.dp
|
||||
|
||||
open fun calcCdOffsetY(): Dp = cdSize / 2 - 10.dp
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
@Composable
|
||||
open fun RenderTextArr(
|
||||
stateFontSize: TextUnit,
|
||||
infoFontSize: TextUnit
|
||||
) {
|
||||
val textWidth = if (size.width == AppUtils.HORIZONTAL_RECTANGLE.width) {
|
||||
size.width - 135.dp
|
||||
} else {
|
||||
size.width - 75.dp
|
||||
}
|
||||
val playTextArr = playText.split(",")
|
||||
var playStateText = playTextArr[1]
|
||||
if (!isShutdown && isPlaying) {
|
||||
playStateText = playTextArr[0]
|
||||
}
|
||||
Text(
|
||||
text = playStateText,
|
||||
style = TextStyle(
|
||||
fontSize = stateFontSize,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = ColorProvider(Color.Gray)
|
||||
),
|
||||
maxLines = 1,
|
||||
modifier = GlanceModifier.width(textWidth),
|
||||
)
|
||||
|
||||
Text(
|
||||
text = songName,
|
||||
style = TextStyle(
|
||||
fontSize = infoFontSize,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = ColorProvider(if (isWhite) Color.Black else Color.White)
|
||||
),
|
||||
maxLines = 1,
|
||||
modifier = GlanceModifier.width(textWidth),
|
||||
)
|
||||
|
||||
Text(
|
||||
text = songArtist,
|
||||
style = TextStyle(
|
||||
fontSize = infoFontSize,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = ColorProvider(Color.Gray)
|
||||
),
|
||||
maxLines = 1,
|
||||
modifier = GlanceModifier.width(textWidth),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
open fun RenderLogo(size: Dp, paddingInDp: Dp) {
|
||||
Image(
|
||||
provider = ImageProvider(R.drawable.logo),
|
||||
contentDescription = "logo",
|
||||
modifier = GlanceModifier
|
||||
.size(size)
|
||||
.padding(start = paddingInDp)
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
@Composable
|
||||
private fun RenderTransportButton(
|
||||
drawable: Int,
|
||||
description: String,
|
||||
uri: String,
|
||||
imageSize: Dp,
|
||||
) {
|
||||
val base = GlanceModifier.size(imageSize)
|
||||
val modifier = if (isShutdown) {
|
||||
base
|
||||
} else {
|
||||
base.clickable(
|
||||
rippleOverride = R.drawable.click_ripple,
|
||||
onClick = widgetAction(LocalContext.current, uri),
|
||||
)
|
||||
}
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = modifier,
|
||||
) {
|
||||
Image(
|
||||
provider = ImageProvider(drawable),
|
||||
contentDescription = description,
|
||||
colorFilter = ColorFilter.tint(ColorProvider(Color.White)),
|
||||
modifier = GlanceModifier.size(imageSize * TRANSPORT_ICON_SCALE),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
open fun RenderCD(isLargePauseWidget: Boolean) {
|
||||
val cdDrawable = if (isWhite) R.drawable.cd_white else R.drawable.cd_black
|
||||
val offsetX = calcCdOffsetX()
|
||||
val offsetY = calcCdOffsetY()
|
||||
|
||||
val startPadding = if (isLargePauseWidget) 0.dp else offsetX
|
||||
val radio = if (isLargePauseWidget) 0.9f else 1f
|
||||
if (isShutdown) {
|
||||
Image(
|
||||
provider = ImageProvider(cdDrawable),
|
||||
contentDescription = "cd",
|
||||
modifier = GlanceModifier
|
||||
.size(size.height * radio)
|
||||
.padding(
|
||||
start = startPadding,
|
||||
top = -offsetY,
|
||||
end = -offsetX,
|
||||
bottom = offsetY
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Image(
|
||||
provider = ImageProvider(cdDrawable),
|
||||
contentDescription = "cd",
|
||||
modifier = GlanceModifier
|
||||
.size(size.height * radio)
|
||||
.padding(
|
||||
start = startPadding,
|
||||
top = -offsetY,
|
||||
end = -offsetX,
|
||||
bottom = offsetY
|
||||
)
|
||||
.clickable(rippleOverride = R.drawable.click_ripple, onClick = widgetAction(LocalContext.current, "${AppUtils.HOME_WIDGET_URI_PREFIX}toggle_play"))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
open fun RenderCover(isLargePauseWidget: Boolean) {
|
||||
val coverSize = cdSize * 0.4f
|
||||
val coverAndCdDiffSize = (cdSize - coverSize) / 2
|
||||
val offsetX = calcCdOffsetX()
|
||||
val offsetY = calcCdOffsetY()
|
||||
val startPadding =
|
||||
if (isLargePauseWidget) coverAndCdDiffSize else offsetX + coverAndCdDiffSize
|
||||
val radio = if (isLargePauseWidget) 0.95f else 1f
|
||||
coverPath?.let {
|
||||
if (File(it).exists()) {
|
||||
Image(
|
||||
provider = ImageProvider(BitmapFactory.decodeFile(it)),
|
||||
contentDescription = "cover",
|
||||
modifier = GlanceModifier
|
||||
.size(size.height * radio)
|
||||
.padding(
|
||||
start = startPadding,
|
||||
top = -offsetY + coverAndCdDiffSize,
|
||||
bottom = offsetY + coverAndCdDiffSize,
|
||||
end = -offsetX + coverAndCdDiffSize
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
@Composable
|
||||
open fun RenderPlayController(isLargePauseWidget: Boolean) {
|
||||
val resId = if (isPlaying) R.drawable.vector_pause else R.drawable.vector_play
|
||||
val toggleAction = widgetAction(
|
||||
LocalContext.current,
|
||||
"${AppUtils.HOME_WIDGET_URI_PREFIX}toggle_play",
|
||||
)
|
||||
|
||||
if (isLargePauseWidget) {
|
||||
val modifier = if (isShutdown) {
|
||||
GlanceModifier.size(20.dp)
|
||||
} else {
|
||||
GlanceModifier
|
||||
.size(20.dp)
|
||||
.clickable(rippleOverride = R.drawable.click_ripple, onClick = toggleAction)
|
||||
}
|
||||
Image(
|
||||
provider = ImageProvider(resId),
|
||||
contentDescription = "playButton",
|
||||
colorFilter = ColorFilter.tint(colorProvider = ColorProvider(Color.White)),
|
||||
modifier = modifier,
|
||||
)
|
||||
} else {
|
||||
val padding =
|
||||
if (size.width == AppUtils.HORIZONTAL_RECTANGLE.width) 26.dp else 23.dp
|
||||
val playModifier = if (isShutdown) {
|
||||
GlanceModifier.size(15.dp)
|
||||
} else {
|
||||
GlanceModifier
|
||||
.size(15.dp)
|
||||
.clickable(rippleOverride = R.drawable.click_ripple, onClick = toggleAction)
|
||||
}
|
||||
Box(
|
||||
contentAlignment = Alignment.TopEnd,
|
||||
modifier = GlanceModifier
|
||||
.fillMaxSize()
|
||||
.padding(top = padding, end = padding)
|
||||
) {
|
||||
Image(
|
||||
provider = ImageProvider(resId),
|
||||
contentDescription = "playButton",
|
||||
colorFilter = ColorFilter.tint(colorProvider = ColorProvider(Color.White)),
|
||||
modifier = playModifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
open fun RenderFavorite(imageSize: Dp, paddingInDp: Dp) {
|
||||
val isLarge = size.width == AppUtils.HORIZONTAL_RECTANGLE.width
|
||||
Box(
|
||||
contentAlignment = Alignment.BottomEnd,
|
||||
modifier = GlanceModifier
|
||||
.width(size.width)
|
||||
.height(size.height)
|
||||
.padding(paddingInDp)
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (isLarge) {
|
||||
RenderTransportButton(
|
||||
drawable = R.drawable.player_play_prev,
|
||||
description = "previous",
|
||||
uri = "${AppUtils.HOME_WIDGET_URI_PREFIX}previous",
|
||||
imageSize = imageSize,
|
||||
)
|
||||
Spacer(GlanceModifier.width(8.dp))
|
||||
}
|
||||
RenderTransportButton(
|
||||
drawable = R.drawable.player_play_next,
|
||||
description = "next",
|
||||
uri = "${AppUtils.HOME_WIDGET_URI_PREFIX}next",
|
||||
imageSize = imageSize,
|
||||
)
|
||||
Spacer(GlanceModifier.width(8.dp))
|
||||
val loveIcon =
|
||||
if (isFavorite) R.drawable.widget_fav_click else R.drawable.widget_fav_unclick
|
||||
val favoriteModifier = if (isShutdown) {
|
||||
GlanceModifier.size(imageSize)
|
||||
} else {
|
||||
GlanceModifier
|
||||
.size(imageSize)
|
||||
.clickable(
|
||||
rippleOverride = R.drawable.click_ripple,
|
||||
onClick = widgetAction(
|
||||
LocalContext.current,
|
||||
"${AppUtils.HOME_WIDGET_URI_PREFIX}toggle_love",
|
||||
),
|
||||
)
|
||||
}
|
||||
Image(
|
||||
provider = ImageProvider(loveIcon),
|
||||
contentDescription = "love",
|
||||
modifier = favoriteModifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GradientRectangle(color: Color) {
|
||||
val bitmap = ImageUtil.createGradientImage(size.width.value, size.height.value, color)
|
||||
|
||||
Box(
|
||||
modifier = GlanceModifier
|
||||
.width(size.width)
|
||||
.height(size.height)
|
||||
.cornerRadius(radius)
|
||||
) {
|
||||
Image(
|
||||
provider = ImageProvider(bitmap),
|
||||
contentDescription = "bg",
|
||||
modifier = GlanceModifier
|
||||
.width(size.width)
|
||||
.height(size.height)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||||
val preferences = context.getSharedPreferences(AppUtils.SP_NAME, Context.MODE_PRIVATE)
|
||||
provideContent { GlanceContent(preferences = preferences) }
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val TRANSPORT_ICON_SCALE = 0.58f
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package top.zhushenwudi.llmp.native.home_widget.large_home_widget
|
||||
package com.zhushenwudi.lovelivemusicplayer.native.home_widget.large_home_widget
|
||||
|
||||
import androidx.glance.appwidget.SizeMode
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package top.zhushenwudi.llmp.native.home_widget.large_home_widget
|
||||
package com.zhushenwudi.lovelivemusicplayer.native.home_widget.large_home_widget
|
||||
|
||||
import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.GlanceAppWidgetReceiver
|
||||
@@ -1,60 +1,59 @@
|
||||
package top.zhushenwudi.llmp.native.home_widget.large_home_widget
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import top.zhushenwudi.llmp.native.home_widget.HomeWidgetContent
|
||||
import top.zhushenwudi.llmp.native.util.AppUtils
|
||||
|
||||
@SuppressLint("CommitPrefEdits", "DefaultLocale")
|
||||
abstract class LargeHomeWidgetContent(
|
||||
override var size: DpSize = AppUtils.HORIZONTAL_RECTANGLE,
|
||||
override var radius: Dp = 12.dp,
|
||||
override var cdSize: Dp = 120.dp,
|
||||
) : HomeWidgetContent() {
|
||||
|
||||
private fun isLargePauseWidget(): Boolean = !isPlaying || isShutdown
|
||||
|
||||
@Composable
|
||||
override fun RenderLogo(size: Dp, paddingInDp: Dp) {
|
||||
super.RenderLogo(size = 30.dp, paddingInDp)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun RenderCD(isLargePauseWidget: Boolean) {
|
||||
super.RenderCD(isLargePauseWidget = isLargePauseWidget())
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun RenderLyric(isLargePauseWidget: Boolean) {
|
||||
super.RenderLyric(isLargePauseWidget = isLargePauseWidget())
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun RenderCover(isLargePauseWidget: Boolean) {
|
||||
super.RenderCover(isLargePauseWidget = isLargePauseWidget())
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun RenderPlayController(isLargePauseWidget: Boolean) {
|
||||
super.RenderPlayController(isLargePauseWidget = isLargePauseWidget())
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun RenderTextArr(stateFontSize: TextUnit, infoFontSize: TextUnit) {
|
||||
super.RenderTextArr(stateFontSize = 12.sp, infoFontSize = 13.sp)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun RenderFavorite(imageSize: Dp, paddingInDp: Dp) {
|
||||
super.RenderFavorite(imageSize = 28.dp, paddingInDp = radius / 2)
|
||||
}
|
||||
|
||||
override fun calcCdOffsetX(): Dp = if (isPlaying) (cdSize / 2 - 10.dp) else 0.dp
|
||||
|
||||
override fun calcCdOffsetY(): Dp = if (isPlaying) (cdSize / 2 - 10.dp) else 0.dp
|
||||
}
|
||||
package com.zhushenwudi.lovelivemusicplayer.native.home_widget.large_home_widget
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.zhushenwudi.lovelivemusicplayer.native.home_widget.HomeWidgetContent
|
||||
import com.zhushenwudi.lovelivemusicplayer.native.util.AppUtils
|
||||
|
||||
@SuppressLint("CommitPrefEdits", "DefaultLocale")
|
||||
abstract class LargeHomeWidgetContent(
|
||||
override var size: DpSize = AppUtils.HORIZONTAL_RECTANGLE,
|
||||
override var radius: Dp = 12.dp,
|
||||
override var cdSize: Dp = 120.dp,
|
||||
) : HomeWidgetContent() {
|
||||
|
||||
private fun isLargePauseWidget(): Boolean {
|
||||
return !isPlaying || isShutdown
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun RenderLogo(size: Dp, paddingInDp: Dp) {
|
||||
super.RenderLogo(size = 30.dp, paddingInDp)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun RenderCD(isLargePauseWidget: Boolean) {
|
||||
super.RenderCD(isLargePauseWidget = isLargePauseWidget())
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun RenderCover(isLargePauseWidget: Boolean) {
|
||||
super.RenderCover(isLargePauseWidget = isLargePauseWidget())
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun RenderPlayController(isLargePauseWidget: Boolean) {
|
||||
super.RenderPlayController(isLargePauseWidget = isLargePauseWidget())
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun RenderTextArr(stateFontSize: TextUnit, infoFontSize: TextUnit) {
|
||||
super.RenderTextArr(stateFontSize = 12.sp, infoFontSize = 13.sp)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun RenderFavorite(imageSize: Dp, paddingInDp: Dp) {
|
||||
super.RenderFavorite(imageSize = 28.dp, paddingInDp = radius / 2)
|
||||
}
|
||||
|
||||
override fun calcCdOffsetX(): Dp =
|
||||
if (isPlaying) cdSize / 2 + 5.dp else 12.dp
|
||||
|
||||
override fun calcCdOffsetY(): Dp =
|
||||
if (isPlaying) cdSize / 2 + 5.dp else 12.dp
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package top.zhushenwudi.llmp.native.home_widget.large_home_widget
|
||||
package com.zhushenwudi.lovelivemusicplayer.native.home_widget.large_home_widget
|
||||
|
||||
import androidx.glance.appwidget.SizeMode
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package top.zhushenwudi.llmp.native.home_widget.large_home_widget
|
||||
package com.zhushenwudi.lovelivemusicplayer.native.home_widget.large_home_widget
|
||||
|
||||
import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.GlanceAppWidgetReceiver
|
||||
@@ -1,4 +1,4 @@
|
||||
package top.zhushenwudi.llmp.native.home_widget.small_home_widget
|
||||
package com.zhushenwudi.lovelivemusicplayer.native.home_widget.small_home_widget
|
||||
|
||||
import androidx.glance.appwidget.SizeMode
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package top.zhushenwudi.llmp.native.home_widget.small_home_widget
|
||||
package com.zhushenwudi.lovelivemusicplayer.native.home_widget.small_home_widget
|
||||
|
||||
import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.GlanceAppWidgetReceiver
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.zhushenwudi.lovelivemusicplayer.native.home_widget.small_home_widget
|
||||
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zhushenwudi.lovelivemusicplayer.native.home_widget.HomeWidgetContent
|
||||
|
||||
abstract class SmallHomeWidgetContent(
|
||||
override var radius: Dp = 6.dp
|
||||
) : HomeWidgetContent()
|
||||
@@ -1,4 +1,4 @@
|
||||
package top.zhushenwudi.llmp.native.home_widget.small_home_widget
|
||||
package com.zhushenwudi.lovelivemusicplayer.native.home_widget.small_home_widget
|
||||
|
||||
import androidx.glance.appwidget.SizeMode
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package top.zhushenwudi.llmp.native.home_widget.small_home_widget
|
||||
package com.zhushenwudi.lovelivemusicplayer.native.home_widget.small_home_widget
|
||||
|
||||
import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.GlanceAppWidgetReceiver
|
||||
@@ -1,9 +1,9 @@
|
||||
package top.zhushenwudi.llmp.native.platform
|
||||
package com.zhushenwudi.lovelivemusicplayer.native.platform
|
||||
|
||||
import top.zhushenwudi.llmp.native.NativeEvents
|
||||
import top.zhushenwudi.llmp.platform.DeepLinkHandler
|
||||
import top.zhushenwudi.llmp.platform.DeepLinkParser
|
||||
import top.zhushenwudi.llmp.platform.DeepLinkPayload
|
||||
import com.zhushenwudi.lovelivemusicplayer.native.NativeEvents
|
||||
import com.zhushenwudi.lovelivemusicplayer.platform.DeepLinkHandler
|
||||
import com.zhushenwudi.lovelivemusicplayer.platform.DeepLinkParser
|
||||
import com.zhushenwudi.lovelivemusicplayer.platform.DeepLinkPayload
|
||||
|
||||
class AndroidDeepLinkHandler(
|
||||
private val onDeepLink: ((DeepLinkPayload) -> Unit)? = null,
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.zhushenwudi.lovelivemusicplayer.native.platform
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import android.widget.Toast
|
||||
import androidx.core.net.toUri
|
||||
import com.zhushenwudi.lovelivemusicplayer.i18n.I18n
|
||||
import com.zhushenwudi.lovelivemusicplayer.platform.AndroidUiHost
|
||||
import com.zhushenwudi.lovelivemusicplayer.platform.DesktopLyricController
|
||||
import com.zhushenwudi.lovelivemusicplayer.player.PlaybackService
|
||||
|
||||
/**
|
||||
* 桌面歌词控制器 — 对齐 Flutter [DesktopLyricPlugin] + [MainActivity]。
|
||||
* 叠加层由 [PlaybackService] 承载(共享 Media3 FGS)。
|
||||
*/
|
||||
class AndroidDesktopLyricController(
|
||||
context: android.content.Context,
|
||||
) : DesktopLyricController {
|
||||
private val appContext = context.applicationContext
|
||||
private var playing: Boolean = false
|
||||
|
||||
override var enabled: Boolean = false
|
||||
|
||||
override fun pipAutoOpen(enable: Boolean): Boolean {
|
||||
if (enable) {
|
||||
val ctx = AndroidUiHost.activity ?: appContext
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(ctx)) {
|
||||
val intent = Intent(
|
||||
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
|
||||
"package:${appContext.packageName}".toUri(),
|
||||
)
|
||||
val act = AndroidUiHost.activity
|
||||
if (act != null) {
|
||||
@Suppress("DEPRECATION")
|
||||
act.startActivityForResult(intent, REQUEST_CODE_OVERLAY_PERMISSION)
|
||||
} else {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
appContext.startActivity(intent)
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
enabled = enable
|
||||
if (!enable) {
|
||||
PlaybackService.hideDesktopLyric(appContext)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun show() {
|
||||
if (!enabled) return
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(appContext)) {
|
||||
return
|
||||
}
|
||||
PlaybackService.showDesktopLyric(appContext)
|
||||
}
|
||||
|
||||
override fun hide() {
|
||||
PlaybackService.hideDesktopLyric(appContext)
|
||||
}
|
||||
|
||||
override fun updateLines(line1: String?, line2: String?, currentLine: Int) {
|
||||
PlaybackService.updateDesktopLyric(line1, line2, currentLine)
|
||||
}
|
||||
|
||||
override fun setPlaying(playing: Boolean) {
|
||||
this.playing = playing
|
||||
}
|
||||
|
||||
override fun isOverlayShowing(): Boolean = PlaybackService.isDesktopLyricShowing()
|
||||
|
||||
fun isPlaying(): Boolean = playing
|
||||
|
||||
companion object {
|
||||
const val REQUEST_CODE_OVERLAY_PERMISSION = 0x10
|
||||
|
||||
fun onOverlayPermissionResult(granted: Boolean) {
|
||||
if (!granted) {
|
||||
val act = AndroidUiHost.activity ?: return
|
||||
Toast.makeText(act, I18n.t("no_permission"), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package com.zhushenwudi.lovelivemusicplayer.native.platform
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.core.content.edit
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import com.zhushenwudi.lovelivemusicplayer.native.home_widget.large_home_widget.BlackLargeHomeWidgetReceiver
|
||||
import com.zhushenwudi.lovelivemusicplayer.native.home_widget.large_home_widget.WhiteLargeHomeWidgetReceiver
|
||||
import com.zhushenwudi.lovelivemusicplayer.native.home_widget.small_home_widget.BlackSmallHomeWidgetReceiver
|
||||
import com.zhushenwudi.lovelivemusicplayer.native.home_widget.small_home_widget.WhiteSmallHomeWidgetReceiver
|
||||
import com.zhushenwudi.lovelivemusicplayer.native.util.AppUtils
|
||||
import com.zhushenwudi.lovelivemusicplayer.native.util.ImageUtil
|
||||
import com.zhushenwudi.lovelivemusicplayer.platform.HomeWidgetController
|
||||
import java.io.File
|
||||
|
||||
class AndroidHomeWidgetController(
|
||||
context: Context,
|
||||
) : HomeWidgetController {
|
||||
private val appContext = context.applicationContext
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
private var preImagePath: String? = null
|
||||
private var lastContent: WidgetContent? = null
|
||||
|
||||
private val receiverClasses = listOf(
|
||||
WhiteSmallHomeWidgetReceiver::class.java,
|
||||
BlackSmallHomeWidgetReceiver::class.java,
|
||||
WhiteLargeHomeWidgetReceiver::class.java,
|
||||
BlackLargeHomeWidgetReceiver::class.java,
|
||||
)
|
||||
|
||||
override fun update(
|
||||
songName: String?,
|
||||
artist: String?,
|
||||
isPlaying: Boolean,
|
||||
favorite: Boolean,
|
||||
coverPath: String?,
|
||||
playText: String,
|
||||
) {
|
||||
val content = WidgetContent(
|
||||
songName = songName.orEmpty(),
|
||||
artist = artist.orEmpty(),
|
||||
isPlaying = isPlaying,
|
||||
favorite = favorite,
|
||||
playText = playText,
|
||||
)
|
||||
val contentChanged = content != lastContent
|
||||
val coverChanged = !coverPath.isNullOrBlank() && coverPath != preImagePath
|
||||
if (!contentChanged && !coverChanged) return
|
||||
|
||||
if (contentChanged) {
|
||||
lastContent = content
|
||||
appContext.getSharedPreferences(AppUtils.SP_NAME, Context.MODE_PRIVATE)
|
||||
.edit(commit = true) {
|
||||
putString("songName", content.songName)
|
||||
putString("songArtist", content.artist)
|
||||
putBoolean("songFavorite", content.favorite)
|
||||
putBoolean("isPlaying", content.isPlaying)
|
||||
putString("playText", content.playText)
|
||||
if (content.songName.isNotBlank() || content.isPlaying) {
|
||||
putBoolean("isShutdown", false)
|
||||
}
|
||||
}
|
||||
refresh()
|
||||
}
|
||||
if (coverChanged) coverPath?.let(::processCover)
|
||||
}
|
||||
|
||||
override fun setShutdown(shutdown: Boolean) {
|
||||
appContext.getSharedPreferences(AppUtils.SP_NAME, Context.MODE_PRIVATE)
|
||||
.edit(commit = true) {
|
||||
putBoolean("isShutdown", shutdown)
|
||||
if (shutdown) {
|
||||
putBoolean("isPlaying", false)
|
||||
}
|
||||
}
|
||||
refresh()
|
||||
}
|
||||
|
||||
override fun refresh() {
|
||||
receiverClasses.forEach { receiverClass ->
|
||||
sendWidgetUpdate(receiverClass)
|
||||
}
|
||||
}
|
||||
|
||||
private fun processCover(coverPath: String) {
|
||||
if (preImagePath == coverPath || !File(coverPath).exists()) return
|
||||
preImagePath = coverPath
|
||||
scope.launch {
|
||||
try {
|
||||
val originBitmap = BitmapFactory.decodeFile(coverPath)
|
||||
val coverBitmap = ImageUtil.squareAndCircularBitmap(originBitmap, 180)
|
||||
val handledCoverPath = ImageUtil.saveBitmapToFile(appContext, coverBitmap)
|
||||
ImageUtil.savePathToSp(appContext, handledCoverPath)
|
||||
val color = ImageUtil.parsePhotoDomainColor(coverBitmap)
|
||||
ImageUtil.saveColorToSp(appContext, color)
|
||||
originBitmap.recycle()
|
||||
coverBitmap.recycle()
|
||||
} catch (_: Exception) {
|
||||
} finally {
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendWidgetUpdate(receiverClass: Class<*>) {
|
||||
val ids = AppWidgetManager.getInstance(appContext)
|
||||
.getAppWidgetIds(ComponentName(appContext, receiverClass))
|
||||
if (ids.isEmpty()) return
|
||||
val intent = Intent(appContext, receiverClass).apply {
|
||||
action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
|
||||
putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
|
||||
}
|
||||
appContext.sendBroadcast(intent)
|
||||
}
|
||||
|
||||
private data class WidgetContent(
|
||||
val songName: String,
|
||||
val artist: String,
|
||||
val isPlaying: Boolean,
|
||||
val favorite: Boolean,
|
||||
val playText: String,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package com.zhushenwudi.lovelivemusicplayer.native.platform
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.widget.Toast
|
||||
import com.tencent.connect.share.QQShare
|
||||
import com.tencent.tauth.IUiListener
|
||||
import com.tencent.tauth.Tencent
|
||||
import com.tencent.tauth.UiError
|
||||
import com.zhushenwudi.lovelivemusicplayer.Const
|
||||
import com.zhushenwudi.lovelivemusicplayer.i18n.I18n
|
||||
import com.zhushenwudi.lovelivemusicplayer.platform.AndroidUiHost
|
||||
import com.zhushenwudi.lovelivemusicplayer.platform.ShareBridge
|
||||
|
||||
class AndroidShareBridge(
|
||||
context: Context,
|
||||
) : ShareBridge {
|
||||
private val appContext = context.applicationContext
|
||||
|
||||
override fun shareText(text: String, title: String?) {
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_TEXT, text)
|
||||
title?.let { putExtra(Intent.EXTRA_TITLE, it) }
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
appContext.startActivity(
|
||||
Intent.createChooser(intent, title ?: I18n.t("share_chooser")).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||
)
|
||||
}
|
||||
|
||||
override fun shareUrl(url: String, title: String?) {
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_TEXT, url)
|
||||
title?.let { putExtra(Intent.EXTRA_TITLE, it) }
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
appContext.startActivity(
|
||||
Intent.createChooser(intent, title ?: I18n.t("share_chooser")).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||
)
|
||||
}
|
||||
|
||||
override fun shareToQq(
|
||||
title: String,
|
||||
text: String,
|
||||
url: String,
|
||||
imageUrl: String?,
|
||||
imagePath: String?,
|
||||
) {
|
||||
val activity = AndroidUiHost.activity
|
||||
if (activity == null) {
|
||||
toast(I18n.t("share_error_n", I18n.t("no_activity")))
|
||||
return
|
||||
}
|
||||
val sdk = tencent(appContext)
|
||||
if (!sdk.isQQInstalled(appContext)) {
|
||||
toast(I18n.t("no_qq"))
|
||||
return
|
||||
}
|
||||
val params = Bundle().apply {
|
||||
putInt(QQShare.SHARE_TO_QQ_KEY_TYPE, QQShare.SHARE_TO_QQ_TYPE_DEFAULT)
|
||||
putString(QQShare.SHARE_TO_QQ_TITLE, title)
|
||||
putString(QQShare.SHARE_TO_QQ_SUMMARY, text)
|
||||
putString(QQShare.SHARE_TO_QQ_TARGET_URL, url)
|
||||
putString(QQShare.SHARE_TO_QQ_APP_NAME, "LLMP")
|
||||
val preview = imageUrl?.takeIf { it.startsWith("http") } ?: Const.SHARE_DEFAULT_LOGO
|
||||
putString(QQShare.SHARE_TO_QQ_IMAGE_URL, preview)
|
||||
}
|
||||
runCatching {
|
||||
sdk.shareToQQ(activity, params, listener)
|
||||
}.onFailure { e ->
|
||||
toast(I18n.t("share_error_n", e.message ?: e.javaClass.simpleName))
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val listener: IUiListener = QqShareUiListener()
|
||||
|
||||
@Volatile
|
||||
private var instance: Tencent? = null
|
||||
|
||||
fun tencent(context: Context): Tencent {
|
||||
Tencent.setIsPermissionGranted(true, Build.MODEL)
|
||||
instance?.let { return it }
|
||||
val created = Tencent.createInstance(
|
||||
Const.QQ_APP_ID,
|
||||
context.applicationContext,
|
||||
"${context.packageName}.fileprovider",
|
||||
)
|
||||
instance = created
|
||||
return created
|
||||
}
|
||||
|
||||
fun handleActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
Tencent.onActivityResultData(requestCode, resultCode, data, listener)
|
||||
}
|
||||
|
||||
fun handleIntentResult(intent: Intent?) {
|
||||
if (intent == null) return
|
||||
Tencent.handleResultData(intent, listener)
|
||||
}
|
||||
|
||||
fun toast(message: String) {
|
||||
val ctx = instanceContext
|
||||
if (ctx == null) return
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
Toast.makeText(ctx, "\u202A$message\u202C", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
@Volatile
|
||||
private var instanceContext: Context? = null
|
||||
}
|
||||
|
||||
init {
|
||||
instanceContext = appContext
|
||||
}
|
||||
}
|
||||
|
||||
private class QqShareUiListener : IUiListener {
|
||||
override fun onComplete(response: Any?) {
|
||||
AndroidShareBridge.toast(I18n.t("share_success"))
|
||||
}
|
||||
|
||||
override fun onError(e: UiError?) {
|
||||
val code = e?.errorCode ?: -1
|
||||
val detail = listOfNotNull(e?.errorMessage, e?.errorDetail)
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotEmpty() }
|
||||
.joinToString(" ")
|
||||
val suffix = if (detail.isEmpty()) "$code" else "$code $detail"
|
||||
AndroidShareBridge.toast(I18n.t("share_error_n", suffix))
|
||||
}
|
||||
|
||||
override fun onCancel() {
|
||||
AndroidShareBridge.toast(I18n.t("share_cancel"))
|
||||
}
|
||||
|
||||
override fun onWarning(code: Int) = Unit
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package top.zhushenwudi.llmp.native.platform
|
||||
package com.zhushenwudi.lovelivemusicplayer.native.platform
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
@@ -8,8 +8,8 @@ import android.os.Build
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import top.zhushenwudi.llmp.platform.FileSystem
|
||||
import top.zhushenwudi.llmp.platform.UsbMountMonitor
|
||||
import com.zhushenwudi.lovelivemusicplayer.platform.FileSystem
|
||||
import com.zhushenwudi.lovelivemusicplayer.platform.UsbMountMonitor
|
||||
|
||||
enum class UsbMountEvent {
|
||||
MOUNTED,
|
||||
@@ -22,12 +22,14 @@ class AndroidUsbMountMonitor(
|
||||
) : UsbMountMonitor {
|
||||
private val appContext = context.applicationContext
|
||||
private var receiver: BroadcastReceiver? = null
|
||||
private var onChanged: (() -> Unit)? = null
|
||||
|
||||
private val _events = MutableSharedFlow<UsbMountEvent>(extraBufferCapacity = 8)
|
||||
val events: SharedFlow<UsbMountEvent> = _events.asSharedFlow()
|
||||
|
||||
var onMount: (() -> Unit)? = null
|
||||
var onEject: (() -> Unit)? = null
|
||||
override fun setOnChanged(listener: (() -> Unit)?) {
|
||||
onChanged = listener
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
if (receiver != null) return
|
||||
@@ -39,19 +41,16 @@ class AndroidUsbMountMonitor(
|
||||
receiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
when (intent?.action) {
|
||||
Intent.ACTION_MEDIA_MOUNTED -> {
|
||||
_events.tryEmit(UsbMountEvent.MOUNTED)
|
||||
onMount?.invoke()
|
||||
}
|
||||
Intent.ACTION_MEDIA_EJECT -> {
|
||||
_events.tryEmit(UsbMountEvent.EJECTED)
|
||||
onEject?.invoke()
|
||||
}
|
||||
Intent.ACTION_MEDIA_MOUNTED -> _events.tryEmit(UsbMountEvent.MOUNTED)
|
||||
Intent.ACTION_MEDIA_EJECT -> _events.tryEmit(UsbMountEvent.EJECTED)
|
||||
else -> return
|
||||
}
|
||||
onChanged?.invoke()
|
||||
}
|
||||
}
|
||||
// 系统 MEDIA_* 广播来自其他进程,必须 EXPORTED,否则 Android 13+ 收不到。
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
appContext.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED)
|
||||
appContext.registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED)
|
||||
} else {
|
||||
@Suppress("UnspecifiedRegisterReceiverFlag")
|
||||
appContext.registerReceiver(receiver, filter)
|
||||
@@ -1,10 +1,11 @@
|
||||
package top.zhushenwudi.llmp.native.platform
|
||||
package com.zhushenwudi.lovelivemusicplayer.native.platform
|
||||
|
||||
import android.content.Context
|
||||
import top.zhushenwudi.llmp.platform.AndroidMediaSessionBridge
|
||||
import top.zhushenwudi.llmp.platform.PlatformServices
|
||||
import top.zhushenwudi.llmp.platform.createFileSystem
|
||||
import top.zhushenwudi.llmp.native.UmengBridge
|
||||
import com.zhushenwudi.lovelivemusicplayer.AppContainer
|
||||
import com.zhushenwudi.lovelivemusicplayer.native.UmengBridge
|
||||
import com.zhushenwudi.lovelivemusicplayer.platform.AndroidMediaSessionBridge
|
||||
import com.zhushenwudi.lovelivemusicplayer.platform.PlatformServices
|
||||
import com.zhushenwudi.lovelivemusicplayer.platform.createFileSystem
|
||||
|
||||
fun installAndroidPlatformServices(context: Context) {
|
||||
val appContext = context.applicationContext
|
||||
@@ -23,4 +24,5 @@ fun installAndroidPlatformServices(context: Context) {
|
||||
)
|
||||
|
||||
(PlatformServices.usbMount as? AndroidUsbMountMonitor)?.start()
|
||||
AppContainer.bindUsbMountListener()
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
package top.zhushenwudi.llmp.native.util
|
||||
package com.zhushenwudi.lovelivemusicplayer.native.util
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import top.zhushenwudi.llmp.native.UmengBridge
|
||||
import com.zhushenwudi.lovelivemusicplayer.native.UmengBridge
|
||||
|
||||
object AppUtils {
|
||||
private const val PREFIX_URL = "llmp://"
|
||||
const val HOME_WIDGET_SCHEME = "homeWidgetExample"
|
||||
const val HOME_WIDGET_URI_PREFIX = "$HOME_WIDGET_SCHEME://"
|
||||
const val SAVE_FILE_NAME = "sharedImage.png"
|
||||
const val SP_NAME = "HomeWidgetPreferences"
|
||||
|
||||
@@ -40,6 +42,7 @@ object AppUtils {
|
||||
return false
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun getRunningService(mContext: Context): ArrayList<ActivityManager.RunningServiceInfo> {
|
||||
val myManager =
|
||||
mContext.applicationContext.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
||||
@@ -1,4 +1,4 @@
|
||||
package top.zhushenwudi.llmp.native.util
|
||||
package com.zhushenwudi.lovelivemusicplayer.native.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
@@ -62,7 +62,7 @@ object ImageUtil {
|
||||
val circularBitmap = createBitmap(bitmap.width, bitmap.height)
|
||||
val paint = Paint()
|
||||
paint.isAntiAlias = true
|
||||
paint.setShader(BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP))
|
||||
paint.shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
|
||||
val canvas = Canvas(circularBitmap)
|
||||
canvas.drawCircle(bitmap.width / 2f, bitmap.height / 2f, bitmap.width / 2f, paint)
|
||||
return circularBitmap
|
||||
@@ -121,7 +121,7 @@ object ImageUtil {
|
||||
null,
|
||||
Shader.TileMode.CLAMP,
|
||||
)
|
||||
paint.setShader(gradient)
|
||||
paint.shader = gradient
|
||||
canvas.drawRect(rect, paint)
|
||||
return bitmap
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package top.zhushenwudi.llmp.player
|
||||
package com.zhushenwudi.lovelivemusicplayer.player
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
@@ -20,15 +20,15 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import top.zhushenwudi.llmp.MainActivity
|
||||
import top.zhushenwudi.llmp.R
|
||||
import top.zhushenwudi.llmp.native.DesktopLyricOverlay
|
||||
import com.zhushenwudi.lovelivemusicplayer.MainActivity
|
||||
import com.zhushenwudi.lovelivemusicplayer.i18n.I18n
|
||||
import com.zhushenwudi.lovelivemusicplayer.R
|
||||
import com.zhushenwudi.lovelivemusicplayer.native.DesktopLyricOverlay
|
||||
|
||||
/**
|
||||
* Media3 [MediaSessionService] hosting the shared ExoPlayer + optional desktop-lyric overlay.
|
||||
* Media3 [androidx.media3.session.MediaSessionService],承载共享 ExoPlayer + 可选桌面歌词叠加层。
|
||||
*
|
||||
* One FGS notification (Media3 media style) covers playback; floating lyrics no longer post a
|
||||
* second "歌词服务" notification.
|
||||
* 一个 FGS 通知(Media3 媒体样式)覆盖播放;悬浮歌词不再发第二条「歌词服务」通知。
|
||||
*/
|
||||
class PlaybackService : MediaSessionService() {
|
||||
private var mediaSession: MediaSession? = null
|
||||
@@ -58,7 +58,7 @@ class PlaybackService : MediaSessionService() {
|
||||
mediaSession = session
|
||||
addSession(session)
|
||||
|
||||
// Satisfy startForegroundService() until Media3 posts the real media notification.
|
||||
// 在 Media3 发布真实媒体通知前满足 startForegroundService()。
|
||||
val notification = bootstrapNotification()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
startForeground(
|
||||
@@ -138,7 +138,7 @@ class PlaybackService : MediaSessionService() {
|
||||
private fun bootstrapNotification() =
|
||||
NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle(getString(R.string.app_name))
|
||||
.setContentText("准备播放")
|
||||
.setContentText(I18n.t("ready_to_play"))
|
||||
.setSmallIcon(R.drawable.logo)
|
||||
.setContentIntent(sessionActivityIntent())
|
||||
.setOngoing(true)
|
||||
@@ -160,8 +160,8 @@ class PlaybackService : MediaSessionService() {
|
||||
companion object {
|
||||
const val CHANNEL_ID = "llmp_playback"
|
||||
const val NOTIF_ID = 1001
|
||||
const val ACTION_SHOW_DESKTOP_LYRIC = "top.zhushenwudi.llmp.SHOW_DESKTOP_LYRIC"
|
||||
const val ACTION_HIDE_DESKTOP_LYRIC = "top.zhushenwudi.llmp.HIDE_DESKTOP_LYRIC"
|
||||
const val ACTION_SHOW_DESKTOP_LYRIC = "com.zhushenwudi.lovelivemusicplayer.SHOW_DESKTOP_LYRIC"
|
||||
const val ACTION_HIDE_DESKTOP_LYRIC = "com.zhushenwudi.lovelivemusicplayer.HIDE_DESKTOP_LYRIC"
|
||||
|
||||
@Volatile
|
||||
private var instance: PlaybackService? = null
|
||||
@@ -171,7 +171,7 @@ class PlaybackService : MediaSessionService() {
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
}
|
||||
|
||||
/** Show floating lyrics; reuses this service's Media3 FGS notification. */
|
||||
/** 显示悬浮歌词;复用本服务的 Media3 FGS 通知。 */
|
||||
fun showDesktopLyric(context: Context) {
|
||||
val existing = instance
|
||||
if (existing != null) {
|
||||
@@ -184,14 +184,14 @@ class PlaybackService : MediaSessionService() {
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
}
|
||||
|
||||
/** Hide floating lyrics without stopping media playback / FGS. */
|
||||
/** 隐藏悬浮歌词,不停止媒体播放/FGS。 */
|
||||
fun hideDesktopLyric(context: Context) {
|
||||
val existing = instance
|
||||
if (existing != null) {
|
||||
existing.hideDesktopLyricInternal()
|
||||
return
|
||||
}
|
||||
// Service not running — nothing to hide.
|
||||
// 服务未运行,无需隐藏。
|
||||
}
|
||||
|
||||
fun updateDesktopLyric(line1: String?, line2: String?, currentLine: Int) {
|
||||
@@ -1,205 +0,0 @@
|
||||
package top.zhushenwudi.llmp
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.edit
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import java.io.File
|
||||
import kotlinx.coroutines.launch
|
||||
import top.zhushenwudi.llmp.db.DriverFactory
|
||||
import top.zhushenwudi.llmp.db.createDatabase
|
||||
import top.zhushenwudi.llmp.native.NativeEvents
|
||||
import top.zhushenwudi.llmp.native.platform.installAndroidPlatformServices
|
||||
import top.zhushenwudi.llmp.native.util.AppUtils
|
||||
import top.zhushenwudi.llmp.platform.AndroidUiHost
|
||||
import top.zhushenwudi.llmp.platform.PlatformServices
|
||||
import top.zhushenwudi.llmp.platform.initAndroidFileSystem
|
||||
import top.zhushenwudi.llmp.player.PlaybackService
|
||||
import top.zhushenwudi.llmp.player.PlayerController
|
||||
import top.zhushenwudi.llmp.settings.createAppSettings
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private var player: PlayerController? = null
|
||||
|
||||
private val pickImageLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.GetContent(),
|
||||
) { uri ->
|
||||
if (uri == null) {
|
||||
AndroidUiHost.imagePickHandler?.invoke(null)
|
||||
return@registerForActivityResult
|
||||
}
|
||||
val path = runCatching {
|
||||
val dir = File(filesDir, "bg")
|
||||
if (!dir.exists()) dir.mkdirs()
|
||||
val out = File(dir, "${System.currentTimeMillis()}.jpg")
|
||||
contentResolver.openInputStream(uri)?.use { input ->
|
||||
out.outputStream().use { output -> input.copyTo(output) }
|
||||
}
|
||||
out.absolutePath
|
||||
}.getOrNull()
|
||||
AndroidUiHost.imagePickHandler?.invoke(path)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
AndroidUiHost.activity = this
|
||||
AndroidUiHost.launchImagePicker = { pickImageLauncher.launch("image/*") }
|
||||
initAndroidFileSystem(applicationContext)
|
||||
|
||||
if (!AppContainer.isInitialized) {
|
||||
val settings = createAppSettings(applicationContext)
|
||||
val database = createDatabase(DriverFactory(applicationContext))
|
||||
player = PlayerController(applicationContext)
|
||||
AppContainer.init(
|
||||
settings = settings,
|
||||
database = database,
|
||||
player = player!!,
|
||||
)
|
||||
AppContainer.musicTransfer.setPlatform("android")
|
||||
} else {
|
||||
player = AppContainer.player
|
||||
}
|
||||
|
||||
installAndroidPlatformServices(applicationContext)
|
||||
PlatformServices.desktopLyric.enabled = AppContainer.settings.enableDesktopLyric
|
||||
PlatformServices.analytics.initIfAgreed(
|
||||
getSharedPreferences("my_prefs", MODE_PRIVATE).getBoolean("key_agreed", false),
|
||||
)
|
||||
|
||||
getSharedPreferences(AppUtils.SP_NAME, MODE_PRIVATE).edit(commit = true) {
|
||||
putBoolean("isShutdown", false)
|
||||
}
|
||||
|
||||
handleIncomingIntent(intent)
|
||||
|
||||
onBackPressedDispatcher.addCallback(
|
||||
this,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
moveTaskToBack(true)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
requestNotifyPermission()
|
||||
startPlaybackService()
|
||||
|
||||
lifecycleScope.launch {
|
||||
NativeEvents.widgetEvents.collect { uri ->
|
||||
when {
|
||||
uri.endsWith("toggle_play") -> player?.toggle()
|
||||
uri.endsWith("toggle_love") -> {
|
||||
val id = player?.state?.value?.current?.musicId ?: return@collect
|
||||
val favorite = AppContainer.toggleLove(id)
|
||||
val current = player?.state?.value?.current
|
||||
PlatformServices.homeWidget.update(
|
||||
songName = current?.musicName,
|
||||
artist = current?.artist,
|
||||
isPlaying = player?.state?.value?.isPlaying == true,
|
||||
favorite = favorite,
|
||||
coverPath = current?.let {
|
||||
AppContainer.pathResolver.absoluteCoverPath(it)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setContent { App() }
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
handleIncomingIntent(intent)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
AndroidUiHost.activity = this
|
||||
AppUtils.getSchemeData(intent) { url ->
|
||||
PlatformServices.deepLink.handle(url)
|
||||
}
|
||||
// Foreground: hide floating lyric (PlaybackService keeps media FGS).
|
||||
if (PlatformServices.desktopLyric.enabled) {
|
||||
PlatformServices.desktopLyric.hide()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
val lyric = PlatformServices.desktopLyric
|
||||
val playing = player?.state?.value?.isPlaying == true
|
||||
if (lyric.enabled && playing) {
|
||||
if (!Settings.canDrawOverlays(this)) {
|
||||
// Permission missing — skip start; Settings toggle requests it.
|
||||
} else {
|
||||
lyric.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (AndroidUiHost.activity === this) {
|
||||
AndroidUiHost.activity = null
|
||||
AndroidUiHost.launchImagePicker = null
|
||||
}
|
||||
if (isFinishing) {
|
||||
getSharedPreferences(AppUtils.SP_NAME, MODE_PRIVATE).edit(commit = true) {
|
||||
putString("curJpLrc", "")
|
||||
putString("nextJpLrc", "")
|
||||
putBoolean("isPlaying", false)
|
||||
putBoolean("isShutdown", true)
|
||||
}
|
||||
PlatformServices.homeWidget.setShutdown(true)
|
||||
PlatformServices.desktopLyric.hide()
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun handleIncomingIntent(intent: Intent?) {
|
||||
val data = intent?.data?.toString() ?: return
|
||||
when {
|
||||
data.startsWith("homeWidgetExample://") -> {
|
||||
NativeEvents.emitWidgetEvent(data)
|
||||
intent.data = null
|
||||
}
|
||||
data.startsWith("llmp://") || data.startsWith("llmp:") -> {
|
||||
PlatformServices.deepLink.handle(data)
|
||||
intent.data = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestNotifyPermission() {
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
|
||||
!= PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
ActivityCompat.requestPermissions(
|
||||
this,
|
||||
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||
100,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startPlaybackService() {
|
||||
PlaybackService.start(this)
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package top.zhushenwudi.llmp.native
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
|
||||
class WebViewActivity : ComponentActivity() {
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val url = intent.getStringExtra(EXTRA_URL).orEmpty()
|
||||
val title = intent.getStringExtra(EXTRA_TITLE)
|
||||
title?.let { setTitle(it) }
|
||||
|
||||
setContent {
|
||||
AndroidView(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
factory = { context ->
|
||||
WebView(context).apply {
|
||||
settings.javaScriptEnabled = true
|
||||
settings.domStorageEnabled = true
|
||||
webViewClient = WebViewClient()
|
||||
if (url.startsWith("file:///android_asset/")) {
|
||||
loadUrl(url)
|
||||
} else if (url.isNotBlank()) {
|
||||
loadUrl(url)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_URL = "url"
|
||||
const val EXTRA_TITLE = "title"
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package top.zhushenwudi.llmp.native.home_widget.small_home_widget
|
||||
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import top.zhushenwudi.llmp.native.home_widget.HomeWidgetContent
|
||||
|
||||
abstract class SmallHomeWidgetContent(
|
||||
override var radius: Dp = 6.dp,
|
||||
) : HomeWidgetContent()
|
||||
@@ -1,53 +0,0 @@
|
||||
package top.zhushenwudi.llmp.native.platform
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.provider.Settings
|
||||
import top.zhushenwudi.llmp.platform.DesktopLyricController
|
||||
import top.zhushenwudi.llmp.player.PlaybackService
|
||||
import androidx.core.net.toUri
|
||||
|
||||
/**
|
||||
* Desktop lyric controller — overlay is hosted by [PlaybackService] (shared Media3 FGS).
|
||||
*/
|
||||
class AndroidDesktopLyricController(
|
||||
private val context: Context,
|
||||
) : DesktopLyricController {
|
||||
private val appContext = context.applicationContext
|
||||
|
||||
override var enabled: Boolean = false
|
||||
private var playing: Boolean = false
|
||||
|
||||
override fun show() {
|
||||
enabled = true
|
||||
PlaybackService.showDesktopLyric(appContext)
|
||||
}
|
||||
|
||||
override fun hide() {
|
||||
// Keep [enabled] as caller decides (settings off vs app resume).
|
||||
PlaybackService.hideDesktopLyric(appContext)
|
||||
}
|
||||
|
||||
override fun updateLines(line1: String?, line2: String?, currentLine: Int) {
|
||||
PlaybackService.updateDesktopLyric(line1, line2, currentLine)
|
||||
}
|
||||
|
||||
override fun setPlaying(playing: Boolean) {
|
||||
this.playing = playing
|
||||
}
|
||||
|
||||
fun isPlaying(): Boolean = playing
|
||||
|
||||
fun isOverlayShowing(): Boolean = PlaybackService.isDesktopLyricShowing()
|
||||
|
||||
fun requestOverlayPermission(context: Context): Intent? {
|
||||
return if (!Settings.canDrawOverlays(context)) {
|
||||
Intent(
|
||||
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
|
||||
"package:${context.packageName}".toUri(),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
package top.zhushenwudi.llmp.native.platform
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.core.content.edit
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import top.zhushenwudi.llmp.native.home_widget.large_home_widget.BlackLargeHomeWidgetReceiver
|
||||
import top.zhushenwudi.llmp.native.home_widget.large_home_widget.WhiteLargeHomeWidgetReceiver
|
||||
import top.zhushenwudi.llmp.native.home_widget.small_home_widget.BlackSmallHomeWidgetReceiver
|
||||
import top.zhushenwudi.llmp.native.home_widget.small_home_widget.WhiteSmallHomeWidgetReceiver
|
||||
import top.zhushenwudi.llmp.native.util.AppUtils
|
||||
import top.zhushenwudi.llmp.native.util.ImageUtil
|
||||
import top.zhushenwudi.llmp.platform.HomeWidgetController
|
||||
import java.io.File
|
||||
|
||||
class AndroidHomeWidgetController(
|
||||
context: Context,
|
||||
) : HomeWidgetController {
|
||||
private val appContext = context.applicationContext
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
private var preImagePath: String? = null
|
||||
|
||||
private val receiverClasses = listOf(
|
||||
WhiteSmallHomeWidgetReceiver::class.java,
|
||||
BlackSmallHomeWidgetReceiver::class.java,
|
||||
WhiteLargeHomeWidgetReceiver::class.java,
|
||||
BlackLargeHomeWidgetReceiver::class.java,
|
||||
)
|
||||
|
||||
override fun update(
|
||||
songName: String?,
|
||||
artist: String?,
|
||||
isPlaying: Boolean,
|
||||
favorite: Boolean,
|
||||
lyricLine1: String?,
|
||||
lyricLine2: String?,
|
||||
currentLine: Int,
|
||||
coverPath: String?,
|
||||
playText: String,
|
||||
) {
|
||||
appContext.getSharedPreferences(AppUtils.SP_NAME, Context.MODE_PRIVATE).edit(commit = true) {
|
||||
putString("songName", songName ?: "")
|
||||
putString("songArtist", artist ?: "")
|
||||
putBoolean("songFavorite", favorite)
|
||||
putBoolean("isPlaying", isPlaying)
|
||||
putString("playText", playText)
|
||||
putString("lyricLine1", lyricLine1 ?: "")
|
||||
putString("lyricLine2", lyricLine2 ?: "")
|
||||
putInt("currentLine", currentLine)
|
||||
}
|
||||
coverPath?.let { processCover(it) } ?: refresh()
|
||||
}
|
||||
|
||||
override fun setShutdown(shutdown: Boolean) {
|
||||
appContext.getSharedPreferences(AppUtils.SP_NAME, Context.MODE_PRIVATE).edit(commit = true) {
|
||||
putBoolean("isShutdown", shutdown)
|
||||
if (shutdown) {
|
||||
putBoolean("isPlaying", false)
|
||||
}
|
||||
}
|
||||
refresh()
|
||||
}
|
||||
|
||||
override fun refresh() {
|
||||
receiverClasses.forEach { receiverClass ->
|
||||
sendWidgetUpdate(receiverClass)
|
||||
}
|
||||
}
|
||||
|
||||
private fun processCover(coverPath: String) {
|
||||
if (preImagePath == coverPath) {
|
||||
refresh()
|
||||
return
|
||||
}
|
||||
if (!File(coverPath).exists()) {
|
||||
refresh()
|
||||
return
|
||||
}
|
||||
preImagePath = coverPath
|
||||
scope.launch {
|
||||
try {
|
||||
val originBitmap = BitmapFactory.decodeFile(coverPath)
|
||||
val coverBitmap = ImageUtil.squareAndCircularBitmap(originBitmap, 180)
|
||||
val handledCoverPath = ImageUtil.saveBitmapToFile(appContext, coverBitmap)
|
||||
ImageUtil.savePathToSp(appContext, handledCoverPath)
|
||||
val color = ImageUtil.parsePhotoDomainColor(coverBitmap)
|
||||
ImageUtil.saveColorToSp(appContext, color)
|
||||
originBitmap.recycle()
|
||||
coverBitmap.recycle()
|
||||
} catch (_: Exception) {
|
||||
} finally {
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendWidgetUpdate(receiverClass: Class<*>) {
|
||||
val intent = Intent(appContext, receiverClass).apply {
|
||||
action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
|
||||
val ids = AppWidgetManager.getInstance(appContext)
|
||||
.getAppWidgetIds(ComponentName(appContext, receiverClass))
|
||||
putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
|
||||
}
|
||||
appContext.sendBroadcast(intent)
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package top.zhushenwudi.llmp.native.platform
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import top.zhushenwudi.llmp.platform.ShareBridge
|
||||
|
||||
class AndroidShareBridge(
|
||||
private val context: Context,
|
||||
) : ShareBridge {
|
||||
private val appContext = context.applicationContext
|
||||
|
||||
override fun shareText(text: String, title: String?) {
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_TEXT, text)
|
||||
title?.let { putExtra(Intent.EXTRA_TITLE, it) }
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
appContext.startActivity(
|
||||
Intent.createChooser(intent, title ?: "分享").addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||
)
|
||||
}
|
||||
|
||||
override fun shareUrl(url: String, title: String?) {
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_TEXT, url)
|
||||
title?.let { putExtra(Intent.EXTRA_TITLE, it) }
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
appContext.startActivity(
|
||||
Intent.createChooser(intent, title ?: "分享").addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#FF6B9D"
|
||||
android:pathData="M0,0h108v108h-108z"/>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M34,28h12v52h-12zM54,28c14,0 26,10 26,26s-12,26 -26,26v-12c8,0 14,-6 14,-14s-6,-14 -14,-14z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="14dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="14">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="m13.7695312,0h0.7421876c0.6040624,0 1.09375,0.4896875 1.09375,1.09375v11.5820312c0,0.6040625 -0.4896876,1.09375 -1.09375,1.09375h-0.7421876c-0.6040624,0 -1.09375,-0.4896875 -1.09375,-1.09375v-11.5820312c0,-0.6040625 0.4896876,-1.09375 1.09375,-1.09375zM10.4748828,7.542793L2.2267578,13.468574c-0.6307422,0.453145 -1.5094141,0.30918 -1.9625781,-0.321563C0.0924,12.907906 0,12.620916 0,12.326504V1.4075C0,0.630859 0.62959,0.00125 1.40625,0.00125c0.277721,0 0.549225,0.082232 0.780273,0.236328l8.257071,5.507012c0.502539,0.335156 0.638223,1.014258 0.303066,1.516796c-0.072944,0.109373 -0.165009,0.2047 -0.271777,0.281407z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,14 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="14dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="14">
|
||||
<group
|
||||
android:pivotX="8"
|
||||
android:pivotY="7"
|
||||
android:scaleX="-1">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="m13.7695312,0h0.7421876c0.6040624,0 1.09375,0.4896875 1.09375,1.09375v11.5820312c0,0.6040625 -0.4896876,1.09375 -1.09375,1.09375h-0.7421876c-0.6040624,0 -1.09375,-0.4896875 -1.09375,-1.09375v-11.5820312c0,-0.6040625 0.4896876,-1.09375 1.09375,-1.09375zM10.4748828,7.542793L2.2267578,13.468574c-0.6307422,0.453145 -1.5094141,0.30918 -1.9625781,-0.321563C0.0924,12.907906 0,12.620916 0,12.326504V1.4075C0,0.630859 0.62959,0.00125 1.40625,0.00125c0.277721,0 0.549225,0.082232 0.780273,0.236328l8.257071,5.507012c0.502539,0.335156 0.638223,1.014258 0.303066,1.516796c-0.072944,0.109373 -0.165009,0.2047 -0.271777,0.281407z" />
|
||||
</group>
|
||||
</vector>
|
||||
@@ -1,10 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#EEEEEE">
|
||||
<ProgressBar
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center" />
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<ProgressBar
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center" />
|
||||
</FrameLayout>
|
||||
|
||||
@@ -55,7 +55,8 @@
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginStart="25dp"
|
||||
android:background="@drawable/ic_close" />
|
||||
android:background="@drawable/ic_close"
|
||||
android:contentDescription="@string/lyric_close" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ic_pip"
|
||||
@@ -64,7 +65,8 @@
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginEnd="25dp"
|
||||
android:background="@drawable/ic_pip" />
|
||||
android:background="@drawable/ic_pip"
|
||||
android:contentDescription="@string/lyric_pip" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ic_icon"
|
||||
@@ -75,7 +77,8 @@
|
||||
android:layout_alignBottom="@id/ll_lyric"
|
||||
android:layout_marginBottom="5dp"
|
||||
android:layout_marginEnd="25dp"
|
||||
android:background="@drawable/logo" />
|
||||
android:background="@drawable/logo"
|
||||
android:contentDescription="@null" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ic_translate"
|
||||
@@ -85,5 +88,6 @@
|
||||
android:layout_alignBottom="@id/ll_lyric"
|
||||
android:layout_marginBottom="5dp"
|
||||
android:layout_marginEnd="25dp"
|
||||
android:background="@drawable/ic_translate" />
|
||||
android:background="@drawable/ic_translate"
|
||||
android:contentDescription="@string/lyric_translate" />
|
||||
</RelativeLayout>
|
||||
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 44 KiB |
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">LLMP KMP</string>
|
||||
<string name="playback_channel_name">Wird abgespielt</string>
|
||||
<string name="lyric_close">Desktop-Lyrics schließen</string>
|
||||
<string name="lyric_pip">Bild-in-Bild</string>
|
||||
<string name="lyric_translate">Übersetzte Lyrics</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">LLMP KMP</string>
|
||||
<string name="playback_channel_name">Now Playing</string>
|
||||
<string name="lyric_close">Close desktop lyrics</string>
|
||||
<string name="lyric_pip">Picture in Picture</string>
|
||||
<string name="lyric_translate">Translated lyrics</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">LLMP KMP</string>
|
||||
<string name="playback_channel_name">再生中</string>
|
||||
<string name="lyric_close">デスクトップ歌詞を閉じる</string>
|
||||
<string name="lyric_pip">ピクチャインピクチャ</string>
|
||||
<string name="lyric_translate">翻訳歌詞</string>
|
||||
</resources>
|
||||
@@ -2,4 +2,7 @@
|
||||
<resources>
|
||||
<string name="app_name">LLMP KMP</string>
|
||||
<string name="playback_channel_name">正在播放</string>
|
||||
<string name="lyric_close">关闭桌面歌词</string>
|
||||
<string name="lyric_pip">画中画</string>
|
||||
<string name="lyric_translate">翻译歌词</string>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:initialLayout="@layout/glance_default_loading_layout"
|
||||
android:minWidth="325dp"
|
||||
android:minHeight="100dp"
|
||||
android:targetCellWidth="5"
|
||||
android:targetCellHeight="2"
|
||||
android:previewImage="@mipmap/ic_widget_black_large"
|
||||
android:updatePeriodMillis="10000"
|
||||
android:resizeMode="none"
|
||||
android:widgetCategory="home_screen" />
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:initialLayout="@layout/glance_default_loading_layout"
|
||||
android:minWidth="170dp"
|
||||
android:minHeight="100dp"
|
||||
android:targetCellWidth="3"
|
||||
android:targetCellHeight="2"
|
||||
android:previewImage="@mipmap/ic_widget_black_small"
|
||||
android:updatePeriodMillis="10000"
|
||||
android:resizeMode="none"
|
||||
android:widgetCategory="home_screen" />
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:initialLayout="@layout/glance_default_loading_layout"
|
||||
android:minWidth="325dp"
|
||||
android:minHeight="100dp"
|
||||
android:targetCellWidth="5"
|
||||
android:targetCellHeight="2"
|
||||
android:previewImage="@mipmap/ic_widget_white_large"
|
||||
android:updatePeriodMillis="10000"
|
||||
android:resizeMode="none"
|
||||
android:widgetCategory="home_screen" />
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:initialLayout="@layout/glance_default_loading_layout"
|
||||
android:minWidth="170dp"
|
||||
android:minHeight="100dp"
|
||||
android:targetCellWidth="3"
|
||||
android:targetCellHeight="2"
|
||||
android:previewImage="@mipmap/ic_widget_white_small"
|
||||
android:updatePeriodMillis="10000"
|
||||
android:resizeMode="none"
|
||||
android:widgetCategory="home_screen" />
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<cache-path name="cache" path="." />
|
||||
<files-path name="files" path="." />
|
||||
<external-files-path name="ext" path="." />
|
||||
</paths>
|
||||