function adapt

This commit is contained in:
2026-08-30 19:36:25 +08:00
parent bea5ead33d
commit 560f3cbac3
450 changed files with 21193 additions and 8334 deletions
+1
View File
@@ -4,6 +4,7 @@
**/build/ **/build/
xcuserdata xcuserdata
local.properties local.properties
key.properties
.idea .idea
.DS_Store .DS_Store
captures captures
+2 -2
View File
@@ -2,7 +2,7 @@
与 Flutter 仓库 `LoveLiveMusicPlayerMobile` **并列**。UI 使用 Compose Multiplatform(Android / iOS 一致);PC 仍为 Electron,本仓库提供协议客户端与共享模型。 与 Flutter 仓库 `LoveLiveMusicPlayerMobile` **并列**。UI 使用 Compose Multiplatform(Android / iOS 一致);PC 仍为 Electron,本仓库提供协议客户端与共享模型。
包名:`top.zhushenwudi.llmp` 包名:`com.zhushenwudi.lovelivemusicplayer`
## Maven / Nexus ## Maven / Nexus
@@ -51,7 +51,7 @@ Debug APK:`composeApp/build/outputs/apk/debug/composeApp-debug.apk`
### 仍需 macOS / Apple 开发者完成的项 ### 仍需 macOS / Apple 开发者完成的项
1. **Xcode 工程**:创建 App + Widget Extension,链接 `ComposeApp.framework`(`./gradlew :composeApp:linkDebugFrameworkIosSimulatorArm64`) 1. **Xcode 工程**:创建 App + Widget Extension,链接 `ComposeApp.framework`(`./gradlew :composeApp:linkDebugFrameworkIosSimulatorArm64`)
2. **签名 / App Group**:`group.top.zhushenwudi.llmp`(已从 Flutter `group.com.zhushenwudi.lovelivemusicplayer` 迁移;见 `iosApp/README.md`) 2. **签名 / App Group**:与 Flutter 一致 — `group.com.zhushenwudi.lovelivemusicplayer`(Bundle ID `com.zhushenwudi.lovelivemusicplayer`;见 `iosApp/README.md`)
3. **CarPlay entitlement**:`com.apple.developer.carplay-audio` + `playable-content` 需 Apple 审批后真机可用 3. **CarPlay entitlement**:`com.apple.developer.carplay-audio` + `playable-content` 需 Apple 审批后真机可用
4. **PiP 资源**:`black.mp4`(Flutter 仓库若缺失需自行放入 `iosApp/iosApp/pip/`) 4. **PiP 资源**:`black.mp4`(Flutter 仓库若缺失需自行放入 `iosApp/iosApp/pip/`)
5. **Swift ↔ Kotlin 播放回调**:Widget/CarPlay 的 NotificationCenter 事件需在 Mac 上接到 `AppContainer.player` 5. **Swift ↔ Kotlin 播放回调**:Widget/CarPlay 的 NotificationCenter 事件需在 Mac 上接到 `AppContainer.player`
+109 -8
View File
@@ -1,4 +1,5 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import java.util.Properties
plugins { plugins {
alias(libs.plugins.androidApplication) alias(libs.plugins.androidApplication)
@@ -9,31 +10,108 @@ plugins {
kotlin { kotlin {
compilerOptions { compilerOptions {
jvmTarget.set(JvmTarget.JVM_11) 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 { android {
namespace = "top.zhushenwudi.llmp" namespace = "com.zhushenwudi.lovelivemusicplayer"
compileSdk = libs.versions.android.compileSdk.get().toInt() compileSdk = libs.versions.android.compileSdk.get().toInt()
defaultConfig { defaultConfig {
applicationId = "top.zhushenwudi.llmp" applicationId = "com.zhushenwudi.lovelivemusicplayer"
minSdk = libs.versions.android.minSdk.get().toInt() minSdk = libs.versions.android.minSdk.get().toInt()
targetSdk = libs.versions.android.targetSdk.get().toInt() targetSdk = libs.versions.android.targetSdk.get().toInt()
versionCode = 1 versionCode = appVersionCode
versionName = "0.2.0-phaseb" 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 { packaging {
resources { resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}" excludes += "/META-INF/{AL2.0,LGPL2.1}"
} }
dex {
useLegacyPackaging = true
}
jniLibs {
useLegacyPackaging = true
}
} }
buildTypes { lint {
getByName("release") { abortOnError = false
isMinifyEnabled = false checkReleaseBuilds = false
} disable += "InvalidPackage"
} }
compileOptions { 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 { dependencies {
implementation(projects.composeApp) implementation(projects.composeApp)
implementation(projects.shared) implementation(projects.shared)
@@ -57,10 +156,12 @@ dependencies {
implementation(libs.zxing.android.embedded) implementation(libs.zxing.android.embedded)
implementation(libs.androidx.lifecycle.service) implementation(libs.androidx.lifecycle.service)
implementation(libs.ktor.client.okhttp) implementation(libs.ktor.client.okhttp)
implementation(files("libs/open_sdk_3.5.19_r9483ffc7_lite.jar"))
implementation(libs.compose.runtime) implementation(libs.compose.runtime)
implementation(libs.compose.foundation) implementation(libs.compose.foundation)
implementation(libs.compose.material3) implementation(libs.compose.material3)
implementation(libs.compose.material.icons.extended)
implementation(libs.compose.ui) implementation(libs.compose.ui)
implementation(libs.compose.ui.tooling.preview) implementation(libs.compose.ui.tooling.preview)
+7
View File
@@ -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
Binary file not shown.
+39
View File
@@ -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.**
Binary file not shown.
Binary file not shown.
+40 -2
View File
@@ -16,9 +16,16 @@
<uses-feature android:name="android.hardware.usb.host" android:required="false" /> <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 <application
android:allowBackup="true" android:allowBackup="true"
android:icon="@drawable/ic_launcher" android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="LLMP KMP" android:label="LLMP KMP"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@android:style/Theme.Material.Light.NoActionBar" android:theme="@android:style/Theme.Material.Light.NoActionBar"
@@ -29,7 +36,8 @@
android:exported="true" android:exported="true"
android:launchMode="singleTop" android:launchMode="singleTop"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden" android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize"
android:enableOnBackInvokedCallback="false">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
@@ -59,9 +67,39 @@
android:configChanges="orientation|screenSize|keyboardHidden" android:configChanges="orientation|screenSize|keyboardHidden"
android:theme="@android:style/Theme.Material.Light.NoActionBar" /> 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 <service
android:name=".player.PlaybackService" android:name=".player.PlaybackService"
android:exported="true" android:exported="true"
android:permission="android.permission.BIND_MEDIA_BROWSER_SERVICE"
android:foregroundServiceType="mediaPlayback"> android:foregroundServiceType="mediaPlayback">
<intent-filter> <intent-filter>
<action android:name="androidx.media3.session.MediaSessionService" /> <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.annotation.SuppressLint
import android.content.Context import android.content.Context
@@ -18,21 +18,23 @@ import android.widget.LinearLayout
import android.widget.RelativeLayout import android.widget.RelativeLayout
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import java.lang.ref.WeakReference
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import top.zhushenwudi.llmp.MainActivity import com.zhushenwudi.lovelivemusicplayer.AppContainer
import top.zhushenwudi.llmp.R 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.math.abs
import kotlin.time.Duration.Companion.seconds 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( class DesktopLyricOverlay(
private val context: Context, private val context: Context,
@@ -40,18 +42,20 @@ class DesktopLyricOverlay(
private val onRequestHide: () -> Unit, private val onRequestHide: () -> Unit,
) { ) {
companion object { companion object {
private const val MSG_HINT = "正在打开应用..."
private val mainHandler = Handler(Looper.getMainLooper()) private val mainHandler = Handler(Looper.getMainLooper())
private var activeRef: WeakReference<DesktopLyricOverlay>? = null private var activeRef: WeakReference<DesktopLyricOverlay>? = null
@Volatile private var cachedLine1: String? = null @Volatile
@Volatile private var cachedLine2: String? = null private var cachedLine1: String? = null
@Volatile private var cachedCurrentLine: Int = -1 @Volatile
private var cachedLine2: String? = null
@Volatile
private var cachedCurrentLine: Int = -1
fun updateLyric(lyricLine1: String?, lyricLine2: String?, currentLine: Int) { fun updateLyric(lyricLine1: String?, lyricLine2: String?, currentLine: Int) {
cachedLine1 = lyricLine1 if (lyricLine1 != null) cachedLine1 = lyricLine1
cachedLine2 = lyricLine2 if (lyricLine2 != null) cachedLine2 = lyricLine2
cachedCurrentLine = currentLine cachedCurrentLine = currentLine
val overlay = activeRef?.get() ?: return val overlay = activeRef?.get() ?: return
if (Looper.myLooper() == Looper.getMainLooper()) { if (Looper.myLooper() == Looper.getMainLooper()) {
@@ -87,6 +91,7 @@ class DesktopLyricOverlay(
fun attach() { fun attach() {
if (root != null) return if (root != null) return
createView() createView()
if (root == null) return
bindView() bindView()
activeRef = WeakReference(this) activeRef = WeakReference(this)
startChromeTimer() startChromeTimer()
@@ -134,7 +139,12 @@ class DesktopLyricOverlay(
} }
windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
root = LayoutInflater.from(context).inflate(R.layout.view_lyric, null) as RelativeLayout 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") @SuppressLint("ClickableViewAccessibility")
@@ -178,6 +188,7 @@ class DesktopLyricOverlay(
sx = mRawX sx = mRawX
sy = mRawY sy = mRawY
} }
MotionEvent.ACTION_MOVE -> { MotionEvent.ACTION_MOVE -> {
isMove = true isMove = true
val params = wmParams ?: return@setOnTouchListener true val params = wmParams ?: return@setOnTouchListener true
@@ -187,6 +198,7 @@ class DesktopLyricOverlay(
sx = mRawX sx = mRawX
sy = mRawY sy = mRawY
} }
MotionEvent.ACTION_UP -> { MotionEvent.ACTION_UP -> {
val elapsed = System.currentTimeMillis() - mLastTime val elapsed = System.currentTimeMillis() - mLastTime
val mStopX = event.rawX.toInt() val mStopX = event.rawX.toInt()
@@ -206,11 +218,11 @@ class DesktopLyricOverlay(
flags = Intent.FLAG_ACTIVITY_NEW_TASK flags = Intent.FLAG_ACTIVITY_NEW_TASK
} }
context.startActivity(intent) context.startActivity(intent)
Toast.makeText(context, MSG_HINT, Toast.LENGTH_LONG).show() Toast.makeText(context, I18n.t("opening_app"), Toast.LENGTH_LONG).show()
onRequestHide() onRequestHide()
} }
ivTranslate?.setOnClickListener { ivTranslate?.setOnClickListener {
NativeEvents.lyricTypeClick() AppContainer.cycleDesktopLyricType()
} }
} }
@@ -218,13 +230,13 @@ class DesktopLyricOverlay(
val lyricLine1 = cachedLine1 val lyricLine1 = cachedLine1
val lyricLine2 = cachedLine2 val lyricLine2 = cachedLine2
val currentLine = cachedCurrentLine val currentLine = cachedCurrentLine
tvLyricLine1?.text = lyricLine1.orEmpty() lyricLine1?.let { tvLyricLine1?.text = it }
tvLyricLine2?.text = lyricLine2.orEmpty() lyricLine2?.let { tvLyricLine2?.text = it }
tvLyricLine1?.setTextColor(if (currentLine == 2) Color.LTGRAY else Color.WHITE) tvLyricLine1?.setTextColor(if (currentLine == 2) Color.LTGRAY else Color.WHITE)
tvLyricLine2?.setTextColor(if (currentLine == 1) Color.LTGRAY else Color.WHITE) tvLyricLine2?.setTextColor(if (currentLine == 1) Color.LTGRAY else Color.WHITE)
if (currentLine == -1) { if (currentLine == -1) {
tvNoLyric?.text = lyricLine1 ?: "暂无歌词" tvNoLyric?.text = lyricLine1 ?: I18n.t("no_lyrics")
tvLyricLine1?.visibility = View.GONE tvLyricLine1?.visibility = View.GONE
tvLyricLine2?.visibility = View.GONE tvLyricLine2?.visibility = View.GONE
tvNoLyric?.visibility = View.VISIBLE 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.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import top.zhushenwudi.llmp.platform.DeepLinkPayload import com.zhushenwudi.lovelivemusicplayer.platform.DeepLinkPayload
object NativeEvents { object NativeEvents {
private val _widgetEvents = MutableSharedFlow<String>(extraBufferCapacity = 8) 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 android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import com.journeyapps.barcodescanner.ScanContract import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions 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() { class QrScanActivity : ComponentActivity() {
private val barcodeLauncher = registerForActivityResult(ScanContract()) { result -> private val barcodeLauncher = registerForActivityResult(ScanContract()) { result ->
@@ -23,7 +24,7 @@ class QrScanActivity : ComponentActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val options = ScanOptions().apply { val options = ScanOptions().apply {
setDesiredBarcodeFormats(ScanOptions.QR_CODE) setDesiredBarcodeFormats(ScanOptions.QR_CODE)
setPrompt("扫描二维码") setPrompt(I18n.t("scan_qr_prompt"))
setBeepEnabled(false) setBeepEnabled(false)
setOrientationLocked(false) setOrientationLocked(false)
setCaptureActivity(com.journeyapps.barcodescanner.CaptureActivity::class.java) 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 android.content.Context
import top.zhushenwudi.llmp.platform.AnalyticsHook import com.zhushenwudi.lovelivemusicplayer.platform.AnalyticsHook
object UmengBridge : AnalyticsHook { object UmengBridge : AnalyticsHook {
private const val UMENG_KEY = "634bd9c688ccdf4b7e4ac67b" 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,11 +1,15 @@
package top.zhushenwudi.llmp.native.home_widget package com.zhushenwudi.lovelivemusicplayer.native.home_widget
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent 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.content.SharedPreferences
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.net.Uri
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
@@ -18,17 +22,16 @@ import androidx.glance.GlanceId
import androidx.glance.GlanceModifier import androidx.glance.GlanceModifier
import androidx.glance.Image import androidx.glance.Image
import androidx.glance.ImageProvider import androidx.glance.ImageProvider
import androidx.glance.LocalContext
import androidx.glance.action.Action
import androidx.glance.action.clickable import androidx.glance.action.clickable
import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.action.actionStartActivity
import androidx.glance.appwidget.cornerRadius import androidx.glance.appwidget.cornerRadius
import androidx.glance.appwidget.provideContent import androidx.glance.appwidget.provideContent
import androidx.glance.background import androidx.glance.background
import androidx.glance.layout.Alignment import androidx.glance.layout.Alignment
import androidx.glance.layout.Box import androidx.glance.layout.Box
import androidx.glance.layout.Column import androidx.glance.layout.Column
import androidx.glance.layout.Row
import androidx.glance.layout.Spacer
import androidx.glance.layout.fillMaxSize import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.height import androidx.glance.layout.height
import androidx.glance.layout.padding import androidx.glance.layout.padding
@@ -38,10 +41,9 @@ import androidx.glance.text.FontWeight
import androidx.glance.text.Text import androidx.glance.text.Text
import androidx.glance.text.TextStyle import androidx.glance.text.TextStyle
import androidx.glance.unit.ColorProvider import androidx.glance.unit.ColorProvider
import top.zhushenwudi.llmp.MainActivity import com.zhushenwudi.lovelivemusicplayer.R
import top.zhushenwudi.llmp.R import com.zhushenwudi.lovelivemusicplayer.native.util.AppUtils
import top.zhushenwudi.llmp.native.util.AppUtils import com.zhushenwudi.lovelivemusicplayer.native.util.ImageUtil
import top.zhushenwudi.llmp.native.util.ImageUtil
import java.io.File import java.io.File
abstract class HomeWidgetContent : GlanceAppWidget() { abstract class HomeWidgetContent : GlanceAppWidget() {
@@ -56,19 +58,15 @@ abstract class HomeWidgetContent : GlanceAppWidget() {
private lateinit var songName: String private lateinit var songName: String
private lateinit var songArtist: String private lateinit var songArtist: String
private lateinit var playText: 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 musicColor = Color.White
private var coverPath: String? = null private var coverPath: String? = null
private var line1Color = Color.White
private var line2Color = Color.White
@Composable
private fun widgetAction(context: Context, uri: String): Action { private fun widgetAction(context: Context, uri: String): Action {
val intent = Intent(context, MainActivity::class.java).apply { val intent = Intent(context, MainActivity::class.java).apply {
action = Intent.ACTION_VIEW action = Intent.ACTION_VIEW
data = Uri.parse(uri) data = uri.toUri()
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP
} }
return actionStartActivity(intent) return actionStartActivity(intent)
@@ -77,17 +75,14 @@ abstract class HomeWidgetContent : GlanceAppWidget() {
@Composable @Composable
fun GlanceContent(preferences: SharedPreferences) { fun GlanceContent(preferences: SharedPreferences) {
sp = preferences sp = preferences
songName = sp.getString("songName", "") ?: "" songName = sp.getString("songName", "")!!
songArtist = sp.getString("songArtist", "") ?: "" songArtist = sp.getString("songArtist", "")!!
isFavorite = sp.getBoolean("songFavorite", false) isFavorite = sp.getBoolean("songFavorite", false)
isPlaying = sp.getBoolean("isPlaying", false) isPlaying = sp.getBoolean("isPlaying", false)
playText = sp.getString("playText", "") ?: "" 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) isShutdown = sp.getBoolean("isShutdown", false)
coverPath = sp.getString("shareImage", null) coverPath = sp.getString("shareImage", null)
val strBgColor = sp.getString("bgColor", "") ?: "" val strBgColor = sp.getString("bgColor", "")!!
coverPath?.let { coverPath?.let {
if (!File(it).exists()) { if (!File(it).exists()) {
@@ -98,9 +93,9 @@ abstract class HomeWidgetContent : GlanceAppWidget() {
if (strBgColor.contains(",")) { if (strBgColor.contains(",")) {
val bgColorArr = strBgColor.split(",") val bgColorArr = strBgColor.split(",")
musicColor = Color( musicColor = Color(
red = bgColorArr.getOrNull(0)?.toFloatOrNull() ?: 1f, red = bgColorArr[0].toDoubleOrNull()?.toInt() ?: 1,
green = bgColorArr.getOrNull(1)?.toFloatOrNull() ?: 1f, green = bgColorArr[1].toDoubleOrNull()?.toInt() ?: 1,
blue = bgColorArr.getOrNull(2)?.toFloatOrNull() ?: 1f, blue = bgColorArr[2].toDoubleOrNull()?.toInt() ?: 1
) )
} }
@@ -108,16 +103,6 @@ abstract class HomeWidgetContent : GlanceAppWidget() {
isPlaying = false 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() RenderAppWidget()
} }
@@ -126,7 +111,7 @@ abstract class HomeWidgetContent : GlanceAppWidget() {
Box( Box(
modifier = GlanceModifier modifier = GlanceModifier
.width(size.width) .width(size.width)
.height(size.height), .height(size.height)
) { ) {
if (isWhite) { if (isWhite) {
GradientRectangle(color = musicColor) GradientRectangle(color = musicColor)
@@ -136,7 +121,7 @@ abstract class HomeWidgetContent : GlanceAppWidget() {
.background(Color(red = 49, green = 49, blue = 49)) .background(Color(red = 49, green = 49, blue = 49))
.width(size.width) .width(size.width)
.height(size.height) .height(size.height)
.cornerRadius(radius), .cornerRadius(radius)
) {} ) {}
} }
@@ -145,13 +130,13 @@ abstract class HomeWidgetContent : GlanceAppWidget() {
modifier = GlanceModifier modifier = GlanceModifier
.width(size.width) .width(size.width)
.height(size.height) .height(size.height)
.cornerRadius(radius), .cornerRadius(radius)
) { ) {
Box( Box(
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
modifier = GlanceModifier modifier = GlanceModifier
.size(size.height) .size(size.height)
.cornerRadius(radius), .cornerRadius(radius)
) { ) {
RenderCD(isLargePauseWidget = false) RenderCD(isLargePauseWidget = false)
RenderCover(isLargePauseWidget = false) RenderCover(isLargePauseWidget = false)
@@ -163,17 +148,12 @@ abstract class HomeWidgetContent : GlanceAppWidget() {
modifier = GlanceModifier modifier = GlanceModifier
.width(size.width) .width(size.width)
.height(size.height) .height(size.height)
.padding(radius), .padding(radius)
) { ) {
Box( Box(
modifier = GlanceModifier.defaultWeight(), modifier = GlanceModifier.defaultWeight()
) { ) {
Column {
RenderLogo(size = 22.dp, paddingInDp = (-6).dp) RenderLogo(size = 22.dp, paddingInDp = (-6).dp)
if (size.width == AppUtils.HORIZONTAL_RECTANGLE.width) {
RenderLyric(isLargePauseWidget = false)
}
}
} }
RenderTextArr(stateFontSize = 9.sp, infoFontSize = 11.sp) RenderTextArr(stateFontSize = 9.sp, infoFontSize = 11.sp)
@@ -191,20 +171,27 @@ abstract class HomeWidgetContent : GlanceAppWidget() {
@Composable @Composable
open fun RenderTextArr( open fun RenderTextArr(
stateFontSize: TextUnit, stateFontSize: TextUnit,
infoFontSize: 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(",") val playTextArr = playText.split(",")
var playStateText = playTextArr.getOrElse(1) { "" } var playStateText = playTextArr[1]
if (!isShutdown && isPlaying) { if (!isShutdown && isPlaying) {
playStateText = playTextArr.getOrElse(0) { playStateText } playStateText = playTextArr[0]
} }
Text( Text(
text = playStateText, text = playStateText,
style = TextStyle( style = TextStyle(
fontSize = stateFontSize, fontSize = stateFontSize,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = ColorProvider(Color.Gray), color = ColorProvider(Color.Gray)
), ),
maxLines = 1,
modifier = GlanceModifier.width(textWidth),
) )
Text( Text(
@@ -212,8 +199,10 @@ abstract class HomeWidgetContent : GlanceAppWidget() {
style = TextStyle( style = TextStyle(
fontSize = infoFontSize, fontSize = infoFontSize,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = ColorProvider(if (isWhite) Color.Black else Color.White), color = ColorProvider(if (isWhite) Color.Black else Color.White)
), ),
maxLines = 1,
modifier = GlanceModifier.width(textWidth),
) )
Text( Text(
@@ -221,8 +210,10 @@ abstract class HomeWidgetContent : GlanceAppWidget() {
style = TextStyle( style = TextStyle(
fontSize = infoFontSize, fontSize = infoFontSize,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = ColorProvider(Color.Gray), color = ColorProvider(Color.Gray)
), ),
maxLines = 1,
modifier = GlanceModifier.width(textWidth),
) )
} }
@@ -233,44 +224,46 @@ abstract class HomeWidgetContent : GlanceAppWidget() {
contentDescription = "logo", contentDescription = "logo",
modifier = GlanceModifier modifier = GlanceModifier
.size(size) .size(size)
.padding(start = paddingInDp), .padding(start = paddingInDp)
) )
} }
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
@Composable @Composable
open fun RenderLyric(isLargePauseWidget: Boolean) { private fun RenderTransportButton(
val basicWidth = size.width drawable: Int,
val width = if (isLargePauseWidget) basicWidth - 150.dp else basicWidth - 60.dp description: String,
Text( uri: String,
text = lyricLine1, imageSize: Dp,
style = TextStyle( ) {
fontSize = 14.sp, val base = GlanceModifier.size(imageSize)
fontWeight = FontWeight.Bold, val modifier = if (isShutdown) {
color = ColorProvider(line1Color), base
), } else {
maxLines = 1, base.clickable(
modifier = GlanceModifier.width(width), rippleOverride = R.drawable.click_ripple,
onClick = widgetAction(LocalContext.current, uri),
) )
}
Text( Box(
text = lyricLine2, contentAlignment = Alignment.Center,
style = TextStyle( modifier = modifier,
fontSize = 14.sp, ) {
fontWeight = FontWeight.Bold, Image(
color = ColorProvider(line2Color), provider = ImageProvider(drawable),
), contentDescription = description,
maxLines = 1, colorFilter = ColorFilter.tint(ColorProvider(Color.White)),
modifier = GlanceModifier.width(width), modifier = GlanceModifier.size(imageSize * TRANSPORT_ICON_SCALE),
) )
} }
}
@Composable @Composable
open fun RenderCD(isLargePauseWidget: Boolean) { open fun RenderCD(isLargePauseWidget: Boolean) {
val context = LocalContext.current
val cdDrawable = if (isWhite) R.drawable.cd_white else R.drawable.cd_black val cdDrawable = if (isWhite) R.drawable.cd_white else R.drawable.cd_black
val offsetX = calcCdOffsetX() val offsetX = calcCdOffsetX()
val offsetY = calcCdOffsetY() val offsetY = calcCdOffsetY()
val startPadding = if (isLargePauseWidget) 0.dp else offsetX val startPadding = if (isLargePauseWidget) 0.dp else offsetX
val radio = if (isLargePauseWidget) 0.9f else 1f val radio = if (isLargePauseWidget) 0.9f else 1f
if (isShutdown) { if (isShutdown) {
@@ -283,8 +276,8 @@ abstract class HomeWidgetContent : GlanceAppWidget() {
start = startPadding, start = startPadding,
top = -offsetY, top = -offsetY,
end = -offsetX, end = -offsetX,
bottom = offsetY, bottom = offsetY
), )
) )
} else { } else {
Image( Image(
@@ -296,12 +289,9 @@ abstract class HomeWidgetContent : GlanceAppWidget() {
start = startPadding, start = startPadding,
top = -offsetY, top = -offsetY,
end = -offsetX, end = -offsetX,
bottom = offsetY, bottom = offsetY
) )
.clickable( .clickable(rippleOverride = R.drawable.click_ripple, onClick = widgetAction(LocalContext.current, "${AppUtils.HOME_WIDGET_URI_PREFIX}toggle_play"))
rippleOverride = R.drawable.click_ripple,
onClick = widgetAction(context, "homeWidgetExample://toggle_play"),
),
) )
} }
} }
@@ -326,8 +316,8 @@ abstract class HomeWidgetContent : GlanceAppWidget() {
start = startPadding, start = startPadding,
top = -offsetY + coverAndCdDiffSize, top = -offsetY + coverAndCdDiffSize,
bottom = offsetY + coverAndCdDiffSize, bottom = offsetY + coverAndCdDiffSize,
end = -offsetX + coverAndCdDiffSize, end = -offsetX + coverAndCdDiffSize
), )
) )
} }
} }
@@ -337,63 +327,100 @@ abstract class HomeWidgetContent : GlanceAppWidget() {
@Composable @Composable
open fun RenderPlayController(isLargePauseWidget: Boolean) { open fun RenderPlayController(isLargePauseWidget: Boolean) {
val resId = if (isPlaying) R.drawable.vector_pause else R.drawable.vector_play 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) { if (isLargePauseWidget) {
val modifier = if (isShutdown) {
GlanceModifier.size(20.dp)
} else {
GlanceModifier
.size(20.dp)
.clickable(rippleOverride = R.drawable.click_ripple, onClick = toggleAction)
}
Image( Image(
provider = ImageProvider(resId), provider = ImageProvider(resId),
contentDescription = "playButton", contentDescription = "playButton",
colorFilter = ColorFilter.tint(colorProvider = ColorProvider(Color.White)), colorFilter = ColorFilter.tint(colorProvider = ColorProvider(Color.White)),
modifier = GlanceModifier.size(20.dp), modifier = modifier,
) )
} else { } else {
val padding = val padding =
if (size.width == AppUtils.HORIZONTAL_RECTANGLE.width) 26.dp else 23.dp 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( Box(
contentAlignment = Alignment.TopEnd, contentAlignment = Alignment.TopEnd,
modifier = GlanceModifier modifier = GlanceModifier
.fillMaxSize() .fillMaxSize()
.padding(top = padding, end = padding), .padding(top = padding, end = padding)
) { ) {
Image( Image(
provider = ImageProvider(resId), provider = ImageProvider(resId),
contentDescription = "playButton", contentDescription = "playButton",
colorFilter = ColorFilter.tint(colorProvider = ColorProvider(Color.White)), colorFilter = ColorFilter.tint(colorProvider = ColorProvider(Color.White)),
modifier = GlanceModifier.size(15.dp), modifier = playModifier,
) )
} }
} }
} }
@Composable @Composable
open fun RenderFavorite(imageSize: Dp, paddingInDp: Dp) { open fun RenderFavorite(imageSize: Dp, paddingInDp: Dp) {
val context = LocalContext.current val isLarge = size.width == AppUtils.HORIZONTAL_RECTANGLE.width
Box( Box(
contentAlignment = Alignment.BottomEnd, contentAlignment = Alignment.BottomEnd,
modifier = GlanceModifier modifier = GlanceModifier
.width(size.width) .width(size.width)
.height(size.height) .height(size.height)
.padding(paddingInDp), .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 = val loveIcon =
if (isFavorite) R.drawable.widget_fav_click else R.drawable.widget_fav_unclick if (isFavorite) R.drawable.widget_fav_click else R.drawable.widget_fav_unclick
if (isShutdown) { val favoriteModifier = if (isShutdown) {
Image( GlanceModifier.size(imageSize)
provider = ImageProvider(loveIcon),
contentDescription = "love",
modifier = GlanceModifier.size(imageSize),
)
} else { } else {
Image( GlanceModifier
provider = ImageProvider(loveIcon),
contentDescription = "love",
modifier = GlanceModifier
.size(imageSize) .size(imageSize)
.clickable( .clickable(
rippleOverride = R.drawable.click_ripple, rippleOverride = R.drawable.click_ripple,
onClick = widgetAction(context, "homeWidgetExample://toggle_love"), onClick = widgetAction(
LocalContext.current,
"${AppUtils.HOME_WIDGET_URI_PREFIX}toggle_love",
), ),
) )
} }
Image(
provider = ImageProvider(loveIcon),
contentDescription = "love",
modifier = favoriteModifier,
)
}
} }
} }
@@ -405,22 +432,24 @@ abstract class HomeWidgetContent : GlanceAppWidget() {
modifier = GlanceModifier modifier = GlanceModifier
.width(size.width) .width(size.width)
.height(size.height) .height(size.height)
.cornerRadius(radius), .cornerRadius(radius)
) { ) {
Image( Image(
provider = ImageProvider(bitmap), provider = ImageProvider(bitmap),
contentDescription = "bg", contentDescription = "bg",
modifier = GlanceModifier modifier = GlanceModifier
.width(size.width) .width(size.width)
.height(size.height), .height(size.height)
) )
} }
} }
override suspend fun provideGlance(context: Context, id: GlanceId) { override suspend fun provideGlance(context: Context, id: GlanceId) {
val preferences = context.getSharedPreferences(AppUtils.SP_NAME, Context.MODE_PRIVATE) val preferences = context.getSharedPreferences(AppUtils.SP_NAME, Context.MODE_PRIVATE)
provideContent { provideContent { GlanceContent(preferences = preferences) }
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 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.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver import androidx.glance.appwidget.GlanceAppWidgetReceiver
@@ -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 android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -7,8 +7,8 @@ import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import top.zhushenwudi.llmp.native.home_widget.HomeWidgetContent import com.zhushenwudi.lovelivemusicplayer.native.home_widget.HomeWidgetContent
import top.zhushenwudi.llmp.native.util.AppUtils import com.zhushenwudi.lovelivemusicplayer.native.util.AppUtils
@SuppressLint("CommitPrefEdits", "DefaultLocale") @SuppressLint("CommitPrefEdits", "DefaultLocale")
abstract class LargeHomeWidgetContent( abstract class LargeHomeWidgetContent(
@@ -17,7 +17,9 @@ abstract class LargeHomeWidgetContent(
override var cdSize: Dp = 120.dp, override var cdSize: Dp = 120.dp,
) : HomeWidgetContent() { ) : HomeWidgetContent() {
private fun isLargePauseWidget(): Boolean = !isPlaying || isShutdown private fun isLargePauseWidget(): Boolean {
return !isPlaying || isShutdown
}
@Composable @Composable
override fun RenderLogo(size: Dp, paddingInDp: Dp) { override fun RenderLogo(size: Dp, paddingInDp: Dp) {
@@ -29,11 +31,6 @@ abstract class LargeHomeWidgetContent(
super.RenderCD(isLargePauseWidget = isLargePauseWidget()) super.RenderCD(isLargePauseWidget = isLargePauseWidget())
} }
@Composable
override fun RenderLyric(isLargePauseWidget: Boolean) {
super.RenderLyric(isLargePauseWidget = isLargePauseWidget())
}
@Composable @Composable
override fun RenderCover(isLargePauseWidget: Boolean) { override fun RenderCover(isLargePauseWidget: Boolean) {
super.RenderCover(isLargePauseWidget = isLargePauseWidget()) super.RenderCover(isLargePauseWidget = isLargePauseWidget())
@@ -54,7 +51,9 @@ abstract class LargeHomeWidgetContent(
super.RenderFavorite(imageSize = 28.dp, paddingInDp = radius / 2) super.RenderFavorite(imageSize = 28.dp, paddingInDp = radius / 2)
} }
override fun calcCdOffsetX(): Dp = if (isPlaying) (cdSize / 2 - 10.dp) else 0.dp override fun calcCdOffsetX(): Dp =
if (isPlaying) cdSize / 2 + 5.dp else 12.dp
override fun calcCdOffsetY(): Dp = if (isPlaying) (cdSize / 2 - 10.dp) else 0.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 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.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver 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 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.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver 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 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.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver 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 com.zhushenwudi.lovelivemusicplayer.native.NativeEvents
import top.zhushenwudi.llmp.platform.DeepLinkHandler import com.zhushenwudi.lovelivemusicplayer.platform.DeepLinkHandler
import top.zhushenwudi.llmp.platform.DeepLinkParser import com.zhushenwudi.lovelivemusicplayer.platform.DeepLinkParser
import top.zhushenwudi.llmp.platform.DeepLinkPayload import com.zhushenwudi.lovelivemusicplayer.platform.DeepLinkPayload
class AndroidDeepLinkHandler( class AndroidDeepLinkHandler(
private val onDeepLink: ((DeepLinkPayload) -> Unit)? = null, 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.BroadcastReceiver
import android.content.Context import android.content.Context
@@ -8,8 +8,8 @@ import android.os.Build
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import top.zhushenwudi.llmp.platform.FileSystem import com.zhushenwudi.lovelivemusicplayer.platform.FileSystem
import top.zhushenwudi.llmp.platform.UsbMountMonitor import com.zhushenwudi.lovelivemusicplayer.platform.UsbMountMonitor
enum class UsbMountEvent { enum class UsbMountEvent {
MOUNTED, MOUNTED,
@@ -22,12 +22,14 @@ class AndroidUsbMountMonitor(
) : UsbMountMonitor { ) : UsbMountMonitor {
private val appContext = context.applicationContext private val appContext = context.applicationContext
private var receiver: BroadcastReceiver? = null private var receiver: BroadcastReceiver? = null
private var onChanged: (() -> Unit)? = null
private val _events = MutableSharedFlow<UsbMountEvent>(extraBufferCapacity = 8) private val _events = MutableSharedFlow<UsbMountEvent>(extraBufferCapacity = 8)
val events: SharedFlow<UsbMountEvent> = _events.asSharedFlow() val events: SharedFlow<UsbMountEvent> = _events.asSharedFlow()
var onMount: (() -> Unit)? = null override fun setOnChanged(listener: (() -> Unit)?) {
var onEject: (() -> Unit)? = null onChanged = listener
}
override fun start() { override fun start() {
if (receiver != null) return if (receiver != null) return
@@ -39,19 +41,16 @@ class AndroidUsbMountMonitor(
receiver = object : BroadcastReceiver() { receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) { when (intent?.action) {
Intent.ACTION_MEDIA_MOUNTED -> { Intent.ACTION_MEDIA_MOUNTED -> _events.tryEmit(UsbMountEvent.MOUNTED)
_events.tryEmit(UsbMountEvent.MOUNTED) Intent.ACTION_MEDIA_EJECT -> _events.tryEmit(UsbMountEvent.EJECTED)
onMount?.invoke() else -> return
}
Intent.ACTION_MEDIA_EJECT -> {
_events.tryEmit(UsbMountEvent.EJECTED)
onEject?.invoke()
}
} }
onChanged?.invoke()
} }
} }
// 系统 MEDIA_* 广播来自其他进程,必须 EXPORTED,否则 Android 13+ 收不到。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 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 { } else {
@Suppress("UnspecifiedRegisterReceiverFlag") @Suppress("UnspecifiedRegisterReceiverFlag")
appContext.registerReceiver(receiver, filter) 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 android.content.Context
import top.zhushenwudi.llmp.platform.AndroidMediaSessionBridge import com.zhushenwudi.lovelivemusicplayer.AppContainer
import top.zhushenwudi.llmp.platform.PlatformServices import com.zhushenwudi.lovelivemusicplayer.native.UmengBridge
import top.zhushenwudi.llmp.platform.createFileSystem import com.zhushenwudi.lovelivemusicplayer.platform.AndroidMediaSessionBridge
import top.zhushenwudi.llmp.native.UmengBridge import com.zhushenwudi.lovelivemusicplayer.platform.PlatformServices
import com.zhushenwudi.lovelivemusicplayer.platform.createFileSystem
fun installAndroidPlatformServices(context: Context) { fun installAndroidPlatformServices(context: Context) {
val appContext = context.applicationContext val appContext = context.applicationContext
@@ -23,4 +24,5 @@ fun installAndroidPlatformServices(context: Context) {
) )
(PlatformServices.usbMount as? AndroidUsbMountMonitor)?.start() (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.app.ActivityManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import top.zhushenwudi.llmp.native.UmengBridge import com.zhushenwudi.lovelivemusicplayer.native.UmengBridge
object AppUtils { object AppUtils {
private const val PREFIX_URL = "llmp://" 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 SAVE_FILE_NAME = "sharedImage.png"
const val SP_NAME = "HomeWidgetPreferences" const val SP_NAME = "HomeWidgetPreferences"
@@ -40,6 +42,7 @@ object AppUtils {
return false return false
} }
@Suppress("DEPRECATION")
private fun getRunningService(mContext: Context): ArrayList<ActivityManager.RunningServiceInfo> { private fun getRunningService(mContext: Context): ArrayList<ActivityManager.RunningServiceInfo> {
val myManager = val myManager =
mContext.applicationContext.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager 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.annotation.SuppressLint
import android.content.Context import android.content.Context
@@ -62,7 +62,7 @@ object ImageUtil {
val circularBitmap = createBitmap(bitmap.width, bitmap.height) val circularBitmap = createBitmap(bitmap.width, bitmap.height)
val paint = Paint() val paint = Paint()
paint.isAntiAlias = true 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) val canvas = Canvas(circularBitmap)
canvas.drawCircle(bitmap.width / 2f, bitmap.height / 2f, bitmap.width / 2f, paint) canvas.drawCircle(bitmap.width / 2f, bitmap.height / 2f, bitmap.width / 2f, paint)
return circularBitmap return circularBitmap
@@ -121,7 +121,7 @@ object ImageUtil {
null, null,
Shader.TileMode.CLAMP, Shader.TileMode.CLAMP,
) )
paint.setShader(gradient) paint.shader = gradient
canvas.drawRect(rect, paint) canvas.drawRect(rect, paint)
return bitmap return bitmap
} }
@@ -1,4 +1,4 @@
package top.zhushenwudi.llmp.player package com.zhushenwudi.lovelivemusicplayer.player
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
@@ -20,15 +20,15 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import top.zhushenwudi.llmp.MainActivity import com.zhushenwudi.lovelivemusicplayer.MainActivity
import top.zhushenwudi.llmp.R import com.zhushenwudi.lovelivemusicplayer.i18n.I18n
import top.zhushenwudi.llmp.native.DesktopLyricOverlay 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 * 一个 FGS 通知(Media3 媒体样式)覆盖播放;悬浮歌词不再发第二条「歌词服务」通知。
* second "歌词服务" notification.
*/ */
class PlaybackService : MediaSessionService() { class PlaybackService : MediaSessionService() {
private var mediaSession: MediaSession? = null private var mediaSession: MediaSession? = null
@@ -58,7 +58,7 @@ class PlaybackService : MediaSessionService() {
mediaSession = session mediaSession = session
addSession(session) addSession(session)
// Satisfy startForegroundService() until Media3 posts the real media notification. // 在 Media3 发布真实媒体通知前满足 startForegroundService()。
val notification = bootstrapNotification() val notification = bootstrapNotification()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground( startForeground(
@@ -138,7 +138,7 @@ class PlaybackService : MediaSessionService() {
private fun bootstrapNotification() = private fun bootstrapNotification() =
NotificationCompat.Builder(this, CHANNEL_ID) NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(getString(R.string.app_name)) .setContentTitle(getString(R.string.app_name))
.setContentText("准备播放") .setContentText(I18n.t("ready_to_play"))
.setSmallIcon(R.drawable.logo) .setSmallIcon(R.drawable.logo)
.setContentIntent(sessionActivityIntent()) .setContentIntent(sessionActivityIntent())
.setOngoing(true) .setOngoing(true)
@@ -160,8 +160,8 @@ class PlaybackService : MediaSessionService() {
companion object { companion object {
const val CHANNEL_ID = "llmp_playback" const val CHANNEL_ID = "llmp_playback"
const val NOTIF_ID = 1001 const val NOTIF_ID = 1001
const val ACTION_SHOW_DESKTOP_LYRIC = "top.zhushenwudi.llmp.SHOW_DESKTOP_LYRIC" const val ACTION_SHOW_DESKTOP_LYRIC = "com.zhushenwudi.lovelivemusicplayer.SHOW_DESKTOP_LYRIC"
const val ACTION_HIDE_DESKTOP_LYRIC = "top.zhushenwudi.llmp.HIDE_DESKTOP_LYRIC" const val ACTION_HIDE_DESKTOP_LYRIC = "com.zhushenwudi.lovelivemusicplayer.HIDE_DESKTOP_LYRIC"
@Volatile @Volatile
private var instance: PlaybackService? = null private var instance: PlaybackService? = null
@@ -171,7 +171,7 @@ class PlaybackService : MediaSessionService() {
ContextCompat.startForegroundService(context, intent) ContextCompat.startForegroundService(context, intent)
} }
/** Show floating lyrics; reuses this service's Media3 FGS notification. */ /** 显示悬浮歌词;复用本服务的 Media3 FGS 通知。 */
fun showDesktopLyric(context: Context) { fun showDesktopLyric(context: Context) {
val existing = instance val existing = instance
if (existing != null) { if (existing != null) {
@@ -184,14 +184,14 @@ class PlaybackService : MediaSessionService() {
ContextCompat.startForegroundService(context, intent) ContextCompat.startForegroundService(context, intent)
} }
/** Hide floating lyrics without stopping media playback / FGS. */ /** 隐藏悬浮歌词,不停止媒体播放/FGS。 */
fun hideDesktopLyric(context: Context) { fun hideDesktopLyric(context: Context) {
val existing = instance val existing = instance
if (existing != null) { if (existing != null) {
existing.hideDesktopLyricInternal() existing.hideDesktopLyricInternal()
return return
} }
// Service not running — nothing to hide. // 服务未运行,无需隐藏。
} }
fun updateDesktopLyric(line1: String?, line2: String?, currentLine: Int) { 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,8 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:background="#EEEEEE">
<ProgressBar <ProgressBar
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -55,7 +55,8 @@
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
android:layout_marginStart="25dp" android:layout_marginStart="25dp"
android:background="@drawable/ic_close" /> android:background="@drawable/ic_close"
android:contentDescription="@string/lyric_close" />
<ImageView <ImageView
android:id="@+id/ic_pip" android:id="@+id/ic_pip"
@@ -64,7 +65,8 @@
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
android:layout_marginEnd="25dp" android:layout_marginEnd="25dp"
android:background="@drawable/ic_pip" /> android:background="@drawable/ic_pip"
android:contentDescription="@string/lyric_pip" />
<ImageView <ImageView
android:id="@+id/ic_icon" android:id="@+id/ic_icon"
@@ -75,7 +77,8 @@
android:layout_alignBottom="@id/ll_lyric" android:layout_alignBottom="@id/ll_lyric"
android:layout_marginBottom="5dp" android:layout_marginBottom="5dp"
android:layout_marginEnd="25dp" android:layout_marginEnd="25dp"
android:background="@drawable/logo" /> android:background="@drawable/logo"
android:contentDescription="@null" />
<ImageView <ImageView
android:id="@+id/ic_translate" android:id="@+id/ic_translate"
@@ -85,5 +88,6 @@
android:layout_alignBottom="@id/ll_lyric" android:layout_alignBottom="@id/ll_lyric"
android:layout_marginBottom="5dp" android:layout_marginBottom="5dp"
android:layout_marginEnd="25dp" android:layout_marginEnd="25dp"
android:background="@drawable/ic_translate" /> android:background="@drawable/ic_translate"
android:contentDescription="@string/lyric_translate" />
</RelativeLayout> </RelativeLayout>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

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> <resources>
<string name="app_name">LLMP KMP</string> <string name="app_name">LLMP KMP</string>
<string name="playback_channel_name">正在播放</string> <string name="playback_channel_name">正在播放</string>
<string name="lyric_close">关闭桌面歌词</string>
<string name="lyric_pip">画中画</string>
<string name="lyric_translate">翻译歌词</string>
</resources> </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>
+11
View File
@@ -7,3 +7,14 @@ plugins {
alias(libs.plugins.kotlinSerialization) apply false alias(libs.plugins.kotlinSerialization) apply false
alias(libs.plugins.sqldelight) apply false alias(libs.plugins.sqldelight) apply false
} }
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask
// expect/actual class 在 Kotlin 2.x 仍为 Beta;统一抑制 IDE/Gradle 警告(KT-61573)。
subprojects {
tasks.withType<KotlinCompilationTask<*>>().configureEach {
compilerOptions {
freeCompilerArgs.add("-Xexpect-actual-classes")
}
}
}
+9 -3
View File
@@ -9,12 +9,17 @@ plugins {
} }
kotlin { kotlin {
compilerOptions {
freeCompilerArgs.add("-Xexpect-actual-classes")
}
android { android {
namespace = "top.zhushenwudi.llmp.composeapp" namespace = "com.zhushenwudi.lovelivemusicplayer.composeapp"
compileSdk = libs.versions.android.compileSdk.get().toInt() compileSdk = libs.versions.android.compileSdk.get().toInt()
minSdk = libs.versions.android.minSdk.get().toInt() minSdk = libs.versions.android.minSdk.get().toInt()
compilerOptions { compilerOptions {
jvmTarget.set(JvmTarget.JVM_11) jvmTarget.set(JvmTarget.JVM_11)
freeCompilerArgs.add("-Xexpect-actual-classes")
} }
androidResources { androidResources {
enable = true enable = true
@@ -28,6 +33,7 @@ kotlin {
iosTarget.binaries.framework { iosTarget.binaries.framework {
baseName = "ComposeApp" baseName = "ComposeApp"
isStatic = true isStatic = true
export(projects.shared)
} }
} }
@@ -41,7 +47,7 @@ kotlin {
implementation(libs.ktor.client.darwin) implementation(libs.ktor.client.darwin)
} }
commonMain.dependencies { commonMain.dependencies {
implementation(projects.shared) api(projects.shared)
implementation(libs.compose.runtime) implementation(libs.compose.runtime)
implementation(libs.compose.foundation) implementation(libs.compose.foundation)
implementation(libs.compose.material3) implementation(libs.compose.material3)
@@ -65,7 +71,7 @@ kotlin {
compose { compose {
resources { resources {
packageOfResClass = "top.zhushenwudi.llmp.generated.resources" packageOfResClass = "com.zhushenwudi.lovelivemusicplayer.generated.resources"
} }
} }
@@ -1,4 +1,4 @@
package top.zhushenwudi.llmp.platform package com.zhushenwudi.lovelivemusicplayer.platform
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -0,0 +1,12 @@
package com.zhushenwudi.lovelivemusicplayer.ui.image
import coil3.PlatformContext
import okio.Path
import okio.Path.Companion.toPath
import java.io.File
internal actual fun persistentImageCacheDirectory(context: PlatformContext): Path {
val dir = File(context.cacheDir, "llmp_image_cache")
if (!dir.exists()) dir.mkdirs()
return dir.absolutePath.toPath()
}
@@ -0,0 +1,22 @@
package com.zhushenwudi.lovelivemusicplayer.ui.platform
import android.content.ClipData
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.toClipEntry
import kotlinx.coroutines.launch
@Composable
actual fun rememberCopyTextHandler(): (String) -> Unit {
val clipboard = LocalClipboard.current
val scope = rememberCoroutineScope()
return { text ->
scope.launch {
clipboard.setClipEntry(
ClipData.newPlainText("text", text).toClipEntry(),
)
}
}
}
@@ -0,0 +1,11 @@
package com.zhushenwudi.lovelivemusicplayer.ui.scroll
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.runtime.Composable
@Composable
actual fun ScrollToTopHandler(listState: LazyListState) = Unit
@Composable
actual fun ScrollToTopHandler(gridState: LazyGridState) = Unit
@@ -1,29 +0,0 @@
package top.zhushenwudi.llmp.ui.player
import android.graphics.BitmapFactory
import androidx.compose.ui.graphics.Color
import androidx.palette.graphics.Palette
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
/**
* Flutter color_thief getColorFromImage → Color.fromARGB(150, r, g, b).
* Android Palette dominant is closest available substitute.
*/
actual suspend fun extractDominantColor(coverPath: String?): Color? =
withContext(Dispatchers.Default) {
if (coverPath.isNullOrBlank()) return@withContext null
val file = File(coverPath)
if (!file.isFile) return@withContext null
val bitmap = BitmapFactory.decodeFile(file.absolutePath) ?: return@withContext null
try {
val dominant = Palette.from(bitmap).generate().getDominantColor(0xFFFFAE00.toInt())
val r = android.graphics.Color.red(dominant)
val g = android.graphics.Color.green(dominant)
val b = android.graphics.Color.blue(dominant)
Color(red = r / 255f, green = g / 255f, blue = b / 255f, alpha = FlutterPaletteAlpha)
} finally {
bitmap.recycle()
}
}
@@ -1,5 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"> <svg xmlns="http://www.w3.org/2000/svg"
<path fill="none" d="M0 0h24v24H0z" /> viewBox="0 0 24 24"
width="16"
height="16">
<path
fill="none"
d="M0 0h24v24H0z" />
<path <path
d="M20 3v14a4 4 0 1 1-2-3.465V5H9v12a4 4 0 1 1-2-3.465V3h13zM5 19a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm11 0a2 2 0 1 0 0-4 2 2 0 0 0 0 4z" d="M20 3v14a4 4 0 1 1-2-3.465V5H9v12a4 4 0 1 1-2-3.465V3h13zM5 19a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm11 0a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"
fill="rgba(102,102,102,1)" /> fill="rgba(102,102,102,1)" />

Before

Width:  |  Height:  |  Size: 319 B

After

Width:  |  Height:  |  Size: 347 B

@@ -1,14 +1,25 @@
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"> <svg height="24"
<g fill="none" fill-rule="evenodd"> viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg">
<g
fill="none"
fill-rule="evenodd">
<path d="m0 0h24v24h-24z" /> <path d="m0 0h24v24h-24z" />
<g stroke="#666" stroke-linejoin="round" stroke-width="2"> <g
stroke="#666"
stroke-linejoin="round"
stroke-width="2">
<path <path
d="m12 22.5c-5.7989925 0-10.5-4.7010075-10.5-10.5s4.7010075-10.5 10.5-10.5 10.5 4.7010075 10.5 10.5" d="m12 22.5c-5.7989925 0-10.5-4.7010075-10.5-10.5s4.7010075-10.5 10.5-10.5 10.5 4.7010075 10.5 10.5"
stroke-linecap="round" /> stroke-linecap="round" />
<path d="m9.9 12v-3.637305l3.15 1.8186525 3.15 1.8186525-3.15 1.8186525-3.15 1.8186525z" />
<path <path
d="m9.9 12v-3.637305l3.15 1.8186525 3.15 1.8186525-3.15 1.8186525-3.15 1.8186525z" /> d="m18.85167 16.2v5.25"
<path d="m18.85167 16.2v5.25" stroke-linecap="round" /> stroke-linecap="round" />
<path d="m21.45 18.798593h-5.25" stroke-linecap="round" /> <path
d="m21.45 18.798593h-5.25"
stroke-linecap="round" />
</g> </g>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 691 B

After

Width:  |  Height:  |  Size: 804 B

@@ -1,5 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"> <svg xmlns="http://www.w3.org/2000/svg"
<path fill="none" d="M0 0h24v24H0z" /> viewBox="0 0 24 24"
<path d="M2 18h10v2H2v-2zm0-7h20v2H2v-2zm0-7h20v2H2V4zm16 14v-3h2v3h3v2h-3v3h-2v-3h-3v-2h3z" width="16"
height="16">
<path
fill="none"
d="M0 0h24v24H0z" />
<path
d="M2 18h10v2H2v-2zm0-7h20v2H2v-2zm0-7h20v2H2V4zm16 14v-3h2v3h3v2h-3v3h-2v-3h-3v-2h3z"
fill="rgba(102,102,102,1)" /> fill="rgba(102,102,102,1)" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 268 B

After

Width:  |  Height:  |  Size: 304 B

@@ -1,5 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"> <svg xmlns="http://www.w3.org/2000/svg"
<path fill="none" d="M0 0h24v24H0z" /> viewBox="0 0 24 24"
width="16"
height="16">
<path
fill="none"
d="M0 0h24v24H0z" />
<path <path
d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z" d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z"
fill="rgba(153,153,153,1)" /> fill="rgba(153,153,153,1)" />

Before

Width:  |  Height:  |  Size: 322 B

After

Width:  |  Height:  |  Size: 350 B

@@ -1,5 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"> <svg xmlns="http://www.w3.org/2000/svg"
<path fill="none" d="M0 0h24v24H0z" /> viewBox="0 0 24 24"
width="16"
height="16">
<path
fill="none"
d="M0 0h24v24H0z" />
<path <path
d="M17 6h5v2h-2v13a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V8H2V6h5V3a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v3zm1 2H6v12h12V8zm-9 3h2v6H9v-6zm4 0h2v6h-2v-6zM9 4v2h6V4H9z" d="M17 6h5v2h-2v13a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V8H2V6h5V3a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v3zm1 2H6v12h12V8zm-9 3h2v6H9v-6zm4 0h2v6h-2v-6zM9 4v2h6V4H9z"
fill="rgba(90,90,90,1)" /> fill="rgba(90,90,90,1)" />

Before

Width:  |  Height:  |  Size: 337 B

After

Width:  |  Height:  |  Size: 365 B

@@ -1,5 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"> <svg xmlns="http://www.w3.org/2000/svg"
<path fill="none" d="M0 0h24v24H0z" /> viewBox="0 0 24 24"
width="16"
height="16">
<path
fill="none"
d="M0 0h24v24H0z" />
<path <path
d="M16.757 3l-2 2H5v14h14V9.243l2-2V20a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h12.757zm3.728-.9L21.9 3.516l-9.192 9.192-1.412.003-.002-1.417L20.485 2.1z" d="M16.757 3l-2 2H5v14h14V9.243l2-2V20a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h12.757zm3.728-.9L21.9 3.516l-9.192 9.192-1.412.003-.002-1.417L20.485 2.1z"
fill="rgba(90,90,90,1)" /> fill="rgba(90,90,90,1)" />

Before

Width:  |  Height:  |  Size: 346 B

After

Width:  |  Height:  |  Size: 374 B

@@ -1,8 +1,20 @@
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"> <svg height="16"
<g fill="none" fill-rule="evenodd" stroke="#666" stroke-linecap="round" stroke-linejoin="round"> viewBox="0 0 16 16"
<path width="16"
d="m6.15789474 2.84210526v-1.10526315c0-.40694685.32988421-.73684211.7368421-.73684211h7.36842106c.4069579 0 .7368421.32989526.7368421.73684211v8.10526315c0 .40695794-.3298842.73684214-.7368421.73684214h-1.4736842" /> xmlns="http://www.w3.org/2000/svg">
<rect height="8.842105" rx="1" width="8.842105" x="1" y="5.789474" /> <g
fill="none"
fill-rule="evenodd"
stroke="#666"
stroke-linecap="round"
stroke-linejoin="round">
<path d="m6.15789474 2.84210526v-1.10526315c0-.40694685.32988421-.73684211.7368421-.73684211h7.36842106c.4069579 0 .7368421.32989526.7368421.73684211v8.10526315c0 .40695794-.3298842.73684214-.7368421.73684214h-1.4736842" />
<rect
height="8.842105"
rx="1"
width="8.842105"
x="1"
y="5.789474" />
<path d="m5.421053 8.368421v3.684211" /> <path d="m5.421053 8.368421v3.684211" />
<path d="m3.578947 10.210526h3.684211" /> <path d="m3.578947 10.210526h3.684211" />
</g> </g>

Before

Width:  |  Height:  |  Size: 621 B

After

Width:  |  Height:  |  Size: 721 B

@@ -1,5 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"> <svg xmlns="http://www.w3.org/2000/svg"
<path fill="none" d="M0 0h24v24H0z" /> viewBox="0 0 24 24"
width="16"
height="16">
<path
fill="none"
d="M0 0h24v24H0z" />
<path <path
d="M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm0 2C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-8a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm0 2a4 4 0 1 1 0-8 4 4 0 0 1 0 8z" d="M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm0 2C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-8a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm0 2a4 4 0 1 1 0-8 4 4 0 0 1 0 8z"
fill="rgba(102,102,102,1)" /> fill="rgba(102,102,102,1)" />

Before

Width:  |  Height:  |  Size: 368 B

After

Width:  |  Height:  |  Size: 396 B

@@ -1,5 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"> <svg xmlns="http://www.w3.org/2000/svg"
<path fill="none" d="M0 0h24v24H0z" /> viewBox="0 0 24 24"
width="16"
height="16">
<path
fill="none"
d="M0 0h24v24H0z" />
<path <path
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zM11 7h2v2h-2V7zm0 4h2v6h-2v-6z" d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zM11 7h2v2h-2V7zm0 4h2v6h-2v-6z"
fill="rgba(102,102,102,1)" /> fill="rgba(102,102,102,1)" />

Before

Width:  |  Height:  |  Size: 332 B

After

Width:  |  Height:  |  Size: 360 B

@@ -1,5 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"> <svg xmlns="http://www.w3.org/2000/svg"
<path fill="none" d="M0 0h24v24H0z" /> viewBox="0 0 24 24"
width="16"
height="16">
<path
fill="none"
d="M0 0h24v24H0z" />
<path <path
d="M4.828 21l-.02.02-.021-.02H2.992A.993.993 0 0 1 2 20.007V3.993A1 1 0 0 1 2.992 3h18.016c.548 0 .992.445.992.993v16.014a1 1 0 0 1-.992.993H4.828zM20 15V5H4v14L14 9l6 6zm0 2.828l-6-6L6.828 19H20v-1.172zM8 11a2 2 0 1 1 0-4 2 2 0 0 1 0 4z" d="M4.828 21l-.02.02-.021-.02H2.992A.993.993 0 0 1 2 20.007V3.993A1 1 0 0 1 2.992 3h18.016c.548 0 .992.445.992.993v16.014a1 1 0 0 1-.992.993H4.828zM20 15V5H4v14L14 9l6 6zm0 2.828l-6-6L6.828 19H20v-1.172zM8 11a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"
fill="rgba(102,102,102,1)" /> fill="rgba(102,102,102,1)" />

Before

Width:  |  Height:  |  Size: 428 B

After

Width:  |  Height:  |  Size: 456 B

@@ -1,5 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"> <svg xmlns="http://www.w3.org/2000/svg"
<path fill="none" d="M0 0h24v24H0z" /> viewBox="0 0 24 24"
width="16"
height="16">
<path
fill="none"
d="M0 0h24v24H0z" />
<path <path
d="M5 5v3h14V5H5zM4 3h16a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1zm2 9h6a1 1 0 0 1 1 1v3h1v6h-4v-6h1v-2H5a1 1 0 0 1-1-1v-2h2v1zm11.732 1.732l1.768-1.768 1.768 1.768a2.5 2.5 0 1 1-3.536 0z" d="M5 5v3h14V5H5zM4 3h16a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1zm2 9h6a1 1 0 0 1 1 1v3h1v6h-4v-6h1v-2H5a1 1 0 0 1-1-1v-2h2v1zm11.732 1.732l1.768-1.768 1.768 1.768a2.5 2.5 0 1 1-3.536 0z"
fill="rgba(102,102,102,1)" /> fill="rgba(102,102,102,1)" />

Before

Width:  |  Height:  |  Size: 398 B

After

Width:  |  Height:  |  Size: 427 B

@@ -1,4 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" <svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
fill="currentColor"> fill="currentColor">
<path <path
d="M9 1V3H15V1H17V3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H7V1H9ZM20 11H4V19H20V11ZM8 13V15H6V13H8ZM13 13V15H11V13H13ZM18 13V15H16V13H18ZM7 5H4V9H20V5H17V7H15V5H9V7H7V5Z" d="M9 1V3H15V1H17V3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H7V1H9ZM20 11H4V19H20V11ZM8 13V15H6V13H8ZM13 13V15H11V13H13ZM18 13V15H16V13H18ZM7 5H4V9H20V5H17V7H15V5H9V7H7V5Z"

Before

Width:  |  Height:  |  Size: 416 B

After

Width:  |  Height:  |  Size: 428 B

@@ -1,5 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"> <svg xmlns="http://www.w3.org/2000/svg"
<path fill="none" d="M0 0h24v24H0z" /> viewBox="0 0 24 24"
width="16"
height="16">
<path
fill="none"
d="M0 0h24v24H0z" />
<path <path
d="M19 20H5V21C5 21.5523 4.55228 22 4 22H3C2.44772 22 2 21.5523 2 21V12L4.51334 5.29775C4.80607 4.51715 5.55231 4 6.386 4H17.614C18.4477 4 19.1939 4.51715 19.4867 5.29775L22 12V21C22 21.5523 21.5523 22 21 22H20C19.4477 22 19 21.5523 19 21V20ZM4.136 12H19.864L17.614 6H6.386L4.136 12ZM6.5 17C7.32843 17 8 16.3284 8 15.5C8 14.6716 7.32843 14 6.5 14C5.67157 14 5 14.6716 5 15.5C5 16.3284 5.67157 17 6.5 17ZM17.5 17C18.3284 17 19 16.3284 19 15.5C19 14.6716 18.3284 14 17.5 14C16.6716 14 16 14.6716 16 15.5C16 16.3284 16.6716 17 17.5 17Z" d="M19 20H5V21C5 21.5523 4.55228 22 4 22H3C2.44772 22 2 21.5523 2 21V12L4.51334 5.29775C4.80607 4.51715 5.55231 4 6.386 4H17.614C18.4477 4 19.1939 4.51715 19.4867 5.29775L22 12V21C22 21.5523 21.5523 22 21 22H20C19.4477 22 19 21.5523 19 21V20ZM4.136 12H19.864L17.614 6H6.386L4.136 12ZM6.5 17C7.32843 17 8 16.3284 8 15.5C8 14.6716 7.32843 14 6.5 14C5.67157 14 5 14.6716 5 15.5C5 16.3284 5.67157 17 6.5 17ZM17.5 17C18.3284 17 19 16.3284 19 15.5C19 14.6716 18.3284 14 17.5 14C16.6716 14 16 14.6716 16 15.5C16 16.3284 16.6716 17 17.5 17Z"
fill="rgba(102,102,102,1)" /> fill="rgba(102,102,102,1)" />

Before

Width:  |  Height:  |  Size: 723 B

After

Width:  |  Height:  |  Size: 751 B

@@ -1,5 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"> <svg xmlns="http://www.w3.org/2000/svg"
<path fill="none" d="M0 0h24v24H0z" /> viewBox="0 0 24 24"
width="24"
height="24">
<path
fill="none"
d="M0 0h24v24H0z" />
<path <path
d="M12 2c5.522 0 10 3.978 10 8.889a5.558 5.558 0 0 1-5.556 5.555h-1.966c-.922 0-1.667.745-1.667 1.667 0 .422.167.811.422 1.1.267.3.434.689.434 1.122C13.667 21.256 12.9 22 12 22 6.478 22 2 17.522 2 12S6.478 2 12 2zm-1.189 16.111a3.664 3.664 0 0 1 3.667-3.667h1.966A3.558 3.558 0 0 0 20 10.89C20 7.139 16.468 4 12 4a8 8 0 0 0-.676 15.972 3.648 3.648 0 0 1-.513-1.86zM7.5 12a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm9 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zM12 9a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z" d="M12 2c5.522 0 10 3.978 10 8.889a5.558 5.558 0 0 1-5.556 5.555h-1.966c-.922 0-1.667.745-1.667 1.667 0 .422.167.811.422 1.1.267.3.434.689.434 1.122C13.667 21.256 12.9 22 12 22 6.478 22 2 17.522 2 12S6.478 2 12 2zm-1.189 16.111a3.664 3.664 0 0 1 3.667-3.667h1.966A3.558 3.558 0 0 0 20 10.89C20 7.139 16.468 4 12 4a8 8 0 0 0-.676 15.972 3.648 3.648 0 0 1-.513-1.86zM7.5 12a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm9 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zM12 9a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"
fill="rgba(102,102,102,1)" /> fill="rgba(102,102,102,1)" />

Before

Width:  |  Height:  |  Size: 682 B

After

Width:  |  Height:  |  Size: 710 B

@@ -1,5 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"> <svg xmlns="http://www.w3.org/2000/svg"
<path fill="none" d="M0 0h24v24H0z" /> viewBox="0 0 24 24"
width="16"
height="16">
<path
fill="none"
d="M0 0h24v24H0z" />
<path <path
d="M13 13v5.585l1.828-1.828 1.415 1.415L12 22.414l-4.243-4.242 1.415-1.415L11 18.585V13h2zM12 2a7.001 7.001 0 0 1 6.954 6.194 5.5 5.5 0 0 1-.953 10.784v-2.014a3.5 3.5 0 1 0-1.112-6.91 5 5 0 1 0-9.777 0 3.5 3.5 0 0 0-1.292 6.88l.18.03v2.014a5.5 5.5 0 0 1-.954-10.784A7 7 0 0 1 12 2z" d="M13 13v5.585l1.828-1.828 1.415 1.415L12 22.414l-4.243-4.242 1.415-1.415L11 18.585V13h2zM12 2a7.001 7.001 0 0 1 6.954 6.194 5.5 5.5 0 0 1-.953 10.784v-2.014a3.5 3.5 0 1 0-1.112-6.91 5 5 0 1 0-9.777 0 3.5 3.5 0 0 0-1.292 6.88l.18.03v2.014a5.5 5.5 0 0 1-.954-10.784A7 7 0 0 1 12 2z"
fill="rgba(102,102,102,1)" /> fill="rgba(102,102,102,1)" />

Before

Width:  |  Height:  |  Size: 472 B

After

Width:  |  Height:  |  Size: 500 B

@@ -1,5 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"> <svg xmlns="http://www.w3.org/2000/svg"
<path fill="none" d="M0 0h24v24H0z" /> viewBox="0 0 24 24"
width="24"
height="24">
<path
fill="none"
d="M0 0h24v24H0z" />
<path <path
d="M16.05 12.05L21 17l-4.95 4.95-1.414-1.414 2.536-2.537L4 18v-2h13.172l-2.536-2.536 1.414-1.414zm-8.1-10l1.414 1.414L6.828 6 20 6v2H6.828l2.536 2.536L7.95 11.95 3 7l4.95-4.95z" d="M16.05 12.05L21 17l-4.95 4.95-1.414-1.414 2.536-2.537L4 18v-2h13.172l-2.536-2.536 1.414-1.414zm-8.1-10l1.414 1.414L6.828 6 20 6v2H6.828l2.536 2.536L7.95 11.95 3 7l4.95-4.95z"
fill="rgba(102,102,102,1)" /> fill="rgba(102,102,102,1)" />

Before

Width:  |  Height:  |  Size: 367 B

After

Width:  |  Height:  |  Size: 395 B

@@ -1,5 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"> <svg xmlns="http://www.w3.org/2000/svg"
<path fill="none" d="M0 0h24v24H0z" /> viewBox="0 0 24 24"
width="16"
height="16">
<path
fill="none"
d="M0 0h24v24H0z" />
<path <path
d="M10 7a7 7 0 0 0 12 4.9v.1c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2h.1A6.979 6.979 0 0 0 10 7zm-6 5a8 8 0 0 0 15.062 3.762A9 9 0 0 1 8.238 4.938 7.999 7.999 0 0 0 4 12z" d="M10 7a7 7 0 0 0 12 4.9v.1c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2h.1A6.979 6.979 0 0 0 10 7zm-6 5a8 8 0 0 0 15.062 3.762A9 9 0 0 1 8.238 4.938 7.999 7.999 0 0 0 4 12z"
fill="rgba(102,102,102,1)" /> fill="rgba(102,102,102,1)" />

Before

Width:  |  Height:  |  Size: 370 B

After

Width:  |  Height:  |  Size: 398 B

@@ -1,5 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"> <svg xmlns="http://www.w3.org/2000/svg"
<path fill="none" d="M0 0h24v24H0z" /> viewBox="0 0 24 24"
width="16"
height="16">
<path
fill="none"
d="M0 0h24v24H0z" />
<path <path
d="M13 19.9a5.002 5.002 0 0 0 4-4.9v-3a4.98 4.98 0 0 0-.415-2h-9.17A4.98 4.98 0 0 0 7 12v3a5.002 5.002 0 0 0 4 4.9V14h2v5.9zm-7.464-2.21A6.979 6.979 0 0 1 5 15H2v-2h3v-1c0-.643.087-1.265.249-1.856L3.036 8.866l1-1.732L6.056 8.3a7.01 7.01 0 0 1 .199-.3h11.49c.069.098.135.199.199.3l2.02-1.166 1 1.732-2.213 1.278c.162.59.249 1.213.249 1.856v1h3v2h-3c0 .953-.19 1.862-.536 2.69l2.5 1.444-1 1.732-2.526-1.458A6.986 6.986 0 0 1 12 22a6.986 6.986 0 0 1-5.438-2.592l-2.526 1.458-1-1.732 2.5-1.443zM8 6a4 4 0 1 1 8 0H8z" d="M13 19.9a5.002 5.002 0 0 0 4-4.9v-3a4.98 4.98 0 0 0-.415-2h-9.17A4.98 4.98 0 0 0 7 12v3a5.002 5.002 0 0 0 4 4.9V14h2v5.9zm-7.464-2.21A6.979 6.979 0 0 1 5 15H2v-2h3v-1c0-.643.087-1.265.249-1.856L3.036 8.866l1-1.732L6.056 8.3a7.01 7.01 0 0 1 .199-.3h11.49c.069.098.135.199.199.3l2.02-1.166 1 1.732-2.213 1.278c.162.59.249 1.213.249 1.856v1h3v2h-3c0 .953-.19 1.862-.536 2.69l2.5 1.444-1 1.732-2.526-1.458A6.986 6.986 0 0 1 12 22a6.986 6.986 0 0 1-5.438-2.592l-2.526 1.458-1-1.732 2.5-1.443zM8 6a4 4 0 1 1 8 0H8z"
fill="rgba(102,102,102,1)" /> fill="rgba(102,102,102,1)" />

Before

Width:  |  Height:  |  Size: 702 B

After

Width:  |  Height:  |  Size: 730 B

@@ -1,5 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" <svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
fill="currentColor"> fill="currentColor">
<path <path d="M20.4668 8.69379L20.7134 8.12811C21.1529 7.11947 21.9445 6.31641 22.9323 5.87708L23.6919 5.53922C24.1027 5.35653 24.1027 4.75881 23.6919 4.57612L22.9748 4.25714C21.9616 3.80651 21.1558 2.97373 20.7238 1.93083L20.4706 1.31953C20.2942 0.893489 19.7058 0.893489 19.5293 1.31953L19.2761 1.93083C18.8442 2.97373 18.0384 3.80651 17.0252 4.25714L16.308 4.57612C15.8973 4.75881 15.8973 5.35653 16.308 5.53922L17.0677 5.87708C18.0555 6.31641 18.8471 7.11947 19.2866 8.12811L19.5331 8.69379C19.7136 9.10792 20.2864 9.10792 20.4668 8.69379ZM2 4C2 3.44772 2.44772 3 3 3H14V5H4V19H20V11H22V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4ZM12 12.1707V6H16V8H14V15C14 16.6569 12.6569 18 11 18C9.34315 18 8 16.6569 8 15C8 13.3431 9.34315 12 11 12C11.3506 12 11.6872 12.0602 12 12.1707Z" />
d="M20.4668 8.69379L20.7134 8.12811C21.1529 7.11947 21.9445 6.31641 22.9323 5.87708L23.6919 5.53922C24.1027 5.35653 24.1027 4.75881 23.6919 4.57612L22.9748 4.25714C21.9616 3.80651 21.1558 2.97373 20.7238 1.93083L20.4706 1.31953C20.2942 0.893489 19.7058 0.893489 19.5293 1.31953L19.2761 1.93083C18.8442 2.97373 18.0384 3.80651 17.0252 4.25714L16.308 4.57612C15.8973 4.75881 15.8973 5.35653 16.308 5.53922L17.0677 5.87708C18.0555 6.31641 18.8471 7.11947 19.2866 8.12811L19.5331 8.69379C19.7136 9.10792 20.2864 9.10792 20.4668 8.69379ZM2 4C2 3.44772 2.44772 3 3 3H14V5H4V19H20V11H22V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4ZM12 12.1707V6H16V8H14V15C14 16.6569 12.6569 18 11 18C9.34315 18 8 16.6569 8 15C8 13.3431 9.34315 12 11 12C11.3506 12 11.6872 12.0602 12 12.1707Z" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 922 B

After

Width:  |  Height:  |  Size: 926 B

@@ -1,5 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <svg xmlns="http://www.w3.org/2000/svg"
<path fill="none" d="M0 0h24v24H0z" /> viewBox="0 0 24 24">
<path
fill="none"
d="M0 0h24v24H0z" />
<path <path
d="M3.91762 8.0366C3.32984 9.23328 3 10.5792 3 11.9999C3 16.9704 7.02944 20.9999 12 20.9999C13.4216 20.9999 14.7684 20.6696 15.9657 20.081C16.8385 20.4544 17.6848 20.6991 18.4564 20.762C19.3582 20.8356 20.3 20.6665 20.9818 19.9847C21.7339 19.2325 21.8625 18.1689 21.7279 17.1727C21.6052 16.2638 21.2481 15.2537 20.726 14.2114C20.9051 13.503 21 12.7619 21 11.9999C21 7.02929 16.9706 2.99986 12 2.99986C11.2389 2.99986 10.4987 3.09454 9.79103 3.27318C8.7474 2.7498 7.73605 2.39172 6.8261 2.26834C5.82897 2.13315 4.76406 2.26128 4.01121 3.01413C3.3287 3.69664 3.16001 4.63956 3.2341 5.54233C3.29752 6.315 3.54313 7.16247 3.91762 8.0366ZM5.3224 5.96574C5.2734 5.75321 5.24204 5.55715 5.2274 5.37873C5.17928 4.79243 5.31727 4.53649 5.42543 4.42834C5.54452 4.30925 5.84797 4.15403 6.55739 4.25021C6.75482 4.27698 6.96951 4.32189 7.2 4.38547C6.50364 4.82527 5.87203 5.35784 5.3224 5.96574ZM19.6124 16.803C19.6751 17.0316 19.7195 17.2445 19.7459 17.4404C19.8416 18.1485 19.6865 18.4515 19.5676 18.5705C19.4595 18.6785 19.204 18.8163 18.6189 18.7686C18.4419 18.7542 18.2475 18.7233 18.0368 18.675C18.6427 18.1268 19.1736 17.4971 19.6124 16.803ZM15.8812 17.8264C14.2046 16.9482 12.2571 15.5026 10.3752 13.6207C8.4954 11.741 7.05092 9.79561 6.17241 8.12028C7.06357 6.78465 8.40124 5.77312 9.96933 5.29866C10.6108 5.10457 11.2923 4.99986 12 4.99986C15.866 4.99986 19 8.13386 19 11.9999C19 12.7083 18.8951 13.3904 18.7006 14.0325C18.2261 15.5991 17.2155 16.9356 15.8812 17.8264ZM13.6052 18.8152C13.0901 18.9359 12.5528 18.9999 12 18.9999C8.13401 18.9999 5 15.8658 5 11.9999C5 11.4476 5.06377 10.9109 5.18429 10.3963C6.14883 11.913 7.43475 13.5087 8.96096 15.0349C10.489 16.563 12.0868 17.8502 13.6052 18.8152Z" d="M3.91762 8.0366C3.32984 9.23328 3 10.5792 3 11.9999C3 16.9704 7.02944 20.9999 12 20.9999C13.4216 20.9999 14.7684 20.6696 15.9657 20.081C16.8385 20.4544 17.6848 20.6991 18.4564 20.762C19.3582 20.8356 20.3 20.6665 20.9818 19.9847C21.7339 19.2325 21.8625 18.1689 21.7279 17.1727C21.6052 16.2638 21.2481 15.2537 20.726 14.2114C20.9051 13.503 21 12.7619 21 11.9999C21 7.02929 16.9706 2.99986 12 2.99986C11.2389 2.99986 10.4987 3.09454 9.79103 3.27318C8.7474 2.7498 7.73605 2.39172 6.8261 2.26834C5.82897 2.13315 4.76406 2.26128 4.01121 3.01413C3.3287 3.69664 3.16001 4.63956 3.2341 5.54233C3.29752 6.315 3.54313 7.16247 3.91762 8.0366ZM5.3224 5.96574C5.2734 5.75321 5.24204 5.55715 5.2274 5.37873C5.17928 4.79243 5.31727 4.53649 5.42543 4.42834C5.54452 4.30925 5.84797 4.15403 6.55739 4.25021C6.75482 4.27698 6.96951 4.32189 7.2 4.38547C6.50364 4.82527 5.87203 5.35784 5.3224 5.96574ZM19.6124 16.803C19.6751 17.0316 19.7195 17.2445 19.7459 17.4404C19.8416 18.1485 19.6865 18.4515 19.5676 18.5705C19.4595 18.6785 19.204 18.8163 18.6189 18.7686C18.4419 18.7542 18.2475 18.7233 18.0368 18.675C18.6427 18.1268 19.1736 17.4971 19.6124 16.803ZM15.8812 17.8264C14.2046 16.9482 12.2571 15.5026 10.3752 13.6207C8.4954 11.741 7.05092 9.79561 6.17241 8.12028C7.06357 6.78465 8.40124 5.77312 9.96933 5.29866C10.6108 5.10457 11.2923 4.99986 12 4.99986C15.866 4.99986 19 8.13386 19 11.9999C19 12.7083 18.8951 13.3904 18.7006 14.0325C18.2261 15.5991 17.2155 16.9356 15.8812 17.8264ZM13.6052 18.8152C13.0901 18.9359 12.5528 18.9999 12 18.9999C8.13401 18.9999 5 15.8658 5 11.9999C5 11.4476 5.06377 10.9109 5.18429 10.3963C6.14883 11.913 7.43475 13.5087 8.96096 15.0349C10.489 16.563 12.0868 17.8502 13.6052 18.8152Z"
fill="rgba(102,102,102,1)" /> fill="rgba(102,102,102,1)" />

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -1,5 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"> <svg xmlns="http://www.w3.org/2000/svg"
<path fill="none" d="M0 0h24v24H0z" /> viewBox="0 0 24 24"
width="16"
height="16">
<path
fill="none"
d="M0 0h24v24H0z" />
<path <path
d="M18.031 16.617l4.283 4.282-1.415 1.415-4.282-4.283A8.96 8.96 0 0 1 11 20c-4.968 0-9-4.032-9-9s4.032-9 9-9 9 4.032 9 9a8.96 8.96 0 0 1-1.969 5.617zm-2.006-.742A6.977 6.977 0 0 0 18 11c0-3.868-3.133-7-7-7-3.868 0-7 3.132-7 7 0 3.867 3.132 7 7 7a6.977 6.977 0 0 0 4.875-1.975l.15-.15zm-3.847-8.699a2 2 0 1 0 2.646 2.646 4 4 0 1 1-2.646-2.646z" d="M18.031 16.617l4.283 4.282-1.415 1.415-4.282-4.283A8.96 8.96 0 0 1 11 20c-4.968 0-9-4.032-9-9s4.032-9 9-9 9 4.032 9 9a8.96 8.96 0 0 1-1.969 5.617zm-2.006-.742A6.977 6.977 0 0 0 18 11c0-3.868-3.133-7-7-7-3.868 0-7 3.132-7 7 0 3.867 3.132 7 7 7a6.977 6.977 0 0 0 4.875-1.975l.15-.15zm-3.847-8.699a2 2 0 1 0 2.646 2.646 4 4 0 1 1-2.646-2.646z"
fill="rgba(102,102,102,1)" /> fill="rgba(102,102,102,1)" />

Before

Width:  |  Height:  |  Size: 533 B

After

Width:  |  Height:  |  Size: 561 B

@@ -1,5 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"> <svg xmlns="http://www.w3.org/2000/svg"
<path fill="none" d="M0 0h24v24H0z" /> viewBox="0 0 24 24"
width="16"
height="16">
<path
fill="none"
d="M0 0h24v24H0z" />
<path <path
d="M4.929 2.929l1.414 1.414A7.975 7.975 0 0 0 4 10c0 2.21.895 4.21 2.343 5.657L4.93 17.07A9.969 9.969 0 0 1 2 10a9.969 9.969 0 0 1 2.929-7.071zm14.142 0A9.969 9.969 0 0 1 22 10a9.969 9.969 0 0 1-2.929 7.071l-1.414-1.414A7.975 7.975 0 0 0 20 10c0-2.21-.895-4.21-2.343-5.657L19.07 2.93zM7.757 5.757l1.415 1.415A3.987 3.987 0 0 0 8 10c0 1.105.448 2.105 1.172 2.828l-1.415 1.415A5.981 5.981 0 0 1 6 10c0-1.657.672-3.157 1.757-4.243zm8.486 0A5.981 5.981 0 0 1 18 10a5.981 5.981 0 0 1-1.757 4.243l-1.415-1.415A3.987 3.987 0 0 0 16 10a3.987 3.987 0 0 0-1.172-2.828l1.415-1.415zM12 12a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 2c.58 0 1.077.413 1.184.983L14.5 22h-5l1.316-7.017c.107-.57.604-.983 1.184-.983z" d="M4.929 2.929l1.414 1.414A7.975 7.975 0 0 0 4 10c0 2.21.895 4.21 2.343 5.657L4.93 17.07A9.969 9.969 0 0 1 2 10a9.969 9.969 0 0 1 2.929-7.071zm14.142 0A9.969 9.969 0 0 1 22 10a9.969 9.969 0 0 1-2.929 7.071l-1.414-1.414A7.975 7.975 0 0 0 20 10c0-2.21-.895-4.21-2.343-5.657L19.07 2.93zM7.757 5.757l1.415 1.415A3.987 3.987 0 0 0 8 10c0 1.105.448 2.105 1.172 2.828l-1.415 1.415A5.981 5.981 0 0 1 6 10c0-1.657.672-3.157 1.757-4.243zm8.486 0A5.981 5.981 0 0 1 18 10a5.981 5.981 0 0 1-1.757 4.243l-1.415-1.415A3.987 3.987 0 0 0 16 10a3.987 3.987 0 0 0-1.172-2.828l1.415-1.415zM12 12a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 2c.58 0 1.077.413 1.184.983L14.5 22h-5l1.316-7.017c.107-.57.604-.983 1.184-.983z"
fill="rgba(102,102,102,1)" /> fill="rgba(102,102,102,1)" />

Before

Width:  |  Height:  |  Size: 879 B

After

Width:  |  Height:  |  Size: 907 B

@@ -1,5 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"> <svg xmlns="http://www.w3.org/2000/svg"
<path fill="none" d="M0 0h24v24H0z" /> viewBox="0 0 24 24"
width="16"
height="16">
<path
fill="none"
d="M0 0h24v24H0z" />
<path <path
d="M18.537 19.567A9.961 9.961 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10c0 2.136-.67 4.116-1.81 5.74L17 12h3a8 8 0 1 0-2.46 5.772l.997 1.795z" d="M18.537 19.567A9.961 9.961 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10c0 2.136-.67 4.116-1.81 5.74L17 12h3a8 8 0 1 0-2.46 5.772l.997 1.795z"
fill="rgba(102,102,102,1)" /> fill="rgba(102,102,102,1)" />

Before

Width:  |  Height:  |  Size: 352 B

After

Width:  |  Height:  |  Size: 380 B

@@ -1,5 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" <svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
fill="currentColor"> fill="currentColor">
<path <path d="M6 7.82843V20H18V4H9.82843L6 7.82843ZM4.29289 6.70711L9 2H19C19.5523 2 20 2.44772 20 3V21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V7.41421C4 7.149 4.10536 6.89464 4.29289 6.70711ZM15 5H17V9H15V5ZM12 5H14V9H12V5ZM9 6H11V9H9V6Z" />
d="M6 7.82843V20H18V4H9.82843L6 7.82843ZM4.29289 6.70711L9 2H19C19.5523 2 20 2.44772 20 3V21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V7.41421C4 7.149 4.10536 6.89464 4.29289 6.70711ZM15 5H17V9H15V5ZM12 5H14V9H12V5ZM9 6H11V9H9V6Z" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 384 B

After

Width:  |  Height:  |  Size: 390 B

@@ -1,5 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"> <svg xmlns="http://www.w3.org/2000/svg"
<path fill="none" d="M0 0h24v24H0z" /> viewBox="0 0 24 24"
width="16"
height="16">
<path
fill="none"
d="M0 0h24v24H0z" />
<path <path
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zM11 7h2v2h-2V7zm0 4h2v6h-2v-6z" d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zM11 7h2v2h-2V7zm0 4h2v6h-2v-6z"
fill="rgba(102,102,102,1)" /> fill="rgba(102,102,102,1)" />

Before

Width:  |  Height:  |  Size: 332 B

After

Width:  |  Height:  |  Size: 360 B

@@ -1,5 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"> <svg xmlns="http://www.w3.org/2000/svg"
<path fill="none" d="M0 0h24v24H0z" /> viewBox="0 0 24 24"
width="16"
height="16">
<path
fill="none"
d="M0 0h24v24H0z" />
<path <path
d="M2 12c0-.865.11-1.703.316-2.504A3 3 0 0 0 4.99 4.867a9.99 9.99 0 0 1 4.335-2.505 3 3 0 0 0 5.348 0 9.99 9.99 0 0 1 4.335 2.505 3 3 0 0 0 2.675 4.63c.206.8.316 1.638.316 2.503 0 .865-.11 1.703-.316 2.504a3 3 0 0 0-2.675 4.629 9.99 9.99 0 0 1-4.335 2.505 3 3 0 0 0-5.348 0 9.99 9.99 0 0 1-4.335-2.505 3 3 0 0 0-2.675-4.63C2.11 13.704 2 12.866 2 12zm4.804 3c.63 1.091.81 2.346.564 3.524.408.29.842.541 1.297.75A4.993 4.993 0 0 1 12 18c1.26 0 2.438.471 3.335 1.274.455-.209.889-.46 1.297-.75A4.993 4.993 0 0 1 17.196 15a4.993 4.993 0 0 1 2.77-2.25 8.126 8.126 0 0 0 0-1.5A4.993 4.993 0 0 1 17.195 9a4.993 4.993 0 0 1-.564-3.524 7.989 7.989 0 0 0-1.297-.75A4.993 4.993 0 0 1 12 6a4.993 4.993 0 0 1-3.335-1.274 7.99 7.99 0 0 0-1.297.75A4.993 4.993 0 0 1 6.804 9a4.993 4.993 0 0 1-2.77 2.25 8.126 8.126 0 0 0 0 1.5A4.993 4.993 0 0 1 6.805 15zM12 15a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0-2a1 1 0 1 0 0-2 1 1 0 0 0 0 2z" d="M2 12c0-.865.11-1.703.316-2.504A3 3 0 0 0 4.99 4.867a9.99 9.99 0 0 1 4.335-2.505 3 3 0 0 0 5.348 0 9.99 9.99 0 0 1 4.335 2.505 3 3 0 0 0 2.675 4.63c.206.8.316 1.638.316 2.503 0 .865-.11 1.703-.316 2.504a3 3 0 0 0-2.675 4.629 9.99 9.99 0 0 1-4.335 2.505 3 3 0 0 0-5.348 0 9.99 9.99 0 0 1-4.335-2.505 3 3 0 0 0-2.675-4.63C2.11 13.704 2 12.866 2 12zm4.804 3c.63 1.091.81 2.346.564 3.524.408.29.842.541 1.297.75A4.993 4.993 0 0 1 12 18c1.26 0 2.438.471 3.335 1.274.455-.209.889-.46 1.297-.75A4.993 4.993 0 0 1 17.196 15a4.993 4.993 0 0 1 2.77-2.25 8.126 8.126 0 0 0 0-1.5A4.993 4.993 0 0 1 17.195 9a4.993 4.993 0 0 1-.564-3.524 7.989 7.989 0 0 0-1.297-.75A4.993 4.993 0 0 1 12 6a4.993 4.993 0 0 1-3.335-1.274 7.99 7.99 0 0 0-1.297.75A4.993 4.993 0 0 1 6.804 9a4.993 4.993 0 0 1-2.77 2.25 8.126 8.126 0 0 0 0 1.5A4.993 4.993 0 0 1 6.805 15zM12 15a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0-2a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"
fill="rgba(102,102,102,1)" /> fill="rgba(102,102,102,1)" />

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -1,5 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"> <svg xmlns="http://www.w3.org/2000/svg"
<path fill="none" d="M0 0h24v24H0z" /> viewBox="0 0 24 24"
width="16"
height="16">
<path
fill="none"
d="M0 0h24v24H0z" />
<path <path
d="M11 2.05v2.012A8.001 8.001 0 0 0 12 20a8.001 8.001 0 0 0 7.938-7h2.013c-.502 5.053-4.766 9-9.951 9-5.523 0-10-4.477-10-10 0-5.185 3.947-9.449 9-9.95zm7.707 4.657L12 13.414 10.586 12l6.707-6.707L14 2h8v8l-3.293-3.293z" d="M11 2.05v2.012A8.001 8.001 0 0 0 12 20a8.001 8.001 0 0 0 7.938-7h2.013c-.502 5.053-4.766 9-9.951 9-5.523 0-10-4.477-10-10 0-5.185 3.947-9.449 9-9.95zm7.707 4.657L12 13.414 10.586 12l6.707-6.707L14 2h8v8l-3.293-3.293z"
fill="rgba(102,102,102,1)" /> fill="rgba(102,102,102,1)" />

Before

Width:  |  Height:  |  Size: 409 B

After

Width:  |  Height:  |  Size: 438 B

@@ -1,5 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"> <svg xmlns="http://www.w3.org/2000/svg"
<path fill="none" d="M0 0h24v24H0z" /> viewBox="0 0 24 24"
width="16"
height="16">
<path
fill="none"
d="M0 0h24v24H0z" />
<path <path
d="M14.515 5l2.606-2.607a1 1 0 0 1 1.415 0l4.242 4.243a1 1 0 0 1 0 1.414L19 11.828V21a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1v-9.172L1.222 8.05a1 1 0 0 1 0-1.414l4.242-4.243a1 1 0 0 1 1.415 0L9.485 5h5.03zm.828 2H8.657L6.172 4.515 3.343 7.343 7 11v9h10v-9l3.657-3.657-2.829-2.828L15.343 7z" d="M14.515 5l2.606-2.607a1 1 0 0 1 1.415 0l4.242 4.243a1 1 0 0 1 0 1.414L19 11.828V21a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1v-9.172L1.222 8.05a1 1 0 0 1 0-1.414l4.242-4.243a1 1 0 0 1 1.415 0L9.485 5h5.03zm.828 2H8.657L6.172 4.515 3.343 7.343 7 11v9h10v-9l3.657-3.657-2.829-2.828L15.343 7z"
fill="rgba(102,102,102,1)" /> fill="rgba(102,102,102,1)" />

Before

Width:  |  Height:  |  Size: 470 B

After

Width:  |  Height:  |  Size: 498 B

@@ -1,5 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"> <svg xmlns="http://www.w3.org/2000/svg"
<path fill="none" d="M0 0h24v24H0z" /> viewBox="0 0 24 24"
width="16"
height="16">
<path
fill="none"
d="M0 0h24v24H0z" />
<path <path
d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM13 12H17V14H11V7H13V12Z" d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM13 12H17V14H11V7H13V12Z"
fill="rgba(102,102,102,1)" /> fill="rgba(102,102,102,1)" />

Before

Width:  |  Height:  |  Size: 444 B

After

Width:  |  Height:  |  Size: 472 B

@@ -1,5 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"> <svg xmlns="http://www.w3.org/2000/svg"
<path fill="none" d="M0 0h24v24H0z" /> viewBox="0 0 24 24"
width="16"
height="16">
<path
fill="none"
d="M0 0h24v24H0z" />
<path <path
d="M12 12.586l4.243 4.242-1.415 1.415L13 16.415V22h-2v-5.587l-1.828 1.83-1.415-1.415L12 12.586zM12 2a7.001 7.001 0 0 1 6.954 6.194 5.5 5.5 0 0 1-.953 10.784v-2.014a3.5 3.5 0 1 0-1.112-6.91 5 5 0 1 0-9.777 0 3.5 3.5 0 0 0-1.292 6.88l.18.03v2.014a5.5 5.5 0 0 1-.954-10.784A7 7 0 0 1 12 2z" d="M12 12.586l4.243 4.242-1.415 1.415L13 16.415V22h-2v-5.587l-1.828 1.83-1.415-1.415L12 12.586zM12 2a7.001 7.001 0 0 1 6.954 6.194 5.5 5.5 0 0 1-.953 10.784v-2.014a3.5 3.5 0 1 0-1.112-6.91 5 5 0 1 0-9.777 0 3.5 3.5 0 0 0-1.292 6.88l.18.03v2.014a5.5 5.5 0 0 1-.954-10.784A7 7 0 0 1 12 2z"
fill="rgba(102,102,102,1)" /> fill="rgba(102,102,102,1)" />

Before

Width:  |  Height:  |  Size: 477 B

After

Width:  |  Height:  |  Size: 505 B

@@ -1,4 +1,7 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 512"> <svg id="Layer_1"
data-name="Layer 1"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1024 512">
<path <path
d="M265.71,312.6a9.62,9.62,0,0,1-8.6-5.31l-39.48-79L178.24,306a9.61,9.61,0,0,1-17.14-8.69l48-94.72a9.61,9.61,0,0,1,8.57-5.26h0a9.59,9.59,0,0,1,8.56,5.31l48,96a9.61,9.61,0,0,1-8.58,13.9Z" d="M265.71,312.6a9.62,9.62,0,0,1-8.6-5.31l-39.48-79L178.24,306a9.61,9.61,0,0,1-17.14-8.69l48-94.72a9.61,9.61,0,0,1,8.57-5.26h0a9.59,9.59,0,0,1,8.56,5.31l48,96a9.61,9.61,0,0,1-8.58,13.9Z"
style="fill:#f15a24" /> style="fill:#f15a24" />

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

@@ -1,4 +1,7 @@
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 512"> <svg id="Layer_2"
data-name="Layer 2"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1024 512">
<path <path
d="M750.1,313c-10.07,0-20.35-3-27.91-8.84a9.92,9.92,0,0,1,12.07-15.74c7.36,5.64,19.41,5.92,26,2.65,4.28-2.11,4.28-4.52,4.28-5.42v-.09c-1.2-2.12-9.81-5-14.44-6.52-12.31-4.1-29.16-9.7-30.37-26.62-.84-11.77,5.32-21.85,16.48-27,14.08-6.46,32.2-3.32,43.07,7.47a9.92,9.92,0,1,1-14,14.08c-5.64-5.6-15-6.19-20.82-3.52-5.33,2.44-5.06,6.13-5,7.52.26,3.7,9.45,6.76,16.84,9.22,11.82,3.93,28,9.32,28,25.43,0,9.94-5.6,18.4-15.35,23.21A43.19,43.19,0,0,1,750.1,313Z" d="M750.1,313c-10.07,0-20.35-3-27.91-8.84a9.92,9.92,0,0,1,12.07-15.74c7.36,5.64,19.41,5.92,26,2.65,4.28-2.11,4.28-4.52,4.28-5.42v-.09c-1.2-2.12-9.81-5-14.44-6.52-12.31-4.1-29.16-9.7-30.37-26.62-.84-11.77,5.32-21.85,16.48-27,14.08-6.46,32.2-3.32,43.07,7.47a9.92,9.92,0,1,1-14,14.08c-5.64-5.6-15-6.19-20.82-3.52-5.33,2.44-5.06,6.13-5,7.52.26,3.7,9.45,6.76,16.84,9.22,11.82,3.93,28,9.32,28,25.43,0,9.94-5.6,18.4-15.35,23.21A43.19,43.19,0,0,1,750.1,313Z"
style="fill:#e4007f" /> style="fill:#e4007f" />
@@ -23,7 +26,11 @@
<path <path
d="M443.54,330.55a9.92,9.92,0,0,1-9.92-9.91V269.07a9.92,9.92,0,1,1,19.84,0v51.57A9.92,9.92,0,0,1,443.54,330.55Z" d="M443.54,330.55a9.92,9.92,0,0,1-9.92-9.91V269.07a9.92,9.92,0,1,1,19.84,0v51.57A9.92,9.92,0,0,1,443.54,330.55Z"
style="fill:#e4007f" /> style="fill:#e4007f" />
<circle cx="443.54" cy="347.08" r="9.92" style="fill:#29abe2" /> <circle
cx="443.54"
cy="347.08"
r="9.92"
style="fill:#29abe2" />
<path <path
d="M346.69,314a9.92,9.92,0,0,1-8.88-5.49L298,229l-39.68,78.3a9.92,9.92,0,0,1-17.7-9l48.6-95.87a9.92,9.92,0,0,1,8.84-5.43h0a9.93,9.93,0,0,1,8.85,5.48l48.59,97.19A9.92,9.92,0,0,1,346.69,314Z" d="M346.69,314a9.92,9.92,0,0,1-8.88-5.49L298,229l-39.68,78.3a9.92,9.92,0,0,1-17.7-9l48.6-95.87a9.92,9.92,0,0,1,8.84-5.43h0a9.93,9.93,0,0,1,8.85,5.48l48.59,97.19A9.92,9.92,0,0,1,346.69,314Z"
style="fill:#e4007f" /> style="fill:#e4007f" />

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -1,24 +1,44 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 512"> <svg xmlns="http://www.w3.org/2000/svg"
<path d="M221,240a32,32,0,1,1-32,32" viewBox="0 0 1024 512">
<path
d="M221,240a32,32,0,1,1-32,32"
style="fill:none;stroke:#007ebc;stroke-linecap:round;stroke-miterlimit:10;stroke-width:19px" /> style="fill:none;stroke:#007ebc;stroke-linecap:round;stroke-miterlimit:10;stroke-width:19px" />
<line x1="189" y1="208" x2="189" y2="272" <line
x1="189"
y1="208"
x2="189"
y2="272"
style="fill:none;stroke:#007ebc;stroke-linecap:round;stroke-miterlimit:10;stroke-width:19px" /> style="fill:none;stroke:#007ebc;stroke-linecap:round;stroke-miterlimit:10;stroke-width:19px" />
<path d="M835,208v64a32,32,0,1,1-32-32" <path
d="M835,208v64a32,32,0,1,1-32-32"
style="fill:none;stroke:#007ebc;stroke-linecap:round;stroke-miterlimit:10;stroke-width:19px" /> style="fill:none;stroke:#007ebc;stroke-linecap:round;stroke-miterlimit:10;stroke-width:19px" />
<path d="M574.14,240a32,32,0,1,1-32,32" <path
d="M574.14,240a32,32,0,1,1-32,32"
style="fill:none;stroke:#007ebc;stroke-linecap:round;stroke-miterlimit:10;stroke-width:19px" /> style="fill:none;stroke:#007ebc;stroke-linecap:round;stroke-miterlimit:10;stroke-width:19px" />
<line x1="542.14" y1="208" x2="542.14" y2="272" <line
x1="542.14"
y1="208"
x2="542.14"
y2="272"
style="fill:none;stroke:#007ebc;stroke-linecap:round;stroke-miterlimit:10;stroke-width:19px" /> style="fill:none;stroke:#007ebc;stroke-linecap:round;stroke-miterlimit:10;stroke-width:19px" />
<path d="M289.29,208v64a32,32,0,0,0,32,32" <path
d="M289.29,208v64a32,32,0,0,0,32,32"
style="fill:none;stroke:#007ebc;stroke-linecap:round;stroke-miterlimit:10;stroke-width:19px" /> style="fill:none;stroke:#007ebc;stroke-linecap:round;stroke-miterlimit:10;stroke-width:19px" />
<path d="M643.43,240v32a32,32,0,0,0,32,32" <path
d="M643.43,240v32a32,32,0,0,0,32,32"
style="fill:none;stroke:#007ebc;stroke-linecap:round;stroke-miterlimit:10;stroke-width:19px" /> style="fill:none;stroke:#007ebc;stroke-linecap:round;stroke-miterlimit:10;stroke-width:19px" />
<path d="M711.71,304V272a32,32,0,0,1,32-32" <path
d="M711.71,304V272a32,32,0,0,1,32-32"
style="fill:none;stroke:#007ebc;stroke-linecap:round;stroke-miterlimit:10;stroke-width:19px" /> style="fill:none;stroke:#007ebc;stroke-linecap:round;stroke-miterlimit:10;stroke-width:19px" />
<path d="M412.57,240v32a32,32,0,0,1-64,0V240" <path
d="M412.57,240v32a32,32,0,0,1-64,0V240"
style="fill:none;stroke:#007ebc;stroke-linecap:round;stroke-miterlimit:10;stroke-width:19px" /> style="fill:none;stroke:#007ebc;stroke-linecap:round;stroke-miterlimit:10;stroke-width:19px" />
<path <path
d="M496.93,247.13a32,32,0,1,0-7.48,54.26,32.8,32.8,0,0,0,12.14-9.17A37.69,37.69,0,0,0,510,274.35" d="M496.93,247.13a32,32,0,1,0-7.48,54.26,32.8,32.8,0,0,0,12.14-9.17A37.69,37.69,0,0,0,510,274.35"
style="fill:none;stroke:#007ebc;stroke-linecap:round;stroke-linejoin:round;stroke-width:19px" /> style="fill:none;stroke:#007ebc;stroke-linecap:round;stroke-linejoin:round;stroke-width:19px" />
<circle cx="478.5" cy="271.5" r="9" style="fill:aqua" /> <circle
cx="478.5"
cy="271.5"
r="9"
style="fill:aqua" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -1,4 +1,7 @@
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 512"> <svg id="Layer_2"
data-name="Layer 2"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1024 512">
<path <path
d="M180.44,303.83q-5.4,7.44-10.32,3.24t1.56-10.68a30.77,30.77,0,0,1,8.76-5.76,2.47,2.47,0,0,0,1.56-2.4V268a.53.53,0,0,0-.6-.6h-3.48q-9,0-9-6t9-6h8.28q9,0,9,9v24.24a3.49,3.49,0,0,0,1.68,3.12l1.2.72q3.72,2.4,7.56,4.68,1.56,1,10,2.16a128.88,128.88,0,0,0,17.88,1q25.44,0,34.44-1.2t9.6,4.8q.48,6.12-8.64,6.36l-31.2.84q-4.56.12-9.12.12A88.17,88.17,0,0,1,214,310c-5.44-.89-8.76-1.68-10-2.4Q200,305.27,198,304t-5.88-4.08a5.91,5.91,0,0,0-3.48-1.2Q184.28,298.67,180.44,303.83Zm63.48-78.36q-7.08,0-7.08-6.48a.53.53,0,0,0-.6-.6H210a.53.53,0,0,0-.6.6q0,6.84-7.08,6.84T195.2,219a.53.53,0,0,0-.6-.6H178.88q-9,0-9-5.88t9-5.76H194.6a.53.53,0,0,0,.6-.6q0-5.64,7.08-5.64t7.08,5.64a.53.53,0,0,0,.6.6h26.28a.53.53,0,0,0,.6-.6q0-5.64,7.08-5.64t7.08,5.64a.53.53,0,0,0,.6.6h16.32q9,0,9,5.76t-9,5.88H251.6a.53.53,0,0,0-.6.6Q251,225.47,243.92,225.47Zm-53.28,18.6q-5.76,3.72-7.74.54a21.18,21.18,0,0,0-4.86-5.46q-7-5.52-3.24-10.32t10.92.24a34.36,34.36,0,0,1,8.1,7.62Q196.28,240.23,190.64,244.07Zm46.44,53.16q-6.6,0-6.6-5.4a.53.53,0,0,0-.6-.6h-21q-9,0-9-5t9-5h21a.53.53,0,0,0,.6-.6v-3.36a.53.53,0,0,0-.6-.6h-15.6q-9,0-9-9v-16.2q0-9,9-9h15.6a.53.53,0,0,0,.6-.6v-3.12a.53.53,0,0,0-.6-.6H210.8q-9,0-9-4.8t9-4.68h19.08a.53.53,0,0,0,.6-.6q0-4.68,6.6-4.68t6.6,4.68a.53.53,0,0,0,.6.6H264.8q9,0,9,4.68t-9,4.8H244.28a.53.53,0,0,0-.6.6v3.12a.53.53,0,0,0,.6.6H261q9,0,9,9v16.2q0,9-9,9H244.28a.53.53,0,0,0-.6.6v3.36a.53.53,0,0,0,.6.6h22.2q9,0,9,5t-9,5h-22.2a.53.53,0,0,0-.6.6Q243.68,297.23,237.08,297.23Zm-18.84-42.36a.53.53,0,0,0,.6.6h11a.53.53,0,0,0,.6-.6v-3.48a.53.53,0,0,0-.6-.6h-11a.53.53,0,0,0-.6.6Zm0,12.72a.53.53,0,0,0,.6.6h11a.53.53,0,0,0,.6-.6V264a.53.53,0,0,0-.6-.6h-11a.53.53,0,0,0-.6.6Zm25.44-12.72a.53.53,0,0,0,.6.6H255.8a.53.53,0,0,0,.6-.6v-3.48a.53.53,0,0,0-.6-.6H244.28a.53.53,0,0,0-.6.6Zm0,12.72a.53.53,0,0,0,.6.6H255.8a.53.53,0,0,0,.6-.6V264a.53.53,0,0,0-.6-.6H244.28a.53.53,0,0,0-.6.6Z" d="M180.44,303.83q-5.4,7.44-10.32,3.24t1.56-10.68a30.77,30.77,0,0,1,8.76-5.76,2.47,2.47,0,0,0,1.56-2.4V268a.53.53,0,0,0-.6-.6h-3.48q-9,0-9-6t9-6h8.28q9,0,9,9v24.24a3.49,3.49,0,0,0,1.68,3.12l1.2.72q3.72,2.4,7.56,4.68,1.56,1,10,2.16a128.88,128.88,0,0,0,17.88,1q25.44,0,34.44-1.2t9.6,4.8q.48,6.12-8.64,6.36l-31.2.84q-4.56.12-9.12.12A88.17,88.17,0,0,1,214,310c-5.44-.89-8.76-1.68-10-2.4Q200,305.27,198,304t-5.88-4.08a5.91,5.91,0,0,0-3.48-1.2Q184.28,298.67,180.44,303.83Zm63.48-78.36q-7.08,0-7.08-6.48a.53.53,0,0,0-.6-.6H210a.53.53,0,0,0-.6.6q0,6.84-7.08,6.84T195.2,219a.53.53,0,0,0-.6-.6H178.88q-9,0-9-5.88t9-5.76H194.6a.53.53,0,0,0,.6-.6q0-5.64,7.08-5.64t7.08,5.64a.53.53,0,0,0,.6.6h26.28a.53.53,0,0,0,.6-.6q0-5.64,7.08-5.64t7.08,5.64a.53.53,0,0,0,.6.6h16.32q9,0,9,5.76t-9,5.88H251.6a.53.53,0,0,0-.6.6Q251,225.47,243.92,225.47Zm-53.28,18.6q-5.76,3.72-7.74.54a21.18,21.18,0,0,0-4.86-5.46q-7-5.52-3.24-10.32t10.92.24a34.36,34.36,0,0,1,8.1,7.62Q196.28,240.23,190.64,244.07Zm46.44,53.16q-6.6,0-6.6-5.4a.53.53,0,0,0-.6-.6h-21q-9,0-9-5t9-5h21a.53.53,0,0,0,.6-.6v-3.36a.53.53,0,0,0-.6-.6h-15.6q-9,0-9-9v-16.2q0-9,9-9h15.6a.53.53,0,0,0,.6-.6v-3.12a.53.53,0,0,0-.6-.6H210.8q-9,0-9-4.8t9-4.68h19.08a.53.53,0,0,0,.6-.6q0-4.68,6.6-4.68t6.6,4.68a.53.53,0,0,0,.6.6H264.8q9,0,9,4.68t-9,4.8H244.28a.53.53,0,0,0-.6.6v3.12a.53.53,0,0,0,.6.6H261q9,0,9,9v16.2q0,9-9,9H244.28a.53.53,0,0,0-.6.6v3.36a.53.53,0,0,0,.6.6h22.2q9,0,9,5t-9,5h-22.2a.53.53,0,0,0-.6.6Q243.68,297.23,237.08,297.23Zm-18.84-42.36a.53.53,0,0,0,.6.6h11a.53.53,0,0,0,.6-.6v-3.48a.53.53,0,0,0-.6-.6h-11a.53.53,0,0,0-.6.6Zm0,12.72a.53.53,0,0,0,.6.6h11a.53.53,0,0,0,.6-.6V264a.53.53,0,0,0-.6-.6h-11a.53.53,0,0,0-.6.6Zm25.44-12.72a.53.53,0,0,0,.6.6H255.8a.53.53,0,0,0,.6-.6v-3.48a.53.53,0,0,0-.6-.6H244.28a.53.53,0,0,0-.6.6Zm0,12.72a.53.53,0,0,0,.6.6H255.8a.53.53,0,0,0,.6-.6V264a.53.53,0,0,0-.6-.6H244.28a.53.53,0,0,0-.6.6Z"
style="fill:#e4007f" /> style="fill:#e4007f" />

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

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