diff --git a/README.md b/README.md
index d14a417c6..1e1e85f95 100644
--- a/README.md
+++ b/README.md
@@ -21,4 +21,10 @@ replace default "Embedded JDK" with download jdk by `File->Project Structrue...-
## Layout Inspector not working
-开发者选项 - 启用视图属性检查功能
\ No newline at end of file
+开发者选项 - 启用视图属性检查功能
+
+## 开发辅助文档
+
+-
+-
+-
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index fa562068f..6ff5ddac2 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -5,10 +5,12 @@ import java.util.Locale
plugins {
id("com.android.application")
id("kotlin-parcelize")
- kotlin("android")
- kotlin("plugin.serialization")
+ id("org.jetbrains.kotlin.android")
+ id("org.jetbrains.kotlin.plugin.serialization")
+ id("org.jetbrains.kotlin.kapt")
id("com.google.devtools.ksp")
id("dev.rikka.tools.refine")
+ id("com.google.dagger.hilt.android")
}
@@ -113,6 +115,9 @@ android {
}
}
+kapt {
+ correctErrorTypes = true
+}
dependencies {
@@ -132,7 +137,6 @@ dependencies {
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso)
-
compileOnly(project(mapOf("path" to ":hidden_api")))
implementation(libs.rikka.shizuku.api)
implementation(libs.rikka.shizuku.provider)
@@ -172,5 +176,7 @@ dependencies {
implementation(libs.destinations.animations)
ksp(libs.destinations.ksp)
-
+ implementation(libs.google.hilt.android)
+ kapt(libs.google.hilt.android.compiler)
+ implementation(libs.androidx.hilt.navigation.compose)
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ec63e29ef..05aab1d23 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -51,21 +51,37 @@
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
diff --git a/app/src/main/java/li/songe/gkd/App.kt b/app/src/main/java/li/songe/gkd/App.kt
index ae64c8bab..339c781ea 100644
--- a/app/src/main/java/li/songe/gkd/App.kt
+++ b/app/src/main/java/li/songe/gkd/App.kt
@@ -3,18 +3,29 @@ package li.songe.gkd
import android.app.Application
import android.content.Context
import android.os.Build
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Add
import com.blankj.utilcode.util.LogUtils
import com.tencent.bugly.crashreport.CrashReport
import com.tencent.mmkv.MMKV
-import li.songe.gkd.utils.Storage
+import dagger.hilt.android.HiltAndroidApp
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
+import li.songe.gkd.data.getAppInfo
+import li.songe.gkd.db.DbSet
+import li.songe.gkd.util.isMainProcess
import org.lsposed.hiddenapibypass.HiddenApiBypass
import rikka.shizuku.ShizukuProvider
+lateinit var app: Application
+var appScope = MainScope()
+
+@HiltAndroidApp
class App : Application() {
- companion object {
- lateinit var context: Application
+ override fun onLowMemory() {
+ super.onLowMemory()
+ appScope.cancel("onLowMemory() called by system")
+ appScope = MainScope()
}
override fun attachBaseContext(base: Context?) {
@@ -26,18 +37,22 @@ class App : Application() {
override fun onCreate() {
super.onCreate()
- context = this
+ app = this
MMKV.initialize(this)
- LogUtils.d(Storage.settings)
- if (!Storage.settings.enableConsoleLogOut) {
- LogUtils.d("关闭日志控制台输出")
- }
LogUtils.getConfig().apply {
isLog2FileSwitch = true
- saveDays = 30
- LogUtils.getConfig().setConsoleSwitch(Storage.settings.enableConsoleLogOut)
+ saveDays = 7
}
ShizukuProvider.enableMultiProcessSupport(true)
CrashReport.initCrashReport(applicationContext, "d0ce46b353", false)
+
+ if (isMainProcess) {
+ appScope.launch(Dispatchers.IO) {
+// 提前获取 appInfo 缓存
+ DbSet.subsItemDao.query().collect {
+ it.forEach { s -> s.subscriptionRaw?.apps?.forEach { app -> getAppInfo(app.id) } }
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/MainActivity.kt b/app/src/main/java/li/songe/gkd/MainActivity.kt
index 9addac938..46d11b382 100644
--- a/app/src/main/java/li/songe/gkd/MainActivity.kt
+++ b/app/src/main/java/li/songe/gkd/MainActivity.kt
@@ -3,81 +3,87 @@ package li.songe.gkd
import android.os.Build
import android.view.WindowManager
import androidx.activity.compose.setContent
-import androidx.compose.material.icons.materialIcon
-import androidx.compose.material.icons.materialPath
import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.Modifier
import androidx.navigation.compose.rememberNavController
-import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
import com.dylanc.activityresult.launcher.StartActivityLauncher
import com.ramcosta.composedestinations.DestinationsNavHost
+import dagger.hilt.android.AndroidEntryPoint
import li.songe.gkd.composition.CompositionActivity
import li.songe.gkd.composition.CompositionExt.useLifeCycleLog
import li.songe.gkd.ui.NavGraphs
import li.songe.gkd.ui.theme.AppTheme
-import li.songe.gkd.utils.LocalLauncher
-import li.songe.gkd.utils.LocalNavController
-import li.songe.gkd.utils.StackCacheProvider
-import li.songe.gkd.utils.Storage
-
+import li.songe.gkd.util.LocalLauncher
+import li.songe.gkd.util.LocalNavController
+import li.songe.gkd.util.storeFlow
+@AndroidEntryPoint
class MainActivity : CompositionActivity({
useLifeCycleLog()
val launcher = StartActivityLauncher(this)
onFinish { fs ->
- if (Storage.settings.excludeFromRecents) {
+ if (storeFlow.value.excludeFromRecents) {
finishAndRemoveTask() // 会让miui桌面回退动画失效
} else {
fs()
}
}
-// https://juejin.cn/post/7169147194400833572
+ // https://juejin.cn/post/7169147194400833572
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
window.attributes.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
}
-// TextView[a==1||b==1||a==1||(a==1&&b==true)]
-// lifecycleScope.launchTry {
-// delay(1000)
-// WindowCompat.setDecorFitsSystemWindows(window, false)
-// val insetsController = WindowCompat.getInsetsController(window, window.decorView)
-// insetsController.hide(WindowInsetsCompat.Type.statusBars())
-// }
-// var shizukuIsOK = false
-// val receivedListener: () -> Unit = {
-// shizukuIsOK = true
-// }
-// Shizuku.addBinderReceivedListenerSticky(receivedListener)
-// onDestroy {
-// Shizuku.removeBinderReceivedListener(receivedListener)
-// }
-// lifecycleScope.launchWhile {
-// if (shizukuIsOK) {
-// val top = activityTaskManager.getTasks(1, false, true)?.firstOrNull()
-// if (top!=null) {
-// LogUtils.d(top.topActivity?.packageName, top.topActivity?.className, top.topActivity?.shortClassName)
+ // lifecycleScope.launchTry {
+ // delay(1000)
+ // WindowCompat.setDecorFitsSystemWindows(window, false)
+ // val insetsController = WindowCompat.getInsetsController(window, window.decorView)
+ // insetsController.hide(WindowInsetsCompat.Type.statusBars())
+ // }
+
+ // var shizukuIsOK = false
+ // val receivedListener: () -> Unit = {
+ // shizukuIsOK = true
+ // }
+ // Shizuku.addBinderReceivedListenerSticky(receivedListener)
+ // onDestroy {
+ // Shizuku.removeBinderReceivedListener(receivedListener)
+ // }
+ // lifecycleScope.launchWhile {
+ // if (shizukuIsOK) {
+ // val top = activityTaskManager.getTasks(1, false, true)?.firstOrNull()
+ // if (top!=null) {
+ // LogUtils.d(top.topActivity?.packageName, top.topActivity?.className, top.topActivity?.shortClassName)
+ // }
+ // }
+ // delay(5000)
+ // }
+
+// lifecycleScope.launchTry(IO) {
+// File("/sdcard/Android/data/${packageName}/files/snapshot").walk().maxDepth(1)
+// .filter { it.isDirectory && !it.name.endsWith("snapshot") }.forEach { folder ->
+// val snapshot = Singleton.json.decodeFromString(File(folder.absolutePath + "/${folder.name}.json").readText())
+// try {
+// DbSet.snapshotDao.insert(snapshot)
+// }catch (e:Exception){
+// e.printStackTrace()
+// LogUtils.d("insert failed, ${snapshot.id}")
+// return@launchTry
+// }
// }
-// }
-// delay(5000)
// }
-
setContent {
val navController = rememberNavController()
AppTheme(false) {
CompositionLocalProvider(
- LocalLauncher provides launcher,
- LocalNavController provides navController
+ LocalLauncher provides launcher, LocalNavController provides navController
) {
- StackCacheProvider(navController = navController) {
- DestinationsNavHost(
- navGraph = NavGraphs.root,
- navController = navController,
- )
- }
+ DestinationsNavHost(
+ navGraph = NavGraphs.root, navController = navController, modifier = Modifier
+ )
}
}
}
diff --git a/app/src/main/java/li/songe/gkd/accessibility/KeepAliveService.kt b/app/src/main/java/li/songe/gkd/accessibility/KeepAliveService.kt
deleted file mode 100644
index 9ce80aa48..000000000
--- a/app/src/main/java/li/songe/gkd/accessibility/KeepAliveService.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package li.songe.gkd.accessibility
-
-import android.content.Context
-import android.content.Intent
-import kotlinx.coroutines.delay
-import li.songe.gkd.App
-import li.songe.gkd.composition.CompositionService
-import li.songe.gkd.composition.CompositionExt.useScope
-import li.songe.gkd.utils.launchWhile
-import li.songe.gkd.utils.Ext.createNotificationChannel
-
-class KeepAliveService : CompositionService({
- createNotificationChannel(this)
- val scope = useScope()
- scope.launchWhile {
- delay(3_000)
- }
-}) {
- companion object {
- fun start(context: Context = App.context) {
- context.startForegroundService(Intent(context, KeepAliveService::class.java))
- }
-
- fun stop(context: Context = App.context) {
- context.stopService(Intent(context, KeepAliveService::class.java))
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/composition/CompositionExt.kt b/app/src/main/java/li/songe/gkd/composition/CompositionExt.kt
index 0ed6486b4..cdef1f08e 100644
--- a/app/src/main/java/li/songe/gkd/composition/CompositionExt.kt
+++ b/app/src/main/java/li/songe/gkd/composition/CompositionExt.kt
@@ -12,7 +12,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
-import li.songe.gkd.utils.Singleton
+import li.songe.gkd.util.Singleton
import kotlin.coroutines.CoroutineContext
object CompositionExt {
diff --git a/app/src/main/java/li/songe/gkd/data/AppInfo.kt b/app/src/main/java/li/songe/gkd/data/AppInfo.kt
index e0a48a29d..38b0b310c 100644
--- a/app/src/main/java/li/songe/gkd/data/AppInfo.kt
+++ b/app/src/main/java/li/songe/gkd/data/AppInfo.kt
@@ -2,24 +2,25 @@ package li.songe.gkd.data
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
-import li.songe.gkd.App
-import li.songe.gkd.utils.Ext.getApplicationInfoExt
+import li.songe.gkd.app
+import li.songe.gkd.util.Ext.getApplicationInfoExt
data class AppInfo(
val id: String,
val name: String? = null,
val icon: Drawable? = null,
- val installed: Boolean = true
-)
+ val installed: Boolean = true,
+) {
+ val realName = if (name.isNullOrBlank()) id else name
+}
private val appInfoCache = mutableMapOf()
fun getAppInfo(id: String): AppInfo {
appInfoCache[id]?.let { return it }
- val packageManager = App.context.packageManager
- val info = try {
-// 需要权限
- val rawInfo = App.context.packageManager.getApplicationInfoExt(
+ val packageManager = app.packageManager
+ val info = try { // 需要权限
+ val rawInfo = app.packageManager.getApplicationInfoExt(
id, PackageManager.GET_META_DATA
)
AppInfo(
@@ -32,4 +33,9 @@ fun getAppInfo(id: String): AppInfo {
}
appInfoCache[id] = info
return info
+}
+
+fun getAppName(id: String?): String? {
+ id ?: return null
+ return getAppInfo(id).name
}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/data/AttrInfo.kt b/app/src/main/java/li/songe/gkd/data/AttrInfo.kt
index 55808d141..f84f04664 100644
--- a/app/src/main/java/li/songe/gkd/data/AttrInfo.kt
+++ b/app/src/main/java/li/songe/gkd/data/AttrInfo.kt
@@ -3,25 +3,56 @@ package li.songe.gkd.data
import android.graphics.Rect
import android.view.accessibility.AccessibilityNodeInfo
import kotlinx.serialization.Serializable
-import li.songe.gkd.accessibility.getDepth
-import li.songe.gkd.accessibility.getIndex
+import li.songe.gkd.service.getDepth
+import li.songe.gkd.service.getIndex
@Serializable
data class AttrInfo(
- val id: String? = null,
- val name: String? = null,
- val text: String? = null,
+ val id: String?,
+ val name: String?,
+ val text: String?,
val textLen: Int? = text?.length,
- val desc: String? = null,
+ val desc: String?,
val descLen: Int? = desc?.length,
- val isClickable: Boolean = false,
- val childCount: Int = 0,
- val index: Int = 0,
- val depth: Int = 0,
+ val hint: String?,
+ val hintLen: Int? = hint?.length,
+ val error: String?,
+ val errorLen: Int? = error?.length,
+ val inputType: Int?,
+ val liveRegion: Int?,
+
+ val enabled: Boolean,
+ val clickable: Boolean,
+ val checked: Boolean,
+ val checkable: Boolean,
+ val focused: Boolean,
+ val focusable: Boolean,
+ val visibleToUser: Boolean,
+ val selected: Boolean,
+ val longClickable: Boolean,
+ val password: Boolean,
+ val scrollable: Boolean,
+ val accessibilityFocused: Boolean,
+ val editable: Boolean,
+ val canOpenPopup: Boolean,
+ val dismissable: Boolean,
+ val multiLine: Boolean,
+ val contentInvalid: Boolean,
+ val contextClickable: Boolean,
+ val importance: Boolean,
+ val showingHintText: Boolean,
+
val left: Int,
val top: Int,
val right: Int,
val bottom: Int,
+
+ val width: Int,
+ val height: Int,
+
+ val index: Int,
+ val depth: Int,
+ val childCount: Int,
) {
companion object {
/**
@@ -29,22 +60,51 @@ data class AttrInfo(
*/
private val rect = Rect()
fun info2data(
- nodeInfo: AccessibilityNodeInfo,
+ node: AccessibilityNodeInfo,
): AttrInfo {
- nodeInfo.getBoundsInScreen(rect)
+ node.getBoundsInScreen(rect)
return AttrInfo(
- id = nodeInfo.viewIdResourceName,
- name = nodeInfo.className?.toString(),
- text = nodeInfo.text?.toString(),
- desc = nodeInfo.contentDescription?.toString(),
- isClickable = nodeInfo.isClickable,
- childCount = nodeInfo.childCount,
- index = nodeInfo.getIndex(),
- depth = nodeInfo.getDepth(),
+ id = node.viewIdResourceName,
+ name = node.className?.toString(),
+ text = node.text?.toString(),
+ desc = node.contentDescription?.toString(),
+ hint = node.hintText?.toString(),
+ error = node.error?.toString(),
+ inputType = node.inputType,
+ liveRegion = node.liveRegion,
+
+ enabled = node.isEnabled,
+ clickable = node.isClickable,
+ checked = node.isChecked,
+ checkable = node.isCheckable,
+ focused = node.isFocused,
+ focusable = node.isFocusable,
+ visibleToUser = node.isVisibleToUser,
+ selected = node.isSelected,
+ longClickable = node.isLongClickable,
+ password = node.isPassword,
+ scrollable = node.isScrollable,
+ accessibilityFocused = node.isAccessibilityFocused,
+ editable = node.isEditable,
+ canOpenPopup = node.canOpenPopup(),
+ dismissable = node.isDismissable,
+ multiLine = node.isMultiLine,
+ contentInvalid = node.isContentInvalid,
+ contextClickable = node.isContextClickable,
+ importance = node.isImportantForAccessibility,
+ showingHintText = node.isShowingHintText,
+
left = rect.left,
top = rect.top,
right = rect.right,
bottom = rect.bottom,
+
+ width = rect.width(),
+ height = rect.height(),
+
+ index = node.getIndex(),
+ depth = node.getDepth(),
+ childCount = node.childCount,
)
}
}
diff --git a/app/src/main/java/li/songe/gkd/data/NodeInfo.kt b/app/src/main/java/li/songe/gkd/data/NodeInfo.kt
index 9418ddb8c..bb12145e4 100644
--- a/app/src/main/java/li/songe/gkd/data/NodeInfo.kt
+++ b/app/src/main/java/li/songe/gkd/data/NodeInfo.kt
@@ -2,7 +2,7 @@ package li.songe.gkd.data
import android.view.accessibility.AccessibilityNodeInfo
import kotlinx.serialization.Serializable
-import li.songe.gkd.accessibility.forEachIndexed
+import li.songe.gkd.service.forEachIndexed
import java.util.ArrayDeque
@Serializable
diff --git a/app/src/main/java/li/songe/gkd/data/RpcError.kt b/app/src/main/java/li/songe/gkd/data/RpcError.kt
index e83492ba1..798ceaf86 100644
--- a/app/src/main/java/li/songe/gkd/data/RpcError.kt
+++ b/app/src/main/java/li/songe/gkd/data/RpcError.kt
@@ -1,16 +1,10 @@
package li.songe.gkd.data
+import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class RpcError(
- override val message: String = "unknown error",
- val code: Int = 0,
- val X_Rpc_Result:String = "error"
-) : Exception(message) {
- companion object {
- const val HeaderKey = "X_Rpc_Result"
- const val HeaderOkValue = "ok"
- const val HeaderErrorValue = "error"
- }
-}
+ override val message: String,
+ @SerialName("__error") val error: Boolean = true,
+) : Exception(message)
diff --git a/app/src/main/java/li/songe/gkd/data/Rule.kt b/app/src/main/java/li/songe/gkd/data/Rule.kt
index a86ff8fdc..8eeab2060 100644
--- a/app/src/main/java/li/songe/gkd/data/Rule.kt
+++ b/app/src/main/java/li/songe/gkd/data/Rule.kt
@@ -1,7 +1,7 @@
package li.songe.gkd.data
import android.view.accessibility.AccessibilityNodeInfo
-import li.songe.gkd.accessibility.querySelector
+import li.songe.gkd.service.querySelector
import li.songe.selector.Selector
data class Rule(
@@ -21,6 +21,8 @@ data class Rule(
val excludeActivityIds: Set = emptySet(),
val key: Int? = null,
val preKeys: Set = emptySet(),
+ val group: SubscriptionRaw.GroupRaw,
+ val subsItem: SubsItem,
) {
private var triggerTime = 0L
fun trigger() {
diff --git a/app/src/main/java/li/songe/gkd/data/RuleManager.kt b/app/src/main/java/li/songe/gkd/data/RuleManager.kt
index ccf6e305c..159cac42e 100644
--- a/app/src/main/java/li/songe/gkd/data/RuleManager.kt
+++ b/app/src/main/java/li/songe/gkd/data/RuleManager.kt
@@ -2,26 +2,63 @@ package li.songe.gkd.data
import li.songe.selector.Selector
-class RuleManager(vararg subscriptionRawArray: SubscriptionRaw) {
+class RuleManager(subsItems: List = listOf()) {
private data class TriggerRecord(val ctime: Long = System.currentTimeMillis(), val rule: Rule)
- private var count: Int = 0
- get() {
- field++
- return field
- }
private val appToRulesMap = mutableMapOf>()
+
+ private val triggerLogQueue = ArrayDeque()
+
+
+ fun trigger(rule: Rule) {
+ rule.trigger()
+ triggerLogQueue.addLast(TriggerRecord(rule = rule))
+ while (triggerLogQueue.size >= 256) {
+ triggerLogQueue.removeFirst()
+ }
+ }
+
+
+ fun match(appId: String? = null, activityId: String? = null) = sequence {
+ if (appId == null) return@sequence
+ val rules = appToRulesMap[appId] ?: return@sequence
+ if (activityId == null) {
+ yieldAll(rules)
+ return@sequence
+ }
+ rules.forEach { rule ->
+ if (rule.excludeActivityIds.any { activityId.startsWith(it) }) return@forEach // 是被排除的 界面 id
+
+ if (rule.matchAnyActivity || rule.activityIds.any { activityId.startsWith(it) } // 在匹配列表
+ ) {
+ yield(rule)
+ }
+ }
+ }
+
+ fun ruleIsAvailable(rule: Rule): Boolean {
+ if (!rule.active) return false // 处于冷却时间
+ if (rule.preKeys.isNotEmpty()) { // 需要提前触发某个规则
+ if (rule.preRules.isEmpty()) return false // 声明了 preKeys 但是没有在当前列表找到
+ val record = triggerLogQueue.lastOrNull() ?: return false
+ if (!rule.preRules.any { it == record.rule }) return false // 上一个触发的规则不在当前需要触发的列表
+ }
+ return true
+ }
+
+
init {
- subscriptionRawArray.forEach { subscriptionRaw ->
+ subsItems.filter { s -> s.enable }.forEach { subsItem ->
+ val subscriptionRaw = subsItem.subscriptionRaw ?: return@forEach
subscriptionRaw.apps.forEach { appRaw ->
val ruleConfigList = appToRulesMap[appRaw.id] ?: mutableListOf()
appToRulesMap[appRaw.id] = ruleConfigList
appRaw.groups.forEach { groupRaw ->
val ruleGroupList = mutableListOf()
- groupRaw.rules.forEach ruleEach@{ ruleRaw ->
+ groupRaw.rules.forEachIndexed ruleEach@{ ruleIndex, ruleRaw ->
if (ruleRaw.matches.isEmpty()) return@ruleEach
val cd = Rule.defaultMiniCd.coerceAtLeast(
ruleRaw.cd ?: groupRaw.cd ?: appRaw.cd ?: Rule.defaultMiniCd
@@ -29,8 +66,7 @@ class RuleManager(vararg subscriptionRawArray: SubscriptionRaw) {
val activityIds =
(ruleRaw.activityIds ?: groupRaw.activityIds ?: appRaw.activityIds
?: listOf("*")).map { activityId ->
- if (activityId.startsWith('.')) {
-// .a.b.c -> com.x.y.x.a.b.c
+ if (activityId.startsWith('.')) { // .a.b.c -> com.x.y.x.a.b.c
return@map appRaw.id + activityId
}
activityId
@@ -39,14 +75,12 @@ class RuleManager(vararg subscriptionRawArray: SubscriptionRaw) {
val excludeActivityIds =
(ruleRaw.excludeActivityIds ?: groupRaw.excludeActivityIds
- ?: appRaw.excludeActivityIds
- ?: emptyList()).toSet()
-
+ ?: appRaw.excludeActivityIds ?: emptyList()).toSet()
ruleGroupList.add(
Rule(
cd = cd,
- index = count,
+ index = ruleIndex,
matches = ruleRaw.matches.map { Selector.parse(it) },
excludeMatches = ruleRaw.excludeMatches.map {
Selector.parse(
@@ -58,13 +92,15 @@ class RuleManager(vararg subscriptionRawArray: SubscriptionRaw) {
excludeActivityIds = excludeActivityIds,
key = ruleRaw.key,
preKeys = ruleRaw.preKeys.toSet(),
+ group = groupRaw,
+ subsItem = subsItem
)
)
}
ruleGroupList.forEachIndexed { index, ruleConfig ->
ruleGroupList[index] = ruleConfig.copy(
preRules = ruleGroupList.filter {
- it.key != null && it.preKeys.contains(
+ (it.key != null) && it.preKeys.contains(
it.key
)
}.toSet()
@@ -74,45 +110,6 @@ class RuleManager(vararg subscriptionRawArray: SubscriptionRaw) {
}
}
}
- }
-
-
- private val triggerLogQueue = ArrayDeque()
-
- fun trigger(rule: Rule) {
- rule.trigger()
- triggerLogQueue.addLast(TriggerRecord(rule = rule))
- while (triggerLogQueue.size >= 256) {
- triggerLogQueue.removeFirst()
- }
- }
-
-
- fun match(appId: String? = null, activityId: String? = null) = sequence {
- if (appId == null) return@sequence
- val rules = appToRulesMap[appId] ?: return@sequence
- if (activityId == null) {
- yieldAll(rules)
- return@sequence
- }
- rules.forEach { rule ->
- if (rule.excludeActivityIds.any { activityId.startsWith(it) }) return@forEach // 是被排除的 界面 id
-
- if (rule.matchAnyActivity || rule.activityIds.any { activityId.startsWith(it) } // 在匹配列表
- ) {
- yield(rule)
- }
- }
- }
-
- fun ruleIsAvailable(rule: Rule): Boolean {
- if (!rule.active) return false // 处于冷却时间
- if (rule.preKeys.isNotEmpty()) { // 需要提前触发某个规则
- if (rule.preRules.isEmpty()) return false // 声明了 preKeys 但是没有在当前列表找到
- val record = triggerLogQueue.lastOrNull() ?: return false
- if (!rule.preRules.any { it == record.rule }) return false // 上一个触发的规则不在当前需要触发的列表
- }
- return true
}
}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/data/Snapshot.kt b/app/src/main/java/li/songe/gkd/data/Snapshot.kt
index 089b087a5..6303ebe1e 100644
--- a/app/src/main/java/li/songe/gkd/data/Snapshot.kt
+++ b/app/src/main/java/li/songe/gkd/data/Snapshot.kt
@@ -13,9 +13,10 @@ import com.blankj.utilcode.util.AppUtils
import com.blankj.utilcode.util.ScreenUtils
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.Serializable
-import li.songe.gkd.accessibility.GkdAbService
+import li.songe.gkd.service.GkdAbService
import li.songe.gkd.db.IgnoreConverters
-import li.songe.gkd.utils.Ext
+import li.songe.gkd.debug.SnapshotExt
+import java.io.File
@TypeConverters(IgnoreConverters::class)
@Entity(
@@ -26,7 +27,7 @@ data class Snapshot(
@PrimaryKey @ColumnInfo(name = "id") val id: Long = System.currentTimeMillis(),
@ColumnInfo(name = "app_id") val appId: String? = null,
@ColumnInfo(name = "activity_id") val activityId: String? = null,
- @ColumnInfo(name = "app_name") val appName: String? = Ext.getAppName(appId),
+ @ColumnInfo(name = "app_name") val appName: String? = getAppName(appId),
@ColumnInfo(name = "app_version_code") val appVersionCode: Int? = appId?.let {
AppUtils.getAppVersionCode(
appId
@@ -40,6 +41,7 @@ data class Snapshot(
@ColumnInfo(name = "screen_height") val screenHeight: Int = ScreenUtils.getScreenHeight(),
@ColumnInfo(name = "screen_width") val screenWidth: Int = ScreenUtils.getScreenWidth(),
+
@ColumnInfo(name = "is_landscape") val isLandscape: Boolean = ScreenUtils.isLandscape(),
@ColumnInfo(name = "device") val device: String = DeviceInfo.instance.device,
@@ -51,8 +53,19 @@ data class Snapshot(
@ColumnInfo(name = "_1") val nodes: List = emptyList(),
) {
+
+ val screenshotFile by lazy {
+ File(
+ SnapshotExt.getScreenshotPath(
+ id
+ )
+ )
+ }
+
companion object {
- fun current(includeNode: Boolean = true): Snapshot {
+ fun current(
+ includeNode: Boolean = true,
+ ): Snapshot {
val currentAbNode = GkdAbService.currentAbNode
val appId = currentAbNode?.packageName?.toString()
val currentActivityId = GkdAbService.currentActivityId
diff --git a/app/src/main/java/li/songe/gkd/data/SubsConfig.kt b/app/src/main/java/li/songe/gkd/data/SubsConfig.kt
index 4994a5f18..38dee8e32 100644
--- a/app/src/main/java/li/songe/gkd/data/SubsConfig.kt
+++ b/app/src/main/java/li/songe/gkd/data/SubsConfig.kt
@@ -22,7 +22,7 @@ data class SubsConfig(
@ColumnInfo(name = "type") val type: Int = SubsType,
@ColumnInfo(name = "enable") val enable: Boolean = true,
- @ColumnInfo(name = "subs_item_id") val subsItemId: Long = -1,
+ @ColumnInfo(name = "subs_item_id") val subsItemId: Long ,
@ColumnInfo(name = "app_id") val appId: String = "",
@ColumnInfo(name = "group_key") val groupKey: Int = -1,
) : Parcelable {
@@ -58,7 +58,7 @@ data class SubsConfig(
fun queryAppTypeConfig(subsItemId: Long): Flow>
@Query("SELECT * FROM subs_config WHERE type=${GroupType} and subs_item_id=:subsItemId and app_id=:appId")
- suspend fun queryGroupTypeConfig(subsItemId: Long, appId: String): List
+ fun queryGroupTypeConfig(subsItemId: Long, appId: String): Flow>
}
}
diff --git a/app/src/main/java/li/songe/gkd/data/SubsItem.kt b/app/src/main/java/li/songe/gkd/data/SubsItem.kt
index 53f3c5ab2..c15374f22 100644
--- a/app/src/main/java/li/songe/gkd/data/SubsItem.kt
+++ b/app/src/main/java/li/songe/gkd/data/SubsItem.kt
@@ -15,7 +15,7 @@ import kotlinx.coroutines.withContext
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import li.songe.gkd.db.DbSet
-import li.songe.gkd.utils.FolderExt
+import li.songe.gkd.util.FolderExt
import java.io.File
@Entity(
@@ -27,12 +27,12 @@ data class SubsItem(
@ColumnInfo(name = "mtime") val mtime: Long = System.currentTimeMillis(),
@ColumnInfo(name = "enable") val enable: Boolean = true,
@ColumnInfo(name = "enable_update") val enableUpdate: Boolean = true,
- @ColumnInfo(name = "order") val order: Int = 0,
+ @ColumnInfo(name = "order") val order: Int = 1,
-// 订阅文件的根字段
+ // 订阅文件的根字段
@ColumnInfo(name = "name") val name: String = "",
@ColumnInfo(name = "author") val author: String = "",
- @ColumnInfo(name = "version") val version: Int = 0,
+ @ColumnInfo(name = "version") val version: Int = 1,
@ColumnInfo(name = "update_url") val updateUrl: String = "",
@ColumnInfo(name = "support_url") val supportUrl: String = "",
@@ -61,6 +61,17 @@ data class SubsItem(
DbSet.subsConfigDao.deleteSubs(id)
}
+ companion object {
+ fun getSubscriptionRaw(subsItemId: Long): SubscriptionRaw? {
+ return try {
+ SubscriptionRaw.parse5(File(FolderExt.subsFolder.absolutePath.plus("/${subsItemId}.json")).readText())
+ } catch (e: Exception) {
+ e.printStackTrace()
+ null
+ }
+ }
+ }
+
@Dao
interface SubsItemDao {
@@ -75,5 +86,10 @@ data class SubsItem(
@Query("SELECT * FROM subs_item ORDER BY `order`")
fun query(): Flow>
+
+ @Query("SELECT * FROM subs_item WHERE id=:id")
+ fun queryById(id: Long): SubsItem?
}
+
+
}
diff --git a/app/src/main/java/li/songe/gkd/data/SubscriptionRaw.kt b/app/src/main/java/li/songe/gkd/data/SubscriptionRaw.kt
index 27d6310b5..1a1ae9c92 100644
--- a/app/src/main/java/li/songe/gkd/data/SubscriptionRaw.kt
+++ b/app/src/main/java/li/songe/gkd/data/SubscriptionRaw.kt
@@ -6,7 +6,7 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.*
-import li.songe.gkd.utils.Singleton
+import li.songe.gkd.util.Singleton
import li.songe.selector.Selector
@@ -36,7 +36,7 @@ data class SubscriptionRaw(
data class GroupRaw(
@SerialName("name") val name: String? = null,
@SerialName("desc") val desc: String? = null,
- @SerialName("key") val key: Int? = null,
+ @SerialName("key") val key: Int,
@SerialName("cd") val cd: Long? = null,
@SerialName("activityIds") val activityIds: List? = null,
@SerialName("excludeActivityIds") val excludeActivityIds: List? = null,
@@ -147,7 +147,7 @@ data class SubscriptionRaw(
}
- private fun jsonToGroupRaw(groupsRawJson: JsonElement): GroupRaw {
+ private fun jsonToGroupRaw(groupIndex: Int, groupsRawJson: JsonElement): GroupRaw {
val groupsJson = when (groupsRawJson) {
JsonNull -> error("")
is JsonObject -> groupsRawJson
@@ -158,7 +158,7 @@ data class SubscriptionRaw(
cd = getLong(groupsJson, "cd"),
name = getString(groupsJson, "name"),
desc = getString(groupsJson, "desc"),
- key = getInt(groupsJson, "key"),
+ key = getInt(groupsJson, "key") ?: groupIndex,
rules = when (val rulesJson = groupsJson["rules"]) {
null, JsonNull -> emptyList()
is JsonPrimitive, is JsonObject -> JsonArray(listOf(rulesJson))
@@ -177,8 +177,8 @@ data class SubscriptionRaw(
null, JsonNull -> emptyList()
is JsonPrimitive, is JsonObject -> JsonArray(listOf(groupsJson))
is JsonArray -> groupsJson
- }).map {
- jsonToGroupRaw(it)
+ }).mapIndexed { index, jsonElement ->
+ jsonToGroupRaw(index, jsonElement)
})
}
diff --git a/app/src/main/java/li/songe/gkd/data/TriggerLog.kt b/app/src/main/java/li/songe/gkd/data/TriggerLog.kt
index ff290923c..8f1c1e067 100644
--- a/app/src/main/java/li/songe/gkd/data/TriggerLog.kt
+++ b/app/src/main/java/li/songe/gkd/data/TriggerLog.kt
@@ -9,33 +9,22 @@ import androidx.room.Insert
import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.room.Update
+import kotlinx.coroutines.flow.Flow
import kotlinx.parcelize.Parcelize
-import java.nio.channels.Selector
@Entity(
tableName = "trigger_log",
)
@Parcelize
data class TriggerLog(
- /**
- * 此 id 与某个 snapshot id 一致, 表示 one to one
- */
- @PrimaryKey @ColumnInfo(name = "id") val id: Long,
- /**
- * 订阅文件 id
- */
+ @PrimaryKey @ColumnInfo(name = "id") val id: Long = System.currentTimeMillis(),
+ @ColumnInfo(name = "app_id") val appId: String? = null,
+ @ColumnInfo(name = "activity_id") val activityId: String? = null,
@ColumnInfo(name = "subs_id") val subsId: Long,
- /**
- * 触发的组 id
- */
@ColumnInfo(name = "group_key") val groupKey: Int,
-
- /**
- * 触发的选择器
- */
- @ColumnInfo(name = "match") val match: String,
-
- ) : Parcelable {
+ @ColumnInfo(name = "rule_index") val ruleIndex: Int,
+ @ColumnInfo(name = "rule_key") val ruleKey: Int? = null,
+) : Parcelable {
@Dao
interface TriggerLogDao {
@@ -43,12 +32,15 @@ data class TriggerLog(
suspend fun update(vararg objects: TriggerLog): Int
@Insert
- suspend fun insert(vararg users: TriggerLog): List
+ suspend fun insert(vararg objects: TriggerLog): List
@Delete
- suspend fun delete(vararg users: TriggerLog): Int
+ suspend fun delete(vararg objects: TriggerLog): Int
+
+ @Query("SELECT * FROM trigger_log ORDER BY id DESC")
+ fun query(): Flow>
- @Query("SELECT * FROM trigger_log")
- suspend fun query(): List
+ @Query("SELECT COUNT(*) FROM trigger_log")
+ fun count(): Flow
}
}
diff --git a/app/src/main/java/li/songe/gkd/data/Value.kt b/app/src/main/java/li/songe/gkd/data/Value.kt
deleted file mode 100644
index 8bf6fcd23..000000000
--- a/app/src/main/java/li/songe/gkd/data/Value.kt
+++ /dev/null
@@ -1,3 +0,0 @@
-package li.songe.gkd.data
-
-data class Value(var value: T)
diff --git a/app/src/main/java/li/songe/gkd/db/DbSet.kt b/app/src/main/java/li/songe/gkd/db/DbSet.kt
index 835324d9b..6e8f7ec80 100644
--- a/app/src/main/java/li/songe/gkd/db/DbSet.kt
+++ b/app/src/main/java/li/songe/gkd/db/DbSet.kt
@@ -2,31 +2,26 @@ package li.songe.gkd.db
import androidx.room.Room
import androidx.room.RoomDatabase
-import com.blankj.utilcode.util.PathUtils
-import li.songe.gkd.App
-import li.songe.gkd.utils.FolderExt
-import java.io.File
+import li.songe.gkd.app
+import li.songe.gkd.util.FolderExt
object DbSet {
-
private fun getDb(
- klass: Class, name: String
+ klass: Class, name: String,
): T {
return Room.databaseBuilder(
- App.context, klass, FolderExt.dbFolder.absolutePath.plus("/${name}.db")
- ).fallbackToDestructiveMigration()
- .enableMultiInstanceInvalidation()
- .build()
+ app, klass, FolderExt.dbFolder.absolutePath.plus("/${name}.db")
+ ).fallbackToDestructiveMigration().enableMultiInstanceInvalidation().build()
}
private val snapshotDb by lazy { getDb(SnapshotDb::class.java, "snapshot") }
private val subsConfigDb by lazy { getDb(SubsConfigDb::class.java, "subsConfig") }
private val subsItemDb by lazy { getDb(SubsItemDb::class.java, "subsItem") }
- private val triggerLogDb by lazy { getDb(TriggerLogDb::class.java, "triggerLog") }
+ val triggerLogDb by lazy { getDb(TriggerLogDb::class.java, "triggerLog-v2") }
val subsItemDao by lazy { subsItemDb.subsItemDao() }
val subsConfigDao by lazy { subsConfigDb.subsConfigDao() }
val snapshotDao by lazy { snapshotDb.snapshotDao() }
- val triggerLogDao by lazy { triggerLogDb.triggerLogDao() }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/db/IgnoreConverters.kt b/app/src/main/java/li/songe/gkd/db/IgnoreConverters.kt
index 6dd88b278..3443eae08 100644
--- a/app/src/main/java/li/songe/gkd/db/IgnoreConverters.kt
+++ b/app/src/main/java/li/songe/gkd/db/IgnoreConverters.kt
@@ -6,9 +6,9 @@ import li.songe.gkd.data.NodeInfo
object IgnoreConverters {
@TypeConverter
@JvmStatic
- fun listToCol(list: List): String? = null
+ fun listToCol(list: List): String = ""
@TypeConverter
@JvmStatic
- fun colToList(value: String?): List = emptyList()
+ fun colToList(value: String): List = emptyList()
}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/db/TriggerLogDb.kt b/app/src/main/java/li/songe/gkd/db/TriggerLogDb.kt
index 5797c5914..1f3181a65 100644
--- a/app/src/main/java/li/songe/gkd/db/TriggerLogDb.kt
+++ b/app/src/main/java/li/songe/gkd/db/TriggerLogDb.kt
@@ -2,6 +2,7 @@ package li.songe.gkd.db
import androidx.room.Database
import androidx.room.RoomDatabase
+import li.songe.gkd.data.Snapshot
import li.songe.gkd.data.TriggerLog
@Database(
diff --git a/app/src/main/java/li/songe/gkd/debug/FloatingService.kt b/app/src/main/java/li/songe/gkd/debug/FloatingService.kt
index 67ca8d858..d53b5ed05 100644
--- a/app/src/main/java/li/songe/gkd/debug/FloatingService.kt
+++ b/app/src/main/java/li/songe/gkd/debug/FloatingService.kt
@@ -6,13 +6,12 @@ import android.content.Intent
import androidx.core.app.NotificationCompat
import com.blankj.utilcode.util.ServiceUtils
import com.torrydo.floatingbubbleview.FloatingBubble
-import li.songe.gkd.App
-import li.songe.gkd.R
+import li.songe.gkd.app
import li.songe.gkd.composition.CompositionExt.useLifeCycleLog
import li.songe.gkd.composition.CompositionFbService
import li.songe.gkd.composition.CompositionExt.useMessage
import li.songe.gkd.composition.InvokeMessage
-import li.songe.gkd.utils.SafeR
+import li.songe.gkd.util.SafeR
class FloatingService : CompositionFbService({
useLifeCycleLog()
@@ -26,7 +25,7 @@ class FloatingService : CompositionFbService({
}
}
setupBubble { _, resolve ->
- val builder = FloatingBubble.Builder(this).bubble(SafeR.capture, 40, 40)
+ val builder = FloatingBubble.Builder(this).bubble(SafeR.ic_capture, 40, 40)
.enableCloseBubble(false)
.addFloatingBubbleListener(object : FloatingBubble.Listener {
override fun onClick() {
@@ -54,7 +53,7 @@ class FloatingService : CompositionFbService({
companion object{
fun isRunning() = ServiceUtils.isServiceRunning(FloatingService::class.java)
- fun stop(context: Context = App.context) {
+ fun stop(context: Context =app) {
if (isRunning()) {
context.stopService(Intent(context, FloatingService::class.java))
}
diff --git a/app/src/main/java/li/songe/gkd/debug/HttpService.kt b/app/src/main/java/li/songe/gkd/debug/HttpService.kt
index 0061564b0..e4736f6ad 100644
--- a/app/src/main/java/li/songe/gkd/debug/HttpService.kt
+++ b/app/src/main/java/li/songe/gkd/debug/HttpService.kt
@@ -21,14 +21,11 @@ import io.ktor.server.routing.route
import io.ktor.server.routing.routing
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
-import li.songe.gkd.App
+import li.songe.gkd.app
import li.songe.gkd.composition.CompositionExt.useMessage
import li.songe.gkd.composition.CompositionService
import li.songe.gkd.composition.InvokeMessage
@@ -36,9 +33,9 @@ import li.songe.gkd.data.DeviceInfo
import li.songe.gkd.data.RpcError
import li.songe.gkd.db.DbSet
import li.songe.gkd.debug.SnapshotExt.captureSnapshot
-import li.songe.gkd.utils.Ext.getIpAddressInLocalNetwork
-import li.songe.gkd.utils.Storage
-import li.songe.gkd.utils.launchTry
+import li.songe.gkd.util.Ext.getIpAddressInLocalNetwork
+import li.songe.gkd.util.launchTry
+import li.songe.gkd.util.storeFlow
import java.io.File
class HttpService : CompositionService({
@@ -70,7 +67,7 @@ class HttpService : CompositionService({
}
val server = embeddedServer(
Netty,
- Storage.settings.httpServerPort,
+ storeFlow.value.httpServerPort,
configure = { tcpKeepAlive = true }
) {
install(CORS) { anyHost() }
@@ -117,7 +114,7 @@ class HttpService : CompositionService({
}
}
scope.launchTry(Dispatchers.IO) {
- LogUtils.d(*getIpAddressInLocalNetwork().map { host -> "http://${host}:${Storage.settings.httpServerPort}" }
+ LogUtils.d(*getIpAddressInLocalNetwork().map { host -> "http://${host}:${storeFlow.value.httpServerPort}" }
.toList().toTypedArray())
server.start(true)
}
@@ -131,13 +128,13 @@ class HttpService : CompositionService({
}) {
companion object {
fun isRunning() = ServiceUtils.isServiceRunning(HttpService::class.java)
- fun stop(context: Context = App.context) {
+ fun stop(context: Context = app) {
if (isRunning()) {
context.stopService(Intent(context, HttpService::class.java))
}
}
- fun start(context: Context = App.context) {
+ fun start(context: Context = app) {
context.startService(Intent(context, HttpService::class.java))
}
diff --git a/app/src/main/java/li/songe/gkd/debug/KtorPlugins.kt b/app/src/main/java/li/songe/gkd/debug/KtorPlugins.kt
index 28cc9ad04..7a585c913 100644
--- a/app/src/main/java/li/songe/gkd/debug/KtorPlugins.kt
+++ b/app/src/main/java/li/songe/gkd/debug/KtorPlugins.kt
@@ -18,8 +18,7 @@ val RpcErrorHeaderPlugin = createApplicationPlugin(name = "RpcErrorHeaderPlugin"
when (cause) {
is RpcError -> {
// 主动抛出的错误
- LogUtils.d(call.request.uri, cause.code, cause.message)
- call.response.header(RpcError.HeaderKey, RpcError.HeaderErrorValue)
+ LogUtils.d(call.request.uri, cause.message)
call.respond(cause)
}
@@ -38,13 +37,5 @@ val RpcErrorHeaderPlugin = createApplicationPlugin(name = "RpcErrorHeaderPlugin"
onCallRespond { call, _ ->
call.response.header("Access-Control-Expose-Headers", "*")
call.response.header("Access-Control-Allow-Private-Network", "true")
- val status = call.response.status() ?: HttpStatusCode.OK
- if (status == HttpStatusCode.OK &&
- !call.response.headers.contains(
- RpcError.HeaderKey
- )
- ) {
- call.response.header(RpcError.HeaderKey, RpcError.HeaderOkValue)
- }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/debug/ScreenshotService.kt b/app/src/main/java/li/songe/gkd/debug/ScreenshotService.kt
index 9d749eb79..c2e1c1cdd 100644
--- a/app/src/main/java/li/songe/gkd/debug/ScreenshotService.kt
+++ b/app/src/main/java/li/songe/gkd/debug/ScreenshotService.kt
@@ -5,11 +5,11 @@ import android.content.Context
import android.content.Intent
import com.blankj.utilcode.util.LogUtils
import com.blankj.utilcode.util.ServiceUtils
-import li.songe.gkd.App
+import li.songe.gkd.app
import li.songe.gkd.composition.CompositionExt.useLifeCycleLog
import li.songe.gkd.composition.CompositionService
-import li.songe.gkd.utils.Ext
-import li.songe.gkd.utils.ScreenshotUtil
+import li.songe.gkd.util.Ext
+import li.songe.gkd.util.ScreenshotUtil
class ScreenshotService : CompositionService({
useLifeCycleLog()
@@ -30,13 +30,13 @@ class ScreenshotService : CompositionService({
suspend fun screenshot() = screenshotUtil?.execute()
private var screenshotUtil: ScreenshotUtil? = null
- fun start(context: Context = App.context, intent: Intent) {
+ fun start(context: Context = app, intent: Intent) {
intent.component = ComponentName(context, ScreenshotService::class.java)
context.startForegroundService(intent)
}
fun isRunning() = ServiceUtils.isServiceRunning(ScreenshotService::class.java)
- fun stop(context: Context = App.context) {
+ fun stop(context: Context = app) {
if (isRunning()) {
context.stopService(Intent(context, ScreenshotService::class.java))
}
diff --git a/app/src/main/java/li/songe/gkd/debug/SnapshotExt.kt b/app/src/main/java/li/songe/gkd/debug/SnapshotExt.kt
index c549535d4..8f39aef19 100644
--- a/app/src/main/java/li/songe/gkd/debug/SnapshotExt.kt
+++ b/app/src/main/java/li/songe/gkd/debug/SnapshotExt.kt
@@ -10,17 +10,17 @@ import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.serialization.encodeToString
-import li.songe.gkd.App
-import li.songe.gkd.accessibility.GkdAbService
+import li.songe.gkd.service.GkdAbService
+import li.songe.gkd.app
import li.songe.gkd.data.RpcError
import li.songe.gkd.data.Snapshot
import li.songe.gkd.db.DbSet
-import li.songe.gkd.utils.Singleton
+import li.songe.gkd.util.Singleton
import java.io.File
object SnapshotExt {
private val snapshotDir by lazy {
- App.context.getExternalFilesDir("snapshot")!!.apply { if (!exists()) mkdir() }
+ app.getExternalFilesDir("snapshot")!!.apply { if (!exists()) mkdir() }
}
private val emptyBitmap by lazy {
diff --git a/app/src/main/java/li/songe/gkd/icon/AddIcon.kt b/app/src/main/java/li/songe/gkd/icon/AddIcon.kt
index f73d83129..040eb8a2f 100644
--- a/app/src/main/java/li/songe/gkd/icon/AddIcon.kt
+++ b/app/src/main/java/li/songe/gkd/icon/AddIcon.kt
@@ -1,6 +1,6 @@
package li.songe.gkd.icon
-import androidx.compose.foundation.Image
+import androidx.compose.material.Icon
import androidx.compose.material.icons.materialIcon
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Brush
@@ -10,7 +10,7 @@ import androidx.compose.ui.tooling.preview.Preview
// @DslMarker
// https://github.com/JetBrains/kotlin-wrappers/blob/master/kotlin-react/src/jsMain/kotlin/react/ChildrenBuilder.kt
-val AddIcon = materialIcon(name = "add") {
+val AddIcon = materialIcon(name = "AddIcon") {
addPath(
pathData = addPathNodes("M18,13h-5v5c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1v-5H6c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1h5V6c0,-0.55 0.45,-1 1,-1s1,0.45 1,1v5h5c0.55,0 1,0.45 1,1s-0.45,1 -1,1z"),
fill = Brush.linearGradient(listOf(Color.Black, Color.Black))
@@ -19,6 +19,6 @@ val AddIcon = materialIcon(name = "add") {
@Preview
@Composable
-fun PreviewIconAdd() {
- Image(imageVector = AddIcon, contentDescription = null)
+fun PreviewAddIcon() {
+ Icon(imageVector = AddIcon, contentDescription = null)
}
diff --git a/app/src/main/java/li/songe/gkd/icon/ArrowIcon.kt b/app/src/main/java/li/songe/gkd/icon/ArrowIcon.kt
new file mode 100644
index 000000000..210be67de
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/icon/ArrowIcon.kt
@@ -0,0 +1,22 @@
+package li.songe.gkd.icon
+
+import androidx.compose.material.Icon
+import androidx.compose.material.icons.materialIcon
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.addPathNodes
+import androidx.compose.ui.tooling.preview.Preview
+
+val ArrowIcon = materialIcon(name = "ArrowIcon") {
+ addPath(
+ pathData = addPathNodes("M6.23 20.23L8 22l10-10L8 2L6.23 3.77L14.46 12z"),
+ fill = Brush.linearGradient(listOf(Color.Black, Color.Black))
+ )
+}
+
+@Preview
+@Composable
+fun PreviewArrowIcon() {
+ Icon(imageVector = ArrowIcon, contentDescription = null)
+}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/icon/HomeIcon.kt b/app/src/main/java/li/songe/gkd/icon/HomeIcon.kt
new file mode 100644
index 000000000..8158a6cc6
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/icon/HomeIcon.kt
@@ -0,0 +1,22 @@
+package li.songe.gkd.icon
+
+import androidx.compose.material.Icon
+import androidx.compose.material.icons.materialIcon
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.addPathNodes
+import androidx.compose.ui.tooling.preview.Preview
+
+val HomeIcon = materialIcon(name = "ArrowIcon") {
+ addPath(
+ pathData = addPathNodes("M16.612 2.214a1.01 1.01 0 0 0-1.242 0L1 13.419l1.243 1.572L4 13.621V26a2.004 2.004 0 0 0 2 2h20a2.004 2.004 0 0 0 2-2V13.63L29.757 15L31 13.428zM18 26h-4v-8h4zm2 0v-8a2.002 2.002 0 0 0-2-2h-4a2.002 2.002 0 0 0-2 2v8H6V12.062l10-7.79l10 7.8V26z"),
+ fill = Brush.linearGradient(listOf(Color.Black, Color.Black))
+ )
+}
+
+@Preview
+@Composable
+fun PreviewHomeIcon() {
+ Icon(imageVector = HomeIcon, contentDescription = null)
+}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/notif/Notif.kt b/app/src/main/java/li/songe/gkd/notif/Notif.kt
new file mode 100644
index 000000000..3be2e2b1b
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/notif/Notif.kt
@@ -0,0 +1,26 @@
+package li.songe.gkd.notif
+
+import li.songe.gkd.util.SafeR
+
+data class Notif(
+ val id: Int,
+ val icon: Int,
+ val title: String,
+ val text: String,
+ val ongoing: Boolean,
+ val autoCancel: Boolean,
+)
+
+
+const val STATUS_NOTIF_ID = 100
+
+val abNotif by lazy {
+ Notif(
+ id = STATUS_NOTIF_ID,
+ icon = SafeR.ic_launcher,
+ title = "搞快点",
+ text = "无障碍正在运行",
+ ongoing = true,
+ autoCancel = false
+ )
+}
diff --git a/app/src/main/java/li/songe/gkd/notif/NotifChannel.kt b/app/src/main/java/li/songe/gkd/notif/NotifChannel.kt
new file mode 100644
index 000000000..b9dcc2b79
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/notif/NotifChannel.kt
@@ -0,0 +1,13 @@
+package li.songe.gkd.notif
+
+data class NotifChannel(
+ val id: String,
+ val name: String,
+ val desc: String,
+)
+
+val defaultChannel by lazy {
+ NotifChannel(
+ id = "default", name = "搞快点", desc = "显示服务运行状态"
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/notif/NotifManager.kt b/app/src/main/java/li/songe/gkd/notif/NotifManager.kt
new file mode 100644
index 000000000..91c6096cd
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/notif/NotifManager.kt
@@ -0,0 +1,48 @@
+package li.songe.gkd.notif
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ServiceInfo
+import android.os.Build
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import li.songe.gkd.MainActivity
+
+fun createChannel(context: Context, notifChannel: NotifChannel) {
+ val importance = NotificationManager.IMPORTANCE_DEFAULT
+ val channel = NotificationChannel(notifChannel.id, notifChannel.name, importance)
+ channel.description = notifChannel.desc
+ val notificationManager = NotificationManagerCompat.from(context)
+ notificationManager.createNotificationChannel(channel)
+}
+
+fun createNotif(context: Service, notifChannel: NotifChannel, notif: Notif) {
+ createChannel(context, notifChannel)
+
+ val intent = Intent(context, MainActivity::class.java).apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ }
+ val pendingIntent: PendingIntent = PendingIntent.getActivity(
+ context, 0, intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ )
+
+ val builder = NotificationCompat.Builder(context, notifChannel.id).setSmallIcon(notif.icon)
+ .setContentTitle(notif.title).setContentText(notif.text).setContentIntent(pendingIntent)
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT).setOngoing(notif.ongoing)
+ .setAutoCancel(notif.autoCancel)
+
+ val notification = builder.build()
+ val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+// manager.notify(notice.id, notification)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ context.startForeground(
+ notif.id, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST
+ )
+ } else {
+ context.startForeground(notif.id, notification)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/accessibility/AbExt.kt b/app/src/main/java/li/songe/gkd/service/AbExt.kt
similarity index 97%
rename from app/src/main/java/li/songe/gkd/accessibility/AbExt.kt
rename to app/src/main/java/li/songe/gkd/service/AbExt.kt
index 7d66b45c4..a0fd63c24 100644
--- a/app/src/main/java/li/songe/gkd/accessibility/AbExt.kt
+++ b/app/src/main/java/li/songe/gkd/service/AbExt.kt
@@ -1,4 +1,4 @@
-package li.songe.gkd.accessibility
+package li.songe.gkd.service
import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.GestureDescription
@@ -46,7 +46,7 @@ fun AccessibilityNodeInfo.click(service: AccessibilityService) = when {
service.dispatchGesture(gestureDescription.build(), null, null)
"(50%, 50%)"
} else {
- "($x, $y) no click"
+ null
}
}
}
@@ -98,6 +98,7 @@ val abTransform = Transform(
"isFocused" -> node.isFocused
"isFocusable" -> node.isFocusable
"isVisibleToUser" -> node.isVisibleToUser
+ ""->node.isAccessibilityFocused
"left" -> node.getTempRect().left
"top" -> node.getTempRect().top
diff --git a/app/src/main/java/li/songe/gkd/accessibility/GkdAbService.kt b/app/src/main/java/li/songe/gkd/service/GkdAbService.kt
similarity index 84%
rename from app/src/main/java/li/songe/gkd/accessibility/GkdAbService.kt
rename to app/src/main/java/li/songe/gkd/service/GkdAbService.kt
index c00a4387c..252646235 100644
--- a/app/src/main/java/li/songe/gkd/accessibility/GkdAbService.kt
+++ b/app/src/main/java/li/songe/gkd/service/GkdAbService.kt
@@ -1,4 +1,4 @@
-package li.songe.gkd.accessibility
+package li.songe.gkd.service
import android.graphics.Bitmap
import android.os.Build
@@ -16,7 +16,6 @@ import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.withContext
import li.songe.gkd.composition.CompositionAbService
import li.songe.gkd.composition.CompositionExt.useLifeCycleLog
import li.songe.gkd.composition.CompositionExt.useScope
@@ -24,15 +23,16 @@ import li.songe.gkd.data.NodeInfo
import li.songe.gkd.data.Rule
import li.songe.gkd.data.RuleManager
import li.songe.gkd.data.SubscriptionRaw
+import li.songe.gkd.data.TriggerLog
import li.songe.gkd.db.DbSet
import li.songe.gkd.debug.SnapshotExt
import li.songe.gkd.shizuku.activityTaskManager
import li.songe.gkd.shizuku.shizukuIsSafeOK
-import li.songe.gkd.utils.Singleton
-import li.songe.gkd.utils.Storage
-import li.songe.gkd.utils.launchTry
-import li.songe.gkd.utils.launchWhile
-import li.songe.gkd.utils.launchWhileTry
+import li.songe.gkd.util.Singleton
+import li.songe.gkd.util.launchTry
+import li.songe.gkd.util.launchWhile
+import li.songe.gkd.util.launchWhileTry
+import li.songe.gkd.util.storeFlow
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@@ -50,11 +50,12 @@ class GkdAbService : CompositionAbService({
currentActivityId = null
}
- KeepAliveService.start(context)
+ ManageService.start(context)
onDestroy {
- KeepAliveService.stop(context)
+ ManageService.stop(context)
}
+
var serviceConnected = false
onServiceConnected { serviceConnected = true }
onInterrupt { serviceConnected = false }
@@ -62,21 +63,21 @@ class GkdAbService : CompositionAbService({
onAccessibilityEvent { event -> // 根据事件获取 activityId, 概率不准确
when (event?.eventType) {
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, AccessibilityEvent.TYPE_WINDOWS_CHANGED -> {
+ val appId = rootInActiveWindow?.packageName?.toString() ?: return@onAccessibilityEvent
val activityId = event.className?.toString() ?: return@onAccessibilityEvent
if (activityId == "com.miui.home.launcher.Launcher") { // 小米桌面 bug
- val appId =
- rootInActiveWindow?.packageName?.toString() ?: return@onAccessibilityEvent
if (appId != "com.miui.home") {
return@onAccessibilityEvent
}
}
- if (activityId.startsWith("android.") ||
- activityId.startsWith("androidx.") ||
- activityId.startsWith("com.android.")
+ if (activityId.startsWith("android.") || activityId.startsWith("androidx.") || activityId.startsWith(
+ "com.android."
+ )
) {
return@onAccessibilityEvent
}
+ currentAppId = appId
currentActivityId = activityId
}
@@ -85,7 +86,7 @@ class GkdAbService : CompositionAbService({
}
onAccessibilityEvent { event -> // 小米手机监听截屏保存快照
- if (!Storage.settings.enableCaptureSystemScreenshot) return@onAccessibilityEvent
+ if (!storeFlow.value.enableCaptureScreenshot) return@onAccessibilityEvent
if (event?.packageName == null || event.className == null) return@onAccessibilityEvent
if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED && event.contentChangeTypes == AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED && event.packageName.contentEquals(
"com.miui.screenshot"
@@ -99,12 +100,13 @@ class GkdAbService : CompositionAbService({
}
}
+
scope.launchWhile { // 屏幕无障碍信息轮询
delay(200)
if (!serviceConnected) return@launchWhile
- if (!Storage.settings.enableService || ScreenUtils.isScreenLock()) return@launchWhile
-
currentAppId = rootInActiveWindow?.packageName?.toString()
+ if (!storeFlow.value.enableService || ScreenUtils.isScreenLock()) return@launchWhile
+
var tempRules = rules
var i = 0
while (i < tempRules.size) {
@@ -119,6 +121,20 @@ class GkdAbService : CompositionAbService({
LogUtils.d(
*rule.matches.toTypedArray(), NodeInfo.abNodeToNode(target), clickResult
)
+
+ if (clickResult != null) {
+ scope.launchTry(IO) {
+ val triggerLog = TriggerLog(
+ appId = currentAppId,
+ activityId = currentActivityId,
+ subsId = rule.subsItem.id,
+ groupKey = rule.group.key,
+ ruleIndex = rule.index,
+ ruleKey = rule.key
+ )
+ DbSet.triggerLogDb.triggerLogDao().insert(triggerLog)
+ }
+ }
}
delay(50)
currentAppId = rootInActiveWindow?.packageName?.toString()
@@ -159,12 +175,9 @@ class GkdAbService : CompositionAbService({
}
scope.launchTry {
+ delay(5000)
DbSet.subsItemDao.query().flowOn(IO).collect {
- val subscriptionRawArray = withContext(IO) {
- it.filter { s -> s.enable }
- .mapNotNull { s -> s.subscriptionRaw }
- }
- ruleManager = RuleManager(*subscriptionRawArray.toTypedArray())
+ ruleManager = RuleManager(it)
}
}
@@ -196,7 +209,7 @@ class GkdAbService : CompositionAbService({
LogUtils.d(
"currentAppId: $currentAppId",
"currentActivityId: $currentActivityId",
- *value.toTypedArray()
+ value.size,
)
}
@@ -242,12 +255,5 @@ class GkdAbService : CompositionAbService({
}
}
}
-
-// fun match(selector: String) {
-// val rootAbNode = service?.rootInActiveWindow ?: return
-// val list =
-// rootAbNode.querySelectorAll(Selector.parse(selector)).map { it.value }.toList()
-// }
-
}
}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/service/ManageService.kt b/app/src/main/java/li/songe/gkd/service/ManageService.kt
new file mode 100644
index 000000000..faebcdadd
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/service/ManageService.kt
@@ -0,0 +1,26 @@
+package li.songe.gkd.service
+
+import android.content.Context
+import android.content.Intent
+import li.songe.gkd.app
+import li.songe.gkd.composition.CompositionExt.useLifeCycleLog
+import li.songe.gkd.composition.CompositionService
+import li.songe.gkd.notif.abNotif
+import li.songe.gkd.notif.createNotif
+import li.songe.gkd.notif.defaultChannel
+
+class ManageService : CompositionService({
+ useLifeCycleLog()
+ val context = this
+ createNotif(context, defaultChannel, abNotif)
+}) {
+ companion object {
+ fun start(context: Context = app) {
+ context.startForegroundService(Intent(context, ManageService::class.java))
+ }
+
+ fun stop(context: Context = app) {
+ context.stopService(Intent(context, ManageService::class.java))
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/accessibility/ShizukuService.kt b/app/src/main/java/li/songe/gkd/service/ShizukuService.kt
similarity index 73%
rename from app/src/main/java/li/songe/gkd/accessibility/ShizukuService.kt
rename to app/src/main/java/li/songe/gkd/service/ShizukuService.kt
index 3d891ca31..d8a420949 100644
--- a/app/src/main/java/li/songe/gkd/accessibility/ShizukuService.kt
+++ b/app/src/main/java/li/songe/gkd/service/ShizukuService.kt
@@ -1,4 +1,4 @@
-package li.songe.gkd.accessibility
+package li.songe.gkd.service
import li.songe.gkd.composition.CompositionService
diff --git a/app/src/main/java/li/songe/gkd/ui/AboutPage.kt b/app/src/main/java/li/songe/gkd/ui/AboutPage.kt
index 2c6cf2909..77b4ef85d 100644
--- a/app/src/main/java/li/songe/gkd/ui/AboutPage.kt
+++ b/app/src/main/java/li/songe/gkd/ui/AboutPage.kt
@@ -1,86 +1,49 @@
package li.songe.gkd.ui
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.material.Icon
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
-import androidx.compose.material.TopAppBar
-import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootNavGraph
-import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import li.songe.gkd.BuildConfig
-import li.songe.gkd.utils.SafeR
+import li.songe.gkd.ui.component.SimpleTopAppBar
+import li.songe.gkd.util.LocalNavController
@RootNavGraph
@Destination
@Composable
-fun AboutPage(navigator: DestinationsNavigator) {
-// val systemUiController = rememberSystemUiController()
-// val context = LocalContext.current as ComponentActivity
-// DisposableEffect(systemUiController) {
-// val oldVisible = systemUiController.isStatusBarVisible
-// systemUiController.isStatusBarVisible = false
-// WindowCompat.setDecorFitsSystemWindows(context.window, false)
-// onDispose {
-// systemUiController.isStatusBarVisible = oldVisible
-// WindowCompat.setDecorFitsSystemWindows(context.window, true)
-// }
-// }
- Scaffold(
- topBar = {
- TopAppBar(
- backgroundColor = Color(0xfff8f9f9),
- navigationIcon = {
- Column(
- modifier = Modifier
- .fillMaxSize(),
- verticalArrangement = Arrangement.Center,
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Icon(
- painter = painterResource(id = SafeR.ic_back),
- contentDescription = null,
- modifier = Modifier
- .size(30.dp)
- .clickable(
- interactionSource = remember { MutableInteractionSource() },
- indication = rememberRipple(bounded = false),
- ) {
- navigator.popBackStack()
- }
- )
- }
- },
- title = { Text(text = "关于") }
- )
- },
- content = { contentPadding ->
- Column(
- Modifier
- .padding(contentPadding)
- .padding(10.dp),
- verticalArrangement = Arrangement.spacedBy(10.dp)
- ) {
- Text(text = "版本代码: " + BuildConfig.VERSION_CODE)
- Text(text = "版本名称: " + BuildConfig.VERSION_NAME)
- Text(text = "构建时间: " + BuildConfig.BUILD_DATE)
- Text(text = "构建类型: " + BuildConfig.BUILD_TYPE)
- }
+fun AboutPage() {
+ // val systemUiController = rememberSystemUiController()
+ // val context = LocalContext.current as ComponentActivity
+ // DisposableEffect(systemUiController) {
+ // val oldVisible = systemUiController.isStatusBarVisible
+ // systemUiController.isStatusBarVisible = false
+ // WindowCompat.setDecorFitsSystemWindows(context.window, false)
+ // onDispose {
+ // systemUiController.isStatusBarVisible = oldVisible
+ // WindowCompat.setDecorFitsSystemWindows(context.window, true)
+ // }
+ // }
+ val navController = LocalNavController.current
+ Scaffold(topBar = {
+ SimpleTopAppBar(onClickIcon = { navController.popBackStack() }, title = "关于")
+ }, content = { contentPadding ->
+ Column(
+ Modifier
+ .padding(contentPadding)
+ .padding(10.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
+ Text(text = "版本代码: " + BuildConfig.VERSION_CODE)
+ Text(text = "版本名称: " + BuildConfig.VERSION_NAME)
+ Text(text = "构建时间: " + BuildConfig.BUILD_DATE)
+ Text(text = "构建类型: " + BuildConfig.BUILD_TYPE)
}
- )
+ })
}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/ui/AppItemPage.kt b/app/src/main/java/li/songe/gkd/ui/AppItemPage.kt
index e3987ed01..5b7f7c886 100644
--- a/app/src/main/java/li/songe/gkd/ui/AppItemPage.kt
+++ b/app/src/main/java/li/songe/gkd/ui/AppItemPage.kt
@@ -1,5 +1,6 @@
package li.songe.gkd.ui
+import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -11,13 +12,13 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.shape.GenericShape
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.Scaffold
import androidx.compose.material.Switch
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -25,168 +26,111 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.CornerRadius
-import androidx.compose.ui.geometry.RoundRect
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
-import com.google.accompanist.placeholder.PlaceholderHighlight
-import com.google.accompanist.placeholder.material.fade
-import com.google.accompanist.placeholder.material.placeholder
+import androidx.hilt.navigation.compose.hiltViewModel
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootNavGraph
import kotlinx.serialization.encodeToString
import li.songe.gkd.data.SubsConfig
import li.songe.gkd.data.SubscriptionRaw
-import li.songe.gkd.data.getAppInfo
+import li.songe.gkd.data.getAppName
import li.songe.gkd.db.DbSet
-import li.songe.gkd.utils.Singleton
-import li.songe.gkd.utils.launchAsFn
+import li.songe.gkd.ui.component.SimpleTopAppBar
+import li.songe.gkd.util.LocalNavController
+import li.songe.gkd.util.Singleton
+import li.songe.gkd.util.launchAsFn
@RootNavGraph
@Destination
@Composable
fun AppItemPage(
- subsApp: SubscriptionRaw.AppRaw,
- subsConfig: SubsConfig,
+ subsItemId: Long,
+ appId: String,
+ focusGroupKey: Int? = null, // 背景/边框高亮一下
) {
val scope = rememberCoroutineScope()
-
- var subsConfigs: List? by remember { mutableStateOf(null) }
-
- LaunchedEffect(Unit) {
- val mutableSet = DbSet.subsConfigDao.queryGroupTypeConfig(subsConfig.subsItemId, subsApp.id)
- val list = mutableListOf()
- subsApp.groups.forEach { group ->
- if (group.key == null) {
- list.add(null)
- } else {
- val item = mutableSet.find { s -> s.groupKey == group.key }
- ?: SubsConfig(
- subsItemId = subsConfig.subsItemId,
- appId = subsConfig.appId,
- groupKey = group.key,
- type = SubsConfig.GroupType
- )
- list.add(item)
- }
- }
- subsConfigs = list
- }
+ val navController = LocalNavController.current
+ val vm = hiltViewModel()
+ val subsConfigs by vm.subsConfigsFlow.collectAsState()
+ val subsApp by vm.subsAppFlow.collectAsState()
var showGroupItem: SubscriptionRaw.GroupRaw? by remember { mutableStateOf(null) }
- LazyColumn(modifier = Modifier.fillMaxSize()) {
- item {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(10.dp, 0.dp)
- ) {
- Text(
- text = getAppInfo(subsApp.id).name ?: "-",
- maxLines = 1,
- softWrap = false,
- overflow = TextOverflow.Ellipsis,
- )
- Spacer(modifier = Modifier.width(10.dp))
- Text(
- text = subsApp.id,
- maxLines = 1,
- softWrap = false,
- overflow = TextOverflow.Ellipsis,
- )
+ Scaffold(topBar = {
+ SimpleTopAppBar(
+ onClickIcon = { navController.popBackStack() },
+ title = getAppName(subsApp?.id) ?: subsApp?.id ?: ""
+ )
+ }, content = { contentPadding ->
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(contentPadding)
+ ) {
+ item {
+ Spacer(modifier = Modifier.height(10.dp))
}
- Spacer(modifier = Modifier.height(10.dp))
- }
-
- items(subsApp.groups.size) { i ->
- val group = subsApp.groups[i]
- Row(
- modifier = Modifier
- .clickable {
- showGroupItem = group
- }
- .padding(10.dp, 6.dp)
- .fillMaxWidth()
- .height(45.dp),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Column(
- modifier = Modifier
- .weight(1f)
- .fillMaxHeight(),
- verticalArrangement = Arrangement.SpaceBetween
- ) {
- Text(
- text = group.name ?: "-",
- maxLines = 1,
- softWrap = false,
- overflow = TextOverflow.Ellipsis,
- modifier = Modifier
- .fillMaxWidth()
- )
- Text(
- text = group.desc ?: "-",
- maxLines = 1,
- softWrap = false,
- overflow = TextOverflow.Ellipsis,
+ subsApp?.groups?.let { groupsVal ->
+ items(groupsVal, { it.key }) { group ->
+ Row(
modifier = Modifier
+ .background(
+ if (group.key == focusGroupKey) Color(0x500a95ff) else Color.Transparent
+ )
+ .clickable { showGroupItem = group }
+ .padding(10.dp, 6.dp)
.fillMaxWidth()
- )
- }
+ .height(45.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Column(
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxHeight(),
+ verticalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = group.name ?: "-",
+ maxLines = 1,
+ softWrap = false,
+ overflow = TextOverflow.Ellipsis,
+ modifier = Modifier.fillMaxWidth()
+ )
+ Text(
+ text = group.desc ?: "-",
+ maxLines = 1,
+ softWrap = false,
+ overflow = TextOverflow.Ellipsis,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
- Spacer(modifier = Modifier.width(10.dp))
+ Spacer(modifier = Modifier.width(10.dp))
- if (group.key != null) {
- val crPx = with(LocalDensity.current) { 4.dp.toPx() }
- Switch(
- checked = subsConfigs?.get(i)?.enable != false,
- modifier = Modifier
- .placeholder(
- subsConfigs == null,
- highlight = PlaceholderHighlight.fade(),
- shape = GenericShape { size, _ ->
- val cr = CornerRadius(crPx, crPx)
- addRoundRect(
- RoundRect(
- left = 0f,
- top = size.height * .25f,
- right = size.width,
- bottom = size.height * .75f,
- topLeftCornerRadius = cr,
- topRightCornerRadius = cr,
- bottomLeftCornerRadius = cr,
- bottomRightCornerRadius = cr,
- )
- )
- }
- ),
- onCheckedChange = scope.launchAsFn { enable ->
- val subsConfigsVal = subsConfigs ?: return@launchAsFn
- val newItem =
- subsConfigsVal[i]?.copy(enable = enable) ?: return@launchAsFn
- DbSet.subsConfigDao.insert(newItem)
- subsConfigs = subsConfigsVal.toMutableList().apply {
- set(i, newItem)
- }
- }
- )
- } else {
- Text(
- text = "-",
- modifier = Modifier
- .width(48.dp)
- .wrapContentHeight(),
- textAlign = TextAlign.Center
- )
+ val subsConfig = subsConfigs.find { it.groupKey == group.key }
+ Switch(checked = subsConfig?.enable != false,
+ modifier = Modifier,
+ onCheckedChange = scope.launchAsFn { enable ->
+ val newItem = (subsConfig ?: SubsConfig(
+ type = SubsConfig.GroupType,
+ subsItemId = subsItemId,
+ appId = appId,
+ )).copy(enable = enable)
+ DbSet.subsConfigDao.insert(newItem)
+ })
+ }
}
}
+
+ item {
+ Spacer(modifier = Modifier.height(20.dp))
+ }
}
- }
+ })
showGroupItem?.let { showGroupItemVal ->
diff --git a/app/src/main/java/li/songe/gkd/ui/AppItemVm.kt b/app/src/main/java/li/songe/gkd/ui/AppItemVm.kt
new file mode 100644
index 000000000..8c405b01e
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/ui/AppItemVm.kt
@@ -0,0 +1,35 @@
+package li.songe.gkd.ui
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+import li.songe.gkd.data.SubsItem
+import li.songe.gkd.data.SubscriptionRaw
+import li.songe.gkd.db.DbSet
+import li.songe.gkd.ui.destinations.AppItemPageDestination
+import javax.inject.Inject
+
+@HiltViewModel
+class AppItemVm @Inject constructor(stateHandle: SavedStateHandle) : ViewModel() {
+ private val args = AppItemPageDestination.argsFrom(stateHandle)
+
+ val subsConfigsFlow = DbSet.subsConfigDao.queryGroupTypeConfig(args.subsItemId, args.appId)
+ .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
+
+ val subsAppFlow = MutableStateFlow(null)
+
+ init {
+ viewModelScope.launch(Dispatchers.IO) {
+ val subscriptionRaw = SubsItem.getSubscriptionRaw(args.subsItemId) ?: return@launch
+ subsAppFlow.value = subscriptionRaw.apps.find { it.id == args.appId }
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/ui/ControlPage.kt b/app/src/main/java/li/songe/gkd/ui/ControlPage.kt
new file mode 100644
index 000000000..3840c3581
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/ui/ControlPage.kt
@@ -0,0 +1,115 @@
+package li.songe.gkd.ui
+
+import android.content.Intent
+import android.provider.Settings
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Scaffold
+import androidx.compose.material.Text
+import androidx.compose.material.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.blankj.utilcode.util.ToastUtils
+import com.ramcosta.composedestinations.navigation.navigate
+import kotlinx.coroutines.delay
+import li.songe.gkd.MainActivity
+import li.songe.gkd.service.GkdAbService
+import li.songe.gkd.ui.component.SettingItem
+import li.songe.gkd.ui.component.TextSwitch
+import li.songe.gkd.ui.destinations.AboutPageDestination
+import li.songe.gkd.ui.destinations.RecordPageDestination
+import li.songe.gkd.ui.destinations.SnapshotPageDestination
+import li.songe.gkd.util.LocalNavController
+import li.songe.gkd.util.SafeR
+import li.songe.gkd.util.launchAsFn
+import li.songe.gkd.util.storeFlow
+import li.songe.gkd.util.updateStore
+import li.songe.gkd.util.usePollState
+import li.songe.gkd.util.useTask
+
+val controlNav = BottomNavItem(label = "主页", icon = SafeR.ic_home, route = "settings")
+
+@Composable
+fun ControlPage() {
+ val context = LocalContext.current as MainActivity
+ val navController = LocalNavController.current
+ val scope = rememberCoroutineScope()
+ val vm = hiltViewModel()
+ val recordCount by vm.recordCountFlow.collectAsState()
+
+ val store by storeFlow.collectAsState()
+
+ Scaffold(
+ topBar = {
+ TopAppBar(backgroundColor = Color(0xfff8f9f9), title = {
+ Text(
+ text = "搞快点", color = Color.Black
+ )
+ })
+ },
+ ) { padding ->
+ Column(
+ modifier = Modifier
+ .verticalScroll(
+ state = rememberScrollState()
+ )
+ .padding(0.dp, 10.dp)
+ .padding(padding)
+ ) {
+ val gkdAccessRunning by usePollState { GkdAbService.isRunning() }
+ TextSwitch(name = "无障碍授权",
+ desc = "用于获取屏幕信息,点击屏幕上的控件",
+ gkdAccessRunning,
+ onCheckedChange = scope.launchAsFn {
+ if (!it) return@launchAsFn
+ ToastUtils.showShort("请先启动无障碍服务")
+ delay(500)
+ val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ context.startActivity(intent)
+ })
+
+
+ Spacer(modifier = Modifier.height(5.dp))
+ TextSwitch(name = "服务开启",
+ desc = "保持服务开启,根据订阅规则匹配屏幕目标节点",
+ checked = store.enableService,
+ onCheckedChange = {
+ updateStore(
+ store.copy(
+ enableService = it
+ )
+ )
+ })
+
+ Spacer(modifier = Modifier.height(5.dp))
+ Text(text = "规则已触发 $recordCount 次", modifier = Modifier.padding(10.dp, 0.dp))
+ Spacer(modifier = Modifier.height(5.dp))
+ Text(text = "最近触发规则组: 微信朋友圈广告", modifier = Modifier.padding(10.dp, 0.dp))
+
+ SettingItem(title = "快照记录", onClick = scope.useTask().launchAsFn {
+ navController.navigate(SnapshotPageDestination)
+ })
+
+ SettingItem(title = "触发记录", onClick = scope.useTask().launchAsFn {
+ navController.navigate(RecordPageDestination)
+ })
+
+ SettingItem(title = "关于", onClick = scope.useTask().launchAsFn {
+ navController.navigate(AboutPageDestination)
+ })
+
+ }
+ }
+}
diff --git a/app/src/main/java/li/songe/gkd/ui/ControlVm.kt b/app/src/main/java/li/songe/gkd/ui/ControlVm.kt
new file mode 100644
index 000000000..450166602
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/ui/ControlVm.kt
@@ -0,0 +1,14 @@
+package li.songe.gkd.ui
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.stateIn
+import li.songe.gkd.db.DbSet
+import javax.inject.Inject
+
+@HiltViewModel
+class ControlVm @Inject constructor() : ViewModel() {
+ val recordCountFlow = DbSet.triggerLogDb.triggerLogDao().count().stateIn(viewModelScope, SharingStarted.Eagerly, 0)
+}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/ui/HomePage.kt b/app/src/main/java/li/songe/gkd/ui/HomePage.kt
new file mode 100644
index 000000000..fb17c0dd6
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/ui/HomePage.kt
@@ -0,0 +1,80 @@
+package li.songe.gkd.ui
+
+import androidx.annotation.DrawableRes
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.BottomNavigation
+import androidx.compose.material.BottomNavigationItem
+import androidx.compose.material.Icon
+import androidx.compose.material.Scaffold
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.ViewModel
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.annotation.RootNavGraph
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import javax.inject.Inject
+
+val BottomNavItems = listOf(
+ subsNav, controlNav, settingsNav
+)
+
+data class BottomNavItem(
+ val label: String,
+ @DrawableRes val icon: Int,
+ val route: String,
+)
+
+@HiltViewModel
+class HomePageVm @Inject constructor() : ViewModel() {
+ val tabFlow = MutableStateFlow(controlNav)
+}
+
+@RootNavGraph(start = true)
+@Destination
+@Composable
+fun HomePage() {
+ val vm = hiltViewModel()
+ val tab by vm.tabFlow.collectAsState()
+
+ Scaffold(bottomBar = {
+ BottomNavigation(
+ backgroundColor = Color.Transparent, elevation = 0.dp
+ ) {
+ BottomNavItems.forEach { navItem ->
+ BottomNavigationItem(selected = tab == navItem,
+ modifier = Modifier.background(Color.Transparent),
+ onClick = {
+ vm.tabFlow.value = navItem
+ },
+ icon = {
+ Icon(
+ painter = painterResource(id = navItem.icon),
+ contentDescription = navItem.label,
+ modifier = Modifier.padding(2.dp)
+ )
+ },
+ label = {
+ Text(text = navItem.label)
+ })
+ }
+ }
+ }, content = { padding ->
+ Box(modifier = Modifier.padding(padding)) {
+ when (tab) {
+ subsNav -> SubsManagePage()
+ controlNav -> ControlPage()
+ settingsNav -> SettingsPage()
+ }
+ }
+ })
+}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/ui/ImagePreviewPage.kt b/app/src/main/java/li/songe/gkd/ui/ImagePreviewPage.kt
index 3427c78be..ab5bbfcd9 100644
--- a/app/src/main/java/li/songe/gkd/ui/ImagePreviewPage.kt
+++ b/app/src/main/java/li/songe/gkd/ui/ImagePreviewPage.kt
@@ -4,8 +4,11 @@ import android.graphics.Bitmap
import android.graphics.BitmapFactory
import androidx.activity.ComponentActivity
import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -14,21 +17,25 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsCompat
import com.blankj.utilcode.util.ToastUtils
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootNavGraph
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.withContext
-import li.songe.gkd.utils.LaunchedEffectTry
+import li.songe.gkd.util.LaunchedEffectTry
@RootNavGraph
@Destination
@Composable
fun ImagePreviewPage(
- filePath: String?
+ filePath: String?,
) {
val context = LocalContext.current as ComponentActivity
val scope = rememberCoroutineScope()
+
var bitmap by remember {
mutableStateOf(null)
}
@@ -39,12 +46,27 @@ fun ImagePreviewPage(
ToastUtils.showShort("图片路径缺失")
}
}
-
- bitmap?.let { bitmapVal ->
- Image(
- bitmap = bitmapVal.asImageBitmap(),
- contentDescription = null,
- Modifier.fillMaxWidth()
- )
+ DisposableEffect(Unit) {
+ val window = context.window
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ val insetsController = WindowCompat.getInsetsController(window, window.decorView)
+ insetsController.hide(WindowInsetsCompat.Type.statusBars())
+ onDispose {
+ WindowCompat.setDecorFitsSystemWindows(window, true)
+ insetsController.show(WindowInsetsCompat.Type.statusBars())
+ }
+ }
+ Column(
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ bitmap?.let { bitmapVal ->
+ Image(
+ bitmap = bitmapVal.asImageBitmap(),
+ contentDescription = null,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(0.dp)
+ )
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/ui/RecordPage.kt b/app/src/main/java/li/songe/gkd/ui/RecordPage.kt
index 80d424412..0623d1847 100644
--- a/app/src/main/java/li/songe/gkd/ui/RecordPage.kt
+++ b/app/src/main/java/li/songe/gkd/ui/RecordPage.kt
@@ -1,13 +1,175 @@
package li.songe.gkd.ui
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.material.Scaffold
+import androidx.compose.material.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import androidx.hilt.navigation.compose.hiltViewModel
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootNavGraph
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.flowOn
+import li.songe.gkd.data.SubsItem
+import li.songe.gkd.data.TriggerLog
+import li.songe.gkd.data.getAppInfo
+import li.songe.gkd.data.getAppName
import li.songe.gkd.db.DbSet
+import li.songe.gkd.ui.component.SimpleTopAppBar
+import li.songe.gkd.ui.destinations.AppItemPageDestination
+import li.songe.gkd.util.LaunchedEffectTry
+import li.songe.gkd.util.LocalNavController
+import li.songe.gkd.util.Singleton
+import li.songe.gkd.util.launchAsFn
+import li.songe.gkd.util.rememberCache
+import com.ramcosta.composedestinations.navigation.navigate
+import li.songe.gkd.util.format
@RootNavGraph
@Destination
@Composable
fun RecordPage() {
- DbSet.triggerLogDao
+ val scope = rememberCoroutineScope()
+ val navController = LocalNavController.current
+
+ val vm = hiltViewModel()
+ val triggerLogs by vm.triggerLogsFlow.collectAsState()
+ val subItems by vm.subItemsFlow.collectAsState(initial = emptyList())
+
+ val groups = remember(triggerLogs, subItems) {
+ triggerLogs.map { logWrapper ->
+ val sub = subItems.find { sub -> sub.id == logWrapper.subsId } ?: return@map null
+ val app = sub.subscriptionRaw?.apps?.find { app -> app.id == logWrapper.appId }
+ app?.groups?.find { group -> group.key == logWrapper.groupKey }
+ }
+ }
+ val rules = remember(groups) {
+ groups.mapIndexed { index, groupRaw ->
+ groupRaw ?: return@mapIndexed null
+ val log = triggerLogs.getOrNull(index)
+ log?.run {
+ if (ruleKey != null) {
+ groupRaw.rules.find { r -> r.key == ruleKey }?.let { return@mapIndexed it }
+ }
+ groupRaw.rules.getOrNull(ruleIndex)
+ }
+ }
+ }
+ var previewTriggerLog by remember {
+ mutableStateOf(null)
+ }
+
+ Scaffold(topBar = {
+ SimpleTopAppBar(
+ onClickIcon = { navController.popBackStack() },
+ title = "触发记录" + if (triggerLogs.isEmpty()) "" else ("-" + triggerLogs.size.toString())
+ )
+ }, content = { contentPadding ->
+ LazyColumn(
+ modifier = Modifier
+ .padding(10.dp, 0.dp, 10.dp, 0.dp)
+ .padding(contentPadding),
+ verticalArrangement = Arrangement.spacedBy(10.dp),
+ ) {
+ item {
+ Spacer(modifier = Modifier.height(5.dp))
+ }
+ itemsIndexed(triggerLogs) { index, triggerLog ->
+ Column(modifier = Modifier
+ .fillMaxWidth()
+ .border(BorderStroke(1.dp, Color.Black))
+ .clickable {
+ previewTriggerLog = triggerLog
+ }) {
+ Row {
+ Text(
+ text = triggerLog.id.format("yyyy-MM-dd HH:mm:ss"),
+ fontFamily = FontFamily.Monospace
+ )
+ Spacer(modifier = Modifier.width(10.dp))
+ Text(
+ text = getAppName(triggerLog.appId) ?: triggerLog.appId ?: ""
+ )
+ }
+ Spacer(modifier = Modifier.width(10.dp))
+ Text(text = triggerLog.activityId ?: "")
+ groups.getOrNull(index)?.name?.let { groupName ->
+ Spacer(modifier = Modifier.width(10.dp))
+ Text(text = groupName)
+ }
+ rules.getOrNull(index)?.name?.let { ruleName ->
+ Spacer(modifier = Modifier.width(10.dp))
+ Text(text = ruleName)
+ }
+ rules.getOrNull(index)?.matches?.lastOrNull()?.let { matchText ->
+ Spacer(modifier = Modifier.width(10.dp))
+ Text(text = matchText)
+ }
+ }
+ }
+ item {
+ Spacer(modifier = Modifier.height(10.dp))
+ }
+ }
+ })
+
+ previewTriggerLog?.let { previewTriggerLogVal ->
+ Dialog(onDismissRequest = { previewTriggerLog = null }) {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ modifier = Modifier
+ .width(250.dp)
+ .background(Color.White)
+ .padding(8.dp)
+ ) {
+ Text(text = "查看规则组", modifier = Modifier
+ .clickable {
+ previewTriggerLogVal.appId ?: return@clickable
+ navController.navigate(
+ AppItemPageDestination(
+ previewTriggerLogVal.subsId,
+ previewTriggerLogVal.appId,
+ previewTriggerLogVal.groupKey
+ )
+ )
+ previewTriggerLog = null
+ }
+ .fillMaxWidth()
+ .padding(10.dp))
+ Text(text = "删除", modifier = Modifier
+ .clickable(onClick = scope.launchAsFn {
+ previewTriggerLog = null
+ DbSet.triggerLogDb
+ .triggerLogDao()
+ .delete(previewTriggerLogVal)
+ })
+ .fillMaxWidth()
+ .padding(10.dp))
+ }
+ }
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/ui/RecordVm.kt b/app/src/main/java/li/songe/gkd/ui/RecordVm.kt
new file mode 100644
index 000000000..0ae30f726
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/ui/RecordVm.kt
@@ -0,0 +1,22 @@
+package li.songe.gkd.ui
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.withContext
+import li.songe.gkd.db.DbSet
+import javax.inject.Inject
+
+@HiltViewModel
+class RecordVm @Inject constructor() : ViewModel() {
+ val triggerLogsFlow = DbSet.triggerLogDb.triggerLogDao().query().stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
+ val subItemsFlow = DbSet.subsItemDao.query().onEach {
+ withContext(Dispatchers.IO) {
+ it.forEach { subs -> subs.subscriptionRaw }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/ui/SettingsPage.kt b/app/src/main/java/li/songe/gkd/ui/SettingsPage.kt
new file mode 100644
index 000000000..52b50c8c9
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/ui/SettingsPage.kt
@@ -0,0 +1,266 @@
+package li.songe.gkd.ui
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.media.projection.MediaProjectionManager
+import android.net.Uri
+import android.provider.Settings
+import androidx.activity.ComponentActivity
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Scaffold
+import androidx.compose.material.Text
+import androidx.compose.material.TextField
+import androidx.compose.material.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import androidx.core.content.ContextCompat
+import com.blankj.utilcode.util.ToastUtils
+import com.dylanc.activityresult.launcher.launchForResult
+import li.songe.gkd.MainActivity
+import li.songe.gkd.debug.FloatingService
+import li.songe.gkd.debug.HttpService
+import li.songe.gkd.debug.ScreenshotService
+import li.songe.gkd.shizuku.shizukuIsSafeOK
+import li.songe.gkd.ui.component.SettingItem
+import li.songe.gkd.ui.component.TextSwitch
+import li.songe.gkd.util.Ext
+import li.songe.gkd.util.LocalLauncher
+import li.songe.gkd.util.SafeR
+import li.songe.gkd.util.launchAsFn
+import li.songe.gkd.util.storeFlow
+import li.songe.gkd.util.updateStore
+import li.songe.gkd.util.usePollState
+import rikka.shizuku.Shizuku
+
+val settingsNav = BottomNavItem(
+ label = "设置", icon = SafeR.ic_cog, route = "settings"
+)
+
+@Composable
+fun SettingsPage() {
+ val context = LocalContext.current as MainActivity
+ val launcher = LocalLauncher.current
+ val scope = rememberCoroutineScope()
+
+ val store by storeFlow.collectAsState()
+
+
+ var showPortDlg by remember {
+ mutableStateOf(false)
+ }
+
+ Scaffold(topBar = {
+ TopAppBar(backgroundColor = Color(0xfff8f9f9), title = {
+ Text(
+ text = "设置", color = Color.Black
+ )
+ })
+ }, content = { contentPadding ->
+ Column(
+ modifier = Modifier
+ .verticalScroll(
+ state = rememberScrollState()
+ )
+ .padding(0.dp, 10.dp)
+ .padding(contentPadding)
+ ) {
+
+ val shizukuIsOk by usePollState { shizukuIsSafeOK() }
+ TextSwitch(name = "Shizuku授权",
+ desc = "高级运行模式,能更准确识别界面活动ID",
+ shizukuIsOk,
+ onCheckedChange = scope.launchAsFn {
+ if (!it) return@launchAsFn
+ try {
+ Shizuku.requestPermission(Activity.RESULT_OK)
+ } catch (e: Exception) {
+ ToastUtils.showShort("Shizuku可能没有运行")
+ }
+ })
+
+ val canDrawOverlays by usePollState {
+ Settings.canDrawOverlays(context)
+ }
+ Spacer(modifier = Modifier.height(5.dp))
+ TextSwitch(name = "悬浮窗授权",
+ desc = "用于后台提示,主动保存快照等功能",
+ canDrawOverlays,
+ onCheckedChange = scope.launchAsFn {
+ if (!Settings.canDrawOverlays(context)) {
+ val intent = Intent(
+ Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
+ Uri.parse("package:$context.packageName")
+ )
+ launcher.launch(intent) { resultCode, _ ->
+ if (resultCode != ComponentActivity.RESULT_OK) return@launch
+ if (!Settings.canDrawOverlays(context)) return@launch
+ val intent1 = Intent(context, FloatingService::class.java)
+ ContextCompat.startForegroundService(context, intent1)
+ }
+ }
+ })
+
+ val httpServerRunning by usePollState { HttpService.isRunning() }
+ TextSwitch(
+ name = "HTTP服务",
+ desc = "开启HTTP服务, 以便在同一局域网下传递数据" + if (httpServerRunning) "\n${
+ Ext.getIpAddressInLocalNetwork()
+ .map { host -> "http://${host}:${store.httpServerPort}" }.joinToString(",")
+ }" else "\n暂无地址",
+ httpServerRunning
+ ) {
+ if (it) {
+ HttpService.start()
+ } else {
+ HttpService.stop()
+ }
+ }
+
+ SettingItem(title = "HTTP服务端口-${store.httpServerPort}") {
+ showPortDlg = true
+ }
+
+ val screenshotRunning by usePollState { ScreenshotService.isRunning() }
+ TextSwitch(name = "截屏服务",
+ desc = "生成快照需要截取屏幕,Android>=11无需开启",
+ screenshotRunning,
+ scope.launchAsFn {
+ if (it) {
+ val mediaProjectionManager =
+ context.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
+ val activityResult =
+ launcher.launchForResult(mediaProjectionManager.createScreenCaptureIntent())
+ if (activityResult.resultCode == Activity.RESULT_OK && activityResult.data != null) {
+ ScreenshotService.start(intent = activityResult.data!!)
+ }
+ } else {
+ ScreenshotService.stop()
+ }
+ })
+
+ val floatingRunning by usePollState {
+ FloatingService.isRunning()
+ }
+ TextSwitch(name = "悬浮窗服务", desc = "便于用户主动保存快照", floatingRunning) {
+ if (it) {
+ if (Settings.canDrawOverlays(context)) {
+ val intent = Intent(context, FloatingService::class.java)
+ ContextCompat.startForegroundService(context, intent)
+ } else {
+ val intent = Intent(
+ Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
+ Uri.parse("package:$context.packageName")
+ )
+ launcher.launch(intent) { resultCode, _ ->
+ if (resultCode != ComponentActivity.RESULT_OK) return@launch
+ if (!Settings.canDrawOverlays(context)) return@launch
+ val intent1 = Intent(context, FloatingService::class.java)
+ ContextCompat.startForegroundService(context, intent1)
+ }
+ }
+ } else {
+ FloatingService.stop(context)
+ }
+ }
+
+
+ TextSwitch(name = "隐藏后台",
+ desc = "在[最近任务]界面中隐藏本应用",
+ checked = store.excludeFromRecents,
+ onCheckedChange = {
+ updateStore(
+ store.copy(
+ excludeFromRecents = it
+ )
+ )
+ })
+
+ TextSwitch(name = "日志输出",
+ desc = "保持日志输出到控制台",
+ checked = store.enableConsoleLogOut,
+ onCheckedChange = {
+ updateStore(
+ store.copy(
+ enableConsoleLogOut = it
+ )
+ )
+ })
+
+
+ Spacer(modifier = Modifier.height(5.dp))
+ TextSwitch(
+ "自动快照",
+ "当用户截屏时,自动保存当前界面的快照,目前仅支持miui",
+ store.enableCaptureScreenshot
+ ) {
+ updateStore(
+ store.copy(
+ enableCaptureScreenshot = it
+ )
+ )
+ }
+ }
+ })
+
+ if (showPortDlg) {
+ Dialog(onDismissRequest = { showPortDlg = false }) {
+ var value by remember {
+ mutableStateOf(store.httpServerPort.toString())
+ }
+ Column(
+ modifier = Modifier
+ .padding(10.dp)
+ .width(300.dp)
+ ) {
+ TextField(value = value, onValueChange = { value = it.trim() }, singleLine = true)
+ Spacer(modifier = Modifier.height(10.dp))
+ Row(
+ horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth()
+ ) {
+ Text(text = "取消",
+ modifier = Modifier
+ .clickable { showPortDlg = false }
+ .padding(5.dp))
+ Spacer(modifier = Modifier.width(5.dp))
+ Text(text = "确认", modifier = Modifier
+ .clickable {
+ val newPort = value.toIntOrNull()
+ if (newPort == null || !(5000 <= newPort && newPort <= 65535)) {
+ ToastUtils.showShort("请输入在 5000~65535 的任意数字")
+ return@clickable
+ }
+ updateStore(
+ store.copy(
+ httpServerPort = newPort
+ )
+ )
+ showPortDlg = false
+ }
+ .padding(5.dp))
+ }
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/ui/SnapshotPage.kt b/app/src/main/java/li/songe/gkd/ui/SnapshotPage.kt
index f2f05ebcb..d8fe3a9f8 100644
--- a/app/src/main/java/li/songe/gkd/ui/SnapshotPage.kt
+++ b/app/src/main/java/li/songe/gkd/ui/SnapshotPage.kt
@@ -16,9 +16,12 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -31,77 +34,80 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.core.content.FileProvider
+import androidx.hilt.navigation.compose.hiltViewModel
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootNavGraph
-import kotlinx.coroutines.Dispatchers
+import com.ramcosta.composedestinations.navigation.navigate
import kotlinx.coroutines.Dispatchers.IO
-import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.flow.firstOrNull
-import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.withContext
import li.songe.gkd.data.Snapshot
import li.songe.gkd.db.DbSet
import li.songe.gkd.debug.SnapshotExt
-import li.songe.gkd.ui.component.StatusBar
-import li.songe.gkd.utils.launchAsFn
-import li.songe.gkd.utils.Singleton
+import li.songe.gkd.ui.component.SimpleTopAppBar
+import li.songe.gkd.ui.destinations.ImagePreviewPageDestination
+import li.songe.gkd.util.LocalNavController
+import li.songe.gkd.util.format
+import li.songe.gkd.util.launchAsFn
@RootNavGraph
@Destination
@Composable
fun SnapshotPage() {
- val context = LocalContext.current as ComponentActivity
val scope = rememberCoroutineScope()
+ val context = LocalContext.current as ComponentActivity
+ val navController = LocalNavController.current
+
+ val vm = hiltViewModel()
+ val snapshots by vm.snapshotsState.collectAsState()
- var snapshots by remember {
- mutableStateOf(listOf())
- }
var selectedSnapshot by remember {
mutableStateOf(null)
}
- LaunchedEffect(Unit) {
- DbSet.snapshotDao.query().flowOn(Dispatchers.IO).collect {
- snapshots = it.reversed()
- }
- }
- LazyColumn(
- modifier = Modifier.padding(10.dp, 0.dp, 10.dp, 0.dp),
- verticalArrangement = Arrangement.spacedBy(10.dp),
- ) {
- item {
- Text(text = "存在 ${snapshots.size} 条快照记录")
- }
- items(snapshots.size) { i ->
- Column(
- modifier = Modifier
+
+ val scrollState = rememberLazyListState()
+
+ Scaffold(topBar = {
+ SimpleTopAppBar(
+ onClickIcon = { navController.popBackStack() },
+ title = if (snapshots.isEmpty()) "快照记录" else "快照记录-${snapshots.size}",
+ )
+ }, content = { contentPadding ->
+ LazyColumn(
+ modifier = Modifier
+ .padding(10.dp, 0.dp, 10.dp, 0.dp)
+ .padding(contentPadding),
+ state = scrollState,
+ verticalArrangement = Arrangement.spacedBy(10.dp),
+ ) {
+ items(snapshots, { it.id }) { snapshot ->
+ Column(modifier = Modifier
.fillMaxWidth()
.border(BorderStroke(1.dp, Color.Black))
.clickable {
- selectedSnapshot = snapshots[i]
+ selectedSnapshot = snapshot
+ }) {
+ Row {
+ Text(
+ text = snapshot.id.format("yyyy-MM-dd HH:mm:ss"),
+ fontFamily = FontFamily.Monospace
+ )
+ Spacer(modifier = Modifier.width(10.dp))
+ Text(text = snapshot.appName ?: "")
}
- ) {
- Row {
- Text(
- text = Singleton.simpleDateFormat.format(snapshots[i].id),
- fontFamily = FontFamily.Monospace
- )
Spacer(modifier = Modifier.width(10.dp))
- Text(text = snapshots[i].appName ?: "")
+ Text(text = snapshot.appId ?: "")
+ Spacer(modifier = Modifier.width(10.dp))
+ Text(text = snapshot.activityId ?: "")
}
- Spacer(modifier = Modifier.width(10.dp))
- Text(text = snapshots[i].appId ?: "")
- Spacer(modifier = Modifier.width(10.dp))
- Text(text = snapshots[i].activityId ?: "")
+ }
+ item {
+ Spacer(modifier = Modifier.height(10.dp))
}
}
- item {
- Spacer(modifier = Modifier.height(10.dp))
- }
- }
+ })
+
selectedSnapshot?.let { snapshot ->
- Dialog(
- onDismissRequest = { selectedSnapshot = null }
- ) {
+ Dialog(onDismissRequest = { selectedSnapshot = null }) {
Box(
Modifier
.width(200.dp)
@@ -115,10 +121,11 @@ fun SnapshotPage() {
Text(
text = "查看", modifier = Modifier
.clickable(onClick = scope.launchAsFn {
-// router.navigate(
-// ImagePreviewPage,
-// SnapshotExt.getScreenshotPath(snapshot.id)
-// )
+ navController.navigate(
+ ImagePreviewPageDestination(
+ filePath = snapshot.screenshotFile.absolutePath
+ )
+ )
selectedSnapshot = null
})
.then(modifier)
@@ -128,9 +135,7 @@ fun SnapshotPage() {
.clickable(onClick = scope.launchAsFn {
val zipFile = SnapshotExt.getSnapshotZipFile(snapshot.id)
val uri = FileProvider.getUriForFile(
- context,
- "${context.packageName}.provider",
- zipFile
+ context, "${context.packageName}.provider", zipFile
)
val intent = Intent().apply {
action = Intent.ACTION_SEND
diff --git a/app/src/main/java/li/songe/gkd/ui/SnapshotVm.kt b/app/src/main/java/li/songe/gkd/ui/SnapshotVm.kt
new file mode 100644
index 000000000..3d84474f8
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/ui/SnapshotVm.kt
@@ -0,0 +1,16 @@
+package li.songe.gkd.ui
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import li.songe.gkd.db.DbSet
+import javax.inject.Inject
+
+@HiltViewModel
+class SnapshotVm @Inject constructor() : ViewModel() {
+ val snapshotsState = DbSet.snapshotDao.query().map { it.reversed() }
+ .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
+}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/ui/home/SubsManagePage.kt b/app/src/main/java/li/songe/gkd/ui/SubsManagePage.kt
similarity index 55%
rename from app/src/main/java/li/songe/gkd/ui/home/SubsManagePage.kt
rename to app/src/main/java/li/songe/gkd/ui/SubsManagePage.kt
index 6b6102a04..3cbbcb70b 100644
--- a/app/src/main/java/li/songe/gkd/ui/home/SubsManagePage.kt
+++ b/app/src/main/java/li/songe/gkd/ui/SubsManagePage.kt
@@ -1,16 +1,44 @@
-package li.songe.gkd.ui.home
+package li.songe.gkd.ui
import android.webkit.URLUtil
-import androidx.compose.foundation.*
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.AlertDialog
import androidx.compose.material.Button
import androidx.compose.material.Card
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.FloatingActionButton
+import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TextField
-import androidx.compose.runtime.*
+import androidx.compose.material.TopAppBar
+import androidx.compose.material.pullrefresh.PullRefreshIndicator
+import androidx.compose.material.pullrefresh.pullRefresh
+import androidx.compose.material.pullrefresh.rememberPullRefreshState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -19,90 +47,55 @@ import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.viewModelScope
import com.blankj.utilcode.util.ClipboardUtils
import com.blankj.utilcode.util.ToastUtils
import com.google.zxing.BarcodeFormat
import com.ramcosta.composedestinations.navigation.navigate
-import io.ktor.client.request.*
-import io.ktor.client.statement.*
+import io.ktor.client.request.get
+import io.ktor.client.statement.bodyAsText
import kotlinx.coroutines.Dispatchers.IO
-import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import li.songe.gkd.data.SubsItem
import li.songe.gkd.data.SubscriptionRaw
import li.songe.gkd.db.DbSet
import li.songe.gkd.ui.component.SubsItemCard
import li.songe.gkd.ui.destinations.SubsPageDestination
-import li.songe.gkd.utils.LaunchedEffectTry
-import li.songe.gkd.utils.LocalNavController
-import li.songe.gkd.utils.SafeR
-import li.songe.gkd.utils.Singleton
-import li.songe.gkd.utils.launchAsFn
-import li.songe.gkd.utils.rememberCache
-import li.songe.gkd.utils.useNavigateForQrcodeResult
-import li.songe.gkd.utils.useTask
+import li.songe.gkd.util.LocalNavController
+import li.songe.gkd.util.SafeR
+import li.songe.gkd.util.Singleton
+import li.songe.gkd.util.launchAsFn
+import li.songe.gkd.util.useNavigateForQrcodeResult
+import li.songe.gkd.util.useTask
-@OptIn(ExperimentalFoundationApi::class)
+val subsNav = BottomNavItem(
+ label = "订阅", icon = SafeR.ic_link, route = "subscription"
+)
+
+@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class)
@Composable
-fun SubscriptionManagePage() {
+fun SubsManagePage() {
val scope = rememberCoroutineScope()
val navController = LocalNavController.current
-
- var subItems by rememberCache { mutableStateOf(listOf()) }
- var shareSubItem: SubsItem? by rememberCache { mutableStateOf(null) }
- var shareQrcode: ImageBitmap? by rememberCache { mutableStateOf(null) }
- var deleteSubItem: SubsItem? by rememberCache { mutableStateOf(null) }
- var moveSubItem: SubsItem? by rememberCache { mutableStateOf(null) }
-
- var showAddDialog by rememberCache { mutableStateOf(false) }
-
- var showLinkDialog by rememberCache { mutableStateOf(false) }
- var link by rememberCache { mutableStateOf("") }
-
val navigateForQrcodeResult = useNavigateForQrcodeResult()
+ val vm = hiltViewModel()
+ val subItems by vm.subsItemsFlow.collectAsState()
- LaunchedEffectTry(Unit) {
- DbSet.subsItemDao.query().flowOn(IO).collect {
- subItems = it
- }
- }
+ var shareSubItem: SubsItem? by remember { mutableStateOf(null) }
+ var shareQrcode: ImageBitmap? by remember { mutableStateOf(null) }
+ var deleteSubItem: SubsItem? by remember { mutableStateOf(null) }
+ var menuSubItem: SubsItem? by remember { mutableStateOf(null) }
- val addSubs = scope.useTask(dialog = true).launchAsFn> { urls ->
- val safeUrls = urls.filter { url ->
- URLUtil.isNetworkUrl(url) && subItems.all { it.updateUrl != url }
- }
- if (safeUrls.isEmpty()) return@launchAsFn
- onChangeLoading(true)
- val newItems = safeUrls.mapIndexedNotNull { index, url ->
- try {
- val text = Singleton.client.get(url).bodyAsText()
- val subscriptionRaw = SubscriptionRaw.parse5(text)
- val newItem = SubsItem(
- updateUrl = subscriptionRaw.updateUrl ?: url,
- name = subscriptionRaw.name,
- version = subscriptionRaw.version,
- order = index + 1 + subItems.size
- )
- withContext(IO) {
- newItem.subsFile.writeText(text)
- }
- newItem
- } catch (e: Exception) {
- null
- }
- }
- if (newItems.isNotEmpty()) {
- DbSet.subsItemDao.insert(*newItems.toTypedArray())
- ToastUtils.showShort("成功添加 ${newItems.size} 条订阅")
- } else {
- ToastUtils.showShort("添加失败")
- }
- }
+ var showAddDialog by remember { mutableStateOf(false) }
+ var showAddLinkDialog by remember { mutableStateOf(false) }
+ var link by remember { mutableStateOf("") }
- val updateSubs = scope.useTask(dialog = true).launchAsFn> { oldItems ->
- if (oldItems.isEmpty()) return@launchAsFn
- onChangeLoading(true)
- val newItems = oldItems.mapNotNull { oldItem ->
+
+ val refreshing = scope.useTask()
+ val pullRefreshState = rememberPullRefreshState(refreshing.loading, refreshing.launchAsFn(IO) {
+ val newItems = subItems.mapNotNull { oldItem ->
try {
val subscriptionRaw = SubscriptionRaw.parse5(
Singleton.client.get(oldItem.updateUrl).bodyAsText()
@@ -135,81 +128,75 @@ fun SubscriptionManagePage() {
DbSet.subsItemDao.update(*newItems.toTypedArray())
ToastUtils.showShort("更新 ${newItems.size} 条订阅")
}
- }
+ })
- LazyColumn(
- verticalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier.fillMaxHeight()
- ) {
- item(subItems) {
- Row(
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier
- .fillMaxWidth()
- .padding(10.dp, 0.dp)
+ Scaffold(
+ topBar = {
+ TopAppBar(backgroundColor = Color(0xfff8f9f9), title = {
+ Text(
+ text = "订阅", color = Color.Black
+ )
+ })
+ },
+ floatingActionButton = {
+ FloatingActionButton(onClick = {}) {
+ Image(painter = painterResource(SafeR.ic_add),
+ contentDescription = "add_subs_item",
+ modifier = Modifier
+ .clickable {
+ showAddDialog = true
+ }
+ .padding(4.dp)
+ .size(25.dp))
+ }
+ },
+ ) { padding ->
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(padding)
+ .pullRefresh(pullRefreshState)
+ ) {
+ LazyColumn(
+ modifier = Modifier.fillMaxHeight(),
+ verticalArrangement = Arrangement.spacedBy(4.dp),
) {
- if (subItems.isEmpty()) {
- Text(
- text = "暂无订阅",
- )
- } else {
- Text(
- text = "共有${subItems.size}条订阅,激活:${subItems.count { it.enable }},禁用:${subItems.count { !it.enable }}",
- )
- }
- Row {
- Image(painter = painterResource(SafeR.ic_add),
- contentDescription = "",
+ items(subItems, { it.id }) { subItem ->
+ Card(
modifier = Modifier
- .clickable {
- showAddDialog = true
- }
- .padding(4.dp)
- .size(25.dp))
- Image(
- painter = painterResource(SafeR.ic_refresh),
- contentDescription = "",
- modifier = Modifier
- .clickable(onClick = {
- updateSubs(subItems)
- })
- .padding(4.dp)
- .size(25.dp)
- )
+ .animateItemPlacement()
+ .padding(vertical = 3.dp, horizontal = 8.dp)
+ .clickable(
+ onClick = scope
+ .useTask()
+ .launchAsFn {
+ navController.navigate(SubsPageDestination(subItem.id))
+ }),
+ elevation = 0.dp,
+ border = BorderStroke(1.dp, Color(0xfff6f6f6)),
+ shape = RoundedCornerShape(8.dp),
+ ) {
+ SubsItemCard(
+ subsItem = subItem,
+ onMenuClick = {
+ menuSubItem = subItem
+ },
+ onCheckedChange = scope.launchAsFn {
+ DbSet.subsItemDao.update(subItem.copy(enable = it))
+ },
+ )
+ }
}
}
- }
-
- items(subItems.size) { i ->
- Card(
- modifier = Modifier
- .animateItemPlacement()
- .padding(vertical = 3.dp, horizontal = 8.dp)
- .combinedClickable(
- onClick = scope
- .useTask()
- .launchAsFn {
- navController.navigate(SubsPageDestination(subItems[i]))
- }, onLongClick = {
- if (subItems.size > 1) {
- moveSubItem = subItems[i]
- }
- }),
- elevation = 0.dp,
- border = BorderStroke(1.dp, Color(0xfff6f6f6)),
- shape = RoundedCornerShape(8.dp),
- ) {
- SubsItemCard(subItems[i], onShareClick = {
- shareSubItem = subItems[i]
- }, onDelClick = {
- deleteSubItem = subItems[i]
- }, onRefreshClick = {
- updateSubs(listOf(subItems[i]))
- })
- }
+ PullRefreshIndicator(
+ refreshing = refreshing.loading,
+ state = pullRefreshState,
+ modifier = Modifier.align(Alignment.TopCenter),
+ )
}
}
+
shareSubItem?.let { shareSubItemVal ->
Dialog(onDismissRequest = { shareSubItem = null }) {
Box(
@@ -233,8 +220,8 @@ fun SubscriptionManagePage() {
Text(text = "导出至剪切板", modifier = Modifier
.clickable {
ClipboardUtils.copyText(shareSubItemVal.updateUrl)
- shareSubItem = null
ToastUtils.showShort("复制成功")
+ shareSubItem = null
}
.fillMaxWidth()
.padding(8.dp))
@@ -253,8 +240,8 @@ fun SubscriptionManagePage() {
}
}
- moveSubItem?.let { moveSubItemVal ->
- Dialog(onDismissRequest = { moveSubItem = null }) {
+ menuSubItem?.let { menuSubItemVal ->
+ Dialog(onDismissRequest = { menuSubItem = null }) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
@@ -263,7 +250,21 @@ fun SubscriptionManagePage() {
.background(Color.White)
.padding(8.dp)
) {
- if (subItems.firstOrNull() != moveSubItemVal) {
+ Text(text = "分享", modifier = Modifier
+ .clickable {
+ shareSubItem = menuSubItemVal
+ menuSubItem = null
+ }
+ .fillMaxWidth()
+ .padding(8.dp))
+ Text(text = "删除", modifier = Modifier
+ .clickable {
+ deleteSubItem = menuSubItemVal
+ menuSubItem = null
+ }
+ .fillMaxWidth()
+ .padding(8.dp))
+ if (subItems.firstOrNull() != menuSubItemVal) {
Text(
text = "上移",
modifier = Modifier
@@ -272,22 +273,21 @@ fun SubscriptionManagePage() {
.useTask()
.launchAsFn {
val lastItem =
- subItems[subItems.indexOf(moveSubItemVal) - 1]
+ subItems[subItems.indexOf(menuSubItemVal) - 1]
DbSet.subsItemDao.update(
lastItem.copy(
- order = moveSubItemVal.order
- ),
- moveSubItemVal.copy(
+ order = menuSubItemVal.order
+ ), menuSubItemVal.copy(
order = lastItem.order
)
)
- moveSubItem = null
+ menuSubItem = null
})
.fillMaxWidth()
.padding(8.dp)
)
}
- if (subItems.lastOrNull() != moveSubItemVal) {
+ if (subItems.lastOrNull() != menuSubItemVal) {
Text(
text = "下移",
modifier = Modifier
@@ -296,16 +296,15 @@ fun SubscriptionManagePage() {
.useTask()
.launchAsFn {
val nextItem =
- subItems[subItems.indexOf(moveSubItemVal) + 1]
+ subItems[subItems.indexOf(menuSubItemVal) + 1]
DbSet.subsItemDao.update(
nextItem.copy(
- order = moveSubItemVal.order
- ),
- moveSubItemVal.copy(
+ order = menuSubItemVal.order
+ ), menuSubItemVal.copy(
order = nextItem.order
)
)
- moveSubItem = null
+ menuSubItem = null
})
.fillMaxWidth()
.padding(8.dp)
@@ -345,21 +344,6 @@ fun SubscriptionManagePage() {
.padding(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
- Text(
- text = "默认订阅", modifier = Modifier
- .clickable(onClick = {
- showAddDialog = false
- addSubs(
- listOf(
- "https://cdn.lisonge.com/startup_ad.json",
- "https://cdn.lisonge.com/internal_ad.json",
- "https://cdn.lisonge.com/quick_util.json",
- )
- )
- })
- .fillMaxWidth()
- .padding(8.dp)
- )
Text(
text = "二维码", modifier = Modifier
.clickable(onClick = scope.launchAsFn {
@@ -367,7 +351,7 @@ fun SubscriptionManagePage() {
val qrCode = navigateForQrcodeResult()
val contents = qrCode.contents
if (contents != null) {
- showLinkDialog = true
+ showAddLinkDialog = true
link = contents
}
})
@@ -376,7 +360,7 @@ fun SubscriptionManagePage() {
)
Text(text = "链接", modifier = Modifier
.clickable {
- showLinkDialog = true
+ showAddLinkDialog = true
showAddDialog = false
}
.fillMaxWidth()
@@ -387,13 +371,13 @@ fun SubscriptionManagePage() {
- LaunchedEffect(showLinkDialog) {
- if (!showLinkDialog) {
+ LaunchedEffect(showAddLinkDialog) {
+ if (!showAddLinkDialog) {
link = ""
}
}
- if (showLinkDialog) {
- Dialog(onDismissRequest = { showLinkDialog = false }) {
+ if (showAddLinkDialog) {
+ Dialog(onDismissRequest = { showAddLinkDialog = false }) {
Box(
Modifier
.width(300.dp)
@@ -406,8 +390,13 @@ fun SubscriptionManagePage() {
value = link, onValueChange = { link = it.trim() }, singleLine = true
)
Button(onClick = {
- addSubs(listOf(link))
- showLinkDialog = false
+ if (!URLUtil.isNetworkUrl(link)) {
+ return@Button ToastUtils.showShort("非法链接")
+ }
+ showAddLinkDialog = false
+ vm.viewModelScope.launch {
+ vm.addSubsFromUrl(url = link)
+ }
}) {
Text(text = "添加")
}
diff --git a/app/src/main/java/li/songe/gkd/ui/SubsManageVm.kt b/app/src/main/java/li/songe/gkd/ui/SubsManageVm.kt
new file mode 100644
index 000000000..a15a7c487
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/ui/SubsManageVm.kt
@@ -0,0 +1,63 @@
+package li.songe.gkd.ui
+
+import android.webkit.URLUtil
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.blankj.utilcode.util.ToastUtils
+import dagger.hilt.android.lifecycle.HiltViewModel
+import io.ktor.client.request.get
+import io.ktor.client.statement.bodyAsText
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.withContext
+import li.songe.gkd.data.SubsItem
+import li.songe.gkd.data.SubscriptionRaw
+import li.songe.gkd.db.DbSet
+import li.songe.gkd.util.Singleton
+import javax.inject.Inject
+
+
+@HiltViewModel
+class SubsManageVm @Inject constructor() : ViewModel() {
+ val subsItemsFlow = DbSet.subsItemDao.query().stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
+
+ suspend fun addSubsFromUrl(url: String) {
+ if (!URLUtil.isNetworkUrl(url)) {
+ ToastUtils.showShort("非法链接")
+ return
+ }
+ val subItems = subsItemsFlow.first()
+ if (subItems.any { it.updateUrl == url }) {
+ ToastUtils.showShort("订阅链接已存在")
+ return
+ }
+ val text = try {
+ Singleton.client.get(url).bodyAsText()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ ToastUtils.showShort("下载订阅文件失败")
+ return
+ }
+ val subscriptionRaw = try {
+ SubscriptionRaw.parse5(text)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ ToastUtils.showShort("解析订阅文件失败")
+ return
+ }
+ val newItem = SubsItem(
+ updateUrl = subscriptionRaw.updateUrl ?: url,
+ name = subscriptionRaw.name,
+ version = subscriptionRaw.version,
+ order = subItems.size + 1
+ )
+ withContext(Dispatchers.IO) {
+ newItem.subsFile.writeText(text)
+ }
+ DbSet.subsItemDao.insert(newItem)
+ ToastUtils.showShort("成功添加订阅")
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/ui/SubsPage.kt b/app/src/main/java/li/songe/gkd/ui/SubsPage.kt
index f8965354b..1a8018c54 100644
--- a/app/src/main/java/li/songe/gkd/ui/SubsPage.kt
+++ b/app/src/main/java/li/songe/gkd/ui/SubsPage.kt
@@ -1,156 +1,92 @@
package li.songe.gkd.ui
import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.material.Text
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.Scaffold
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
-import com.google.accompanist.placeholder.PlaceholderHighlight
-import com.google.accompanist.placeholder.material.fade
-import com.google.accompanist.placeholder.material.placeholder
+import androidx.hilt.navigation.compose.hiltViewModel
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootNavGraph
import com.ramcosta.composedestinations.navigation.navigate
-import kotlinx.coroutines.Dispatchers.IO
-import kotlinx.coroutines.flow.cancellable
-import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.withContext
import li.songe.gkd.data.SubsConfig
-import li.songe.gkd.data.SubsItem
-import li.songe.gkd.data.SubscriptionRaw
import li.songe.gkd.data.getAppInfo
import li.songe.gkd.db.DbSet
+import li.songe.gkd.ui.component.SimpleTopAppBar
import li.songe.gkd.ui.component.SubsAppCard
-import li.songe.gkd.ui.component.SubsAppCardData
import li.songe.gkd.ui.destinations.AppItemPageDestination
-import li.songe.gkd.utils.LaunchedEffectTry
-import li.songe.gkd.utils.LocalNavController
-import li.songe.gkd.utils.launchAsFn
-import li.songe.gkd.utils.rememberCache
-import li.songe.gkd.utils.useTask
+import li.songe.gkd.util.LocalNavController
+import li.songe.gkd.util.launchAsFn
+import java.text.Collator
+import java.util.Locale
@RootNavGraph
@Destination
@Composable
fun SubsPage(
- subsItem: SubsItem
+ subsItemId: Long,
) {
val scope = rememberCoroutineScope()
val navController = LocalNavController.current
- var sub: SubscriptionRaw? by rememberCache { mutableStateOf(null) }
- var subsAppCards: List? by rememberCache { mutableStateOf(null) }
+ val vm = hiltViewModel()
+ val subsItem by vm.subsItemFlow.collectAsState()
+ val subsConfigs by vm.subsConfigsFlow.collectAsState(initial = emptyList())
- LaunchedEffectTry(Unit) {
- scope.launchAsFn { }
- val newSub = if (sub === null) {
- SubscriptionRaw.parse5(subsItem.subsFile.readText()).apply {
- withContext(IO) {
- apps.forEach {
- getAppInfo(it.id)
- }
- }
- }
- } else {
- sub!!
- }
- sub = newSub
- DbSet.subsConfigDao.queryAppTypeConfig(subsItem.id).flowOn(IO).cancellable().collect {
- val mutableSet = it.toMutableSet()
- val newSubsAppCards = newSub.apps.map { appRaw ->
- mutableSet.firstOrNull { v ->
- v.appId == appRaw.id
- }.apply {
- mutableSet.remove(this)
- } ?: SubsConfig(
- subsItemId = subsItem.id,
- appId = appRaw.id,
- type = SubsConfig.AppType
- )
- }.mapIndexed { index, subsConfig ->
- SubsAppCardData(
- subsConfig,
- newSub.apps[index]
- )
+ val orderedApps by remember(subsItem) {
+ derivedStateOf {
+ (subsItem?.subscriptionRaw?.apps ?: emptyList()).sortedWith { a, b ->
+ Collator.getInstance(Locale.CHINESE)
+ .compare(getAppInfo(a.id).realName, getAppInfo(b.id).realName)
}
- subsAppCards = newSubsAppCards
}
}
-
- val openAppPage = scope.useTask().launchAsFn {
- navController.navigate(AppItemPageDestination(it.appRaw, it.subsConfig))
- }
-
- LazyColumn(
- verticalArrangement = Arrangement.spacedBy(0.dp),
- modifier = Modifier
- .fillMaxSize()
- ) {
- item {
- val textModifier = Modifier
- .fillMaxWidth()
- .placeholder(visible = sub == null, highlight = PlaceholderHighlight.fade())
- Column(
- modifier = Modifier.padding(10.dp, 0.dp),
- verticalArrangement = Arrangement.spacedBy(5.dp)
+// val openAppPage = scope.useTask().launchAsFn {
+// navController.navigate(AppItemPageDestination(it.subsConfig.subsItemId, it.appRaw.id))
+// }
+ Scaffold(
+ topBar = {
+ SimpleTopAppBar(
+ onClickIcon = { navController.popBackStack() }, title = subsItem?.name ?: ""
+ )
+// 右上角菜单显示关于 dialog 一级属性
+ },
+ ) { padding ->
+ subsItem?.subscriptionRaw?.let {
+ LazyColumn(
+ verticalArrangement = Arrangement.spacedBy(0.dp),
+ modifier = Modifier.padding(padding)
) {
- Text(
- text = "作者: " + (sub?.author ?: "未知"),
- modifier = textModifier
- )
- Text(
- text = "版本: ${sub?.version}",
- modifier = textModifier
- )
- Text(
- text = "描述: ${sub?.name}",
- modifier = textModifier
- )
- }
- }
- subsAppCards?.let { subsAppCardsVal ->
- items(subsAppCardsVal.size) { i ->
- SubsAppCard(
- sub = subsAppCardsVal[i],
- onClick = {
- openAppPage(subsAppCardsVal[i])
- },
- onValueChange = scope.launchAsFn { enable ->
- val newItem = subsAppCardsVal[i].subsConfig.copy(
+ items(orderedApps, { it.id }) { appRaw ->
+ val subsConfig = subsConfigs.find { s -> s.appId == appRaw.id }
+ SubsAppCard(appRaw = appRaw, subsConfig = subsConfig, onClick = {
+ navController.navigate(AppItemPageDestination(subsItemId, appRaw.id))
+ }, onValueChange = scope.launchAsFn { enable ->
+ val newItem = subsConfig?.copy(
enable = enable
+ ) ?: SubsConfig(
+ enable = enable,
+ type = SubsConfig.AppType,
+ subsItemId = subsItemId,
+ appId = appRaw.id,
)
DbSet.subsConfigDao.insert(newItem)
- }
- )
+ })
+ }
+ item(null) {
+ Spacer(modifier = Modifier.height(10.dp))
+ }
}
}
-// if (subsAppCards == null) {
-// items(placeholderList.size) { i ->
-// Box(
-// modifier = Modifier
-// .wrapContentSize()
-// ) {
-// SubsAppCard(loading = true, sub = placeholderList[i])
-// Text(text = "")
-// }
-// }
-// }
-
- item(true) {
- Spacer(modifier = Modifier.height(10.dp))
- }
}
-
}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/ui/SubsVm.kt b/app/src/main/java/li/songe/gkd/ui/SubsVm.kt
new file mode 100644
index 000000000..00bdd21e9
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/ui/SubsVm.kt
@@ -0,0 +1,31 @@
+package li.songe.gkd.ui
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+import li.songe.gkd.data.SubsItem
+import li.songe.gkd.data.getAppInfo
+import li.songe.gkd.db.DbSet
+import li.songe.gkd.ui.destinations.SubsPageDestination
+import javax.inject.Inject
+
+@HiltViewModel
+class SubsVm @Inject constructor(stateHandle: SavedStateHandle) : ViewModel() {
+ private val args = SubsPageDestination.argsFrom(stateHandle)
+ val subsItemFlow = MutableStateFlow(null)
+ val subsConfigsFlow = DbSet.subsConfigDao.queryAppTypeConfig(args.subsItemId)
+ .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
+
+ init {
+ viewModelScope.launch(Dispatchers.IO) {
+ val subsItem = DbSet.subsItemDao.queryById(args.subsItemId)
+ subsItemFlow.value = subsItem
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/ui/component/SettingItem.kt b/app/src/main/java/li/songe/gkd/ui/component/SettingItem.kt
new file mode 100644
index 000000000..9aa8c7b38
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/ui/component/SettingItem.kt
@@ -0,0 +1,45 @@
+package li.songe.gkd.ui.component
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Icon
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import li.songe.gkd.icon.ArrowIcon
+
+@Composable
+fun SettingItem(
+ title: String,
+ onClick: () -> Unit,
+) {
+ Row(
+ modifier = Modifier
+ .clickable(
+ onClick = onClick
+ )
+ .fillMaxWidth()
+ .padding(10.dp, 10.dp)
+ .height(30.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(text = title, fontSize = 18.sp)
+ Icon(imageVector = ArrowIcon, contentDescription = title)
+ }
+}
+
+@Preview
+@Composable
+fun PreviewSettingItem() {
+ SettingItem(title = "你好", onClick = {})
+}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/ui/component/SimpleTopAppBar.kt b/app/src/main/java/li/songe/gkd/ui/component/SimpleTopAppBar.kt
new file mode 100644
index 000000000..0933911b3
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/ui/component/SimpleTopAppBar.kt
@@ -0,0 +1,47 @@
+package li.songe.gkd.ui.component
+
+import androidx.annotation.DrawableRes
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.Icon
+import androidx.compose.material.Text
+import androidx.compose.material.TopAppBar
+import androidx.compose.material.ripple.rememberRipple
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import li.songe.gkd.util.SafeR
+
+@Composable
+fun SimpleTopAppBar(
+ @DrawableRes iconId: Int = SafeR.ic_back,
+ onClickIcon: (() -> Unit)? = null,
+ title: String,
+) {
+ TopAppBar(backgroundColor = Color(0xfff8f9f9), navigationIcon = {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Icon(painter = painterResource(id = iconId),
+ contentDescription = null,
+ modifier = Modifier
+ .size(30.dp)
+ .clickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = rememberRipple(bounded = false),
+ ) {
+ onClickIcon?.invoke()
+ })
+ }
+ }, title = { Text(text = title) })
+}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/ui/component/SubsAppCard.kt b/app/src/main/java/li/songe/gkd/ui/component/SubsAppCard.kt
index accae8dad..2693f5740 100644
--- a/app/src/main/java/li/songe/gkd/ui/component/SubsAppCard.kt
+++ b/app/src/main/java/li/songe/gkd/ui/component/SubsAppCard.kt
@@ -1,6 +1,5 @@
package li.songe.gkd.ui.component
-import android.graphics.drawable.Drawable
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@@ -23,24 +22,20 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.google.accompanist.drawablepainter.rememberDrawablePainter
-import com.google.accompanist.placeholder.PlaceholderHighlight
-import com.google.accompanist.placeholder.material.fade
-import com.google.accompanist.placeholder.material.placeholder
-import li.songe.gkd.R
import li.songe.gkd.data.SubsConfig
import li.songe.gkd.data.SubscriptionRaw
import li.songe.gkd.data.getAppInfo
-import li.songe.gkd.utils.SafeR
+import li.songe.gkd.util.SafeR
@Composable
fun SubsAppCard(
- loading: Boolean = false,
- sub: SubsAppCardData,
+ appRaw: SubscriptionRaw.AppRaw,
+ subsConfig: SubsConfig? = null,
onClick: (() -> Unit)? = null,
- onValueChange: ((Boolean) -> Unit)? = null
+ onValueChange: ((Boolean) -> Unit)? = null,
) {
- val info = getAppInfo(sub.appRaw.id)
+ val info = getAppInfo(appRaw.id)
Row(
modifier = Modifier
.height(60.dp)
@@ -55,12 +50,9 @@ fun SubsAppCard(
Image(
painter = if (info.icon != null) rememberDrawablePainter(info.icon) else painterResource(
SafeR.ic_app_2
- ),
- contentDescription = null,
- modifier = Modifier
+ ), contentDescription = null, modifier = Modifier
.fillMaxHeight()
.clip(CircleShape)
- .placeholder(loading, highlight = PlaceholderHighlight.fade())
)
Spacer(modifier = Modifier.width(10.dp))
@@ -73,35 +65,28 @@ fun SubsAppCard(
verticalArrangement = Arrangement.SpaceBetween
) {
Text(
- text = info.name ?: "-", maxLines = 1,
+ text = info.realName,
+ maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
- modifier = Modifier
- .fillMaxWidth()
- .placeholder(loading, highlight = PlaceholderHighlight.fade())
+ modifier = Modifier.fillMaxWidth()
)
Text(
- text = sub.appRaw.groups.size.toString() + "组规则", maxLines = 1,
+ text = appRaw.groups.size.toString() + "组规则",
+ maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
- modifier = Modifier
- .fillMaxWidth()
- .placeholder(loading, highlight = PlaceholderHighlight.fade())
+ modifier = Modifier.fillMaxWidth()
)
}
Spacer(modifier = Modifier.width(10.dp))
Switch(
- sub.subsConfig.enable,
+ subsConfig?.enable ?: true,
onValueChange,
- modifier = Modifier.placeholder(loading, highlight = PlaceholderHighlight.fade())
)
}
}
-data class SubsAppCardData(
- val subsConfig: SubsConfig,
- val appRaw: SubscriptionRaw.AppRaw
-)
diff --git a/app/src/main/java/li/songe/gkd/ui/component/SubsItemCard.kt b/app/src/main/java/li/songe/gkd/ui/component/SubsItemCard.kt
index 4b033fef3..3f612720b 100644
--- a/app/src/main/java/li/songe/gkd/ui/component/SubsItemCard.kt
+++ b/app/src/main/java/li/songe/gkd/ui/component/SubsItemCard.kt
@@ -9,11 +9,9 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.Surface
+import androidx.compose.material.Switch
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
@@ -22,19 +20,16 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import li.songe.gkd.data.SubsItem
-import li.songe.gkd.utils.SafeR
-import li.songe.gkd.utils.Singleton
+import li.songe.gkd.util.SafeR
+import li.songe.gkd.util.formatTimeAgo
@Composable
fun SubsItemCard(
subsItem: SubsItem,
- onShareClick: (() -> Unit)? = null,
- onDelClick: (() -> Unit)? = null,
- onRefreshClick: (() -> Unit)? = null,
+ onMenuClick: (() -> Unit)? = null,
+ onCheckedChange: ((Boolean) -> Unit)? = null,
) {
- val dateStr by remember(subsItem) {
- derivedStateOf { "更新于:" + Singleton.simpleDateFormat.format(subsItem.mtime) }
- }
+
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
@@ -42,22 +37,24 @@ fun SubsItemCard(
.alpha(if (subsItem.enable) 1f else .3f),
) {
Column(modifier = Modifier.weight(1f)) {
- Text(
- text = subsItem.name,
- maxLines = 1,
- softWrap = false,
- overflow = TextOverflow.Ellipsis
- )
Row {
Text(
- text = dateStr,
+ text = subsItem.order.toString() + ". " + subsItem.name,
+ maxLines = 1,
+ softWrap = false,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ Row {
+ Text(
+ text = formatTimeAgo(subsItem.mtime) + "更新",
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.width(10.dp))
Text(
- text = "版本:" + subsItem.version,
+ text = "v" + subsItem.version.toString(),
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis
@@ -65,37 +62,47 @@ fun SubsItemCard(
}
}
Spacer(modifier = Modifier.width(5.dp))
- Image(
- painter = painterResource(SafeR.ic_refresh),
+ Image(painter = painterResource(SafeR.ic_menu),
contentDescription = "refresh",
modifier = Modifier
.clickable {
- onRefreshClick?.invoke()
+ onMenuClick?.invoke()
}
- .padding(4.dp)
- .size(20.dp)
- )
- Spacer(modifier = Modifier.width(5.dp))
- Image(
- painter = painterResource(SafeR.ic_share),
- contentDescription = "share",
- modifier = Modifier
- .clickable {
- onShareClick?.invoke()
- }
- .padding(4.dp)
- .size(20.dp)
+ .size(30.dp)
+
)
- Spacer(modifier = Modifier.width(5.dp))
- Image(
- painter = painterResource(SafeR.ic_del),
- contentDescription = "edit",
- modifier = Modifier
- .clickable {
- onDelClick?.invoke()
- }
- .padding(4.dp)
- .size(20.dp)
+// Spacer(modifier = Modifier.width(5.dp))
+// Image(painter = painterResource(SafeR.ic_refresh),
+// contentDescription = "refresh",
+// modifier = Modifier
+// .clickable {
+// onRefreshClick?.invoke()
+// }
+// .padding(4.dp)
+// .size(20.dp))
+// Spacer(modifier = Modifier.width(5.dp))
+// Image(painter = painterResource(SafeR.ic_share),
+// contentDescription = "share",
+// modifier = Modifier
+// .clickable {
+// onShareClick?.invoke()
+// }
+// .padding(4.dp)
+// .size(20.dp))
+// Spacer(modifier = Modifier.width(5.dp))
+// Image(painter = painterResource(SafeR.ic_del),
+// contentDescription = "edit",
+// modifier = Modifier
+// .clickable {
+// onDelClick?.invoke()
+// }
+// .padding(4.dp)
+// .size(20.dp))
+
+ Spacer(modifier = Modifier.width(10.dp))
+ Switch(
+ checked = subsItem.enable,
+ onCheckedChange = onCheckedChange,
)
}
}
@@ -103,11 +110,12 @@ fun SubsItemCard(
@Preview
@Composable
fun PreviewSubscriptionItemCard() {
- Surface(modifier = Modifier.width(300.dp)) {
+ Surface(modifier = Modifier.width(400.dp)) {
SubsItemCard(
SubsItem(
- updateUrl = "https://raw.githubusercontents.com/lisonge/gkd-subscription/main/src/ad-startup.gkd.json",
- name = "APP工具箱"
+ updateUrl = "https://registry.npmmirror.com/@gkd-kit/subscription/latest/files",
+ name = "GKD官方订阅",
+ author = "gkd",
)
)
}
diff --git a/app/src/main/java/li/songe/gkd/ui/component/TextSwitch.kt b/app/src/main/java/li/songe/gkd/ui/component/TextSwitch.kt
index c9dd3a070..919bc8d4c 100644
--- a/app/src/main/java/li/songe/gkd/ui/component/TextSwitch.kt
+++ b/app/src/main/java/li/songe/gkd/ui/component/TextSwitch.kt
@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.Surface
import androidx.compose.material.Switch
@@ -23,16 +24,16 @@ fun TextSwitch(
checked: Boolean = true,
onCheckedChange: ((Boolean) -> Unit)? = null,
) {
- Row(verticalAlignment = Alignment.CenterVertically) {
+ Row(
+ modifier = Modifier.padding(10.dp, 5.dp), verticalAlignment = Alignment.CenterVertically
+ ) {
Column(modifier = Modifier.weight(1f)) {
Text(
- name,
- fontSize = 18.sp
+ name, fontSize = 18.sp
)
Spacer(modifier = Modifier.height(2.dp))
Text(
- desc,
- fontSize = 14.sp
+ desc, fontSize = 14.sp
)
}
Spacer(modifier = Modifier.width(10.dp))
@@ -46,7 +47,5 @@ fun TextSwitch(
@Preview
@Composable
fun PreviewTextSwitch() {
- Surface(modifier = Modifier.width(300.dp)) {
- TextSwitch("隐藏后台", "在最近任务列表中隐藏", true)
- }
+ TextSwitch("隐藏后台", "在最近任务列表中隐藏", true)
}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/ui/home/BottomNavItem.kt b/app/src/main/java/li/songe/gkd/ui/home/BottomNavItem.kt
deleted file mode 100644
index 18ca73d08..000000000
--- a/app/src/main/java/li/songe/gkd/ui/home/BottomNavItem.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package li.songe.gkd.ui.home
-
-import androidx.annotation.DrawableRes
-import li.songe.gkd.R
-import li.songe.gkd.utils.SafeR
-
-data class BottomNavItem(
- val label: String,
- @DrawableRes
- val icon: Int,
- val route: String,
-)
-
-val BottomNavItems = listOf(
- BottomNavItem(
- label = "订阅",
- icon = SafeR.ic_link,
- route = "subscription"
- ),
- BottomNavItem(
- label = "设置",
- icon = SafeR.ic_cog,
- route = "settings"
- ),
-)
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/ui/home/HomePage.kt b/app/src/main/java/li/songe/gkd/ui/home/HomePage.kt
deleted file mode 100644
index 7c3d106ca..000000000
--- a/app/src/main/java/li/songe/gkd/ui/home/HomePage.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-package li.songe.gkd.ui.home
-
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material.Scaffold
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import com.ramcosta.composedestinations.annotation.Destination
-import com.ramcosta.composedestinations.annotation.RootNavGraph
-import li.songe.gkd.utils.LocalStateCache
-import li.songe.gkd.utils.StateCache
-import li.songe.gkd.utils.rememberCache
-
-@RootNavGraph(start = true)
-@Destination
-@Composable
-fun HomePage() {
- var tabIndex by rememberCache { mutableStateOf(0) }
- val subsStateCache = rememberCache { StateCache() }
- val settingStateCache = rememberCache { StateCache() }
- Scaffold(bottomBar = { BottomNavigationBar(tabIndex) { tabIndex = it } }, content = { padding ->
- Box(modifier = Modifier.padding(padding)) {
- when (tabIndex) {
- 0 -> CompositionLocalProvider(LocalStateCache provides subsStateCache) { SubscriptionManagePage() }
- 1 -> CompositionLocalProvider(LocalStateCache provides settingStateCache) { SettingsPage() }
- }
- }
- })
-}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/ui/home/NativePage.kt b/app/src/main/java/li/songe/gkd/ui/home/NativePage.kt
deleted file mode 100644
index 8c41cad60..000000000
--- a/app/src/main/java/li/songe/gkd/ui/home/NativePage.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-package li.songe.gkd.ui.home
-
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.material.Switch
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.unit.dp
-import li.songe.gkd.R
-import li.songe.gkd.utils.SafeR
-
-@Composable
-fun NativePage() {
- Column(
- modifier = Modifier
- .padding(20.dp, 0.dp)
- ) {
- Row(
- modifier = Modifier.height(40.dp)
- ) {
- Image(
- painter = painterResource(SafeR.ic_app_2),
- contentDescription = "",
- modifier = Modifier
- .fillMaxHeight()
- .clip(CircleShape)
- )
- Column {
- Text(text = "应用名称")
- Text(text = "8/10")
- }
- val checkedState = remember { mutableStateOf(true) }
- Switch(checked = checkedState.value,
- onCheckedChange = {
- checkedState.value = it
- }
- )
- }
- }
-}
diff --git a/app/src/main/java/li/songe/gkd/ui/home/Nav.kt b/app/src/main/java/li/songe/gkd/ui/home/Nav.kt
deleted file mode 100644
index a5c3ce288..000000000
--- a/app/src/main/java/li/songe/gkd/ui/home/Nav.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-package li.songe.gkd.ui.home
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material.BottomNavigation
-import androidx.compose.material.BottomNavigationItem
-import androidx.compose.material.Icon
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.unit.dp
-
-
-//@Composable
-//fun NavHostContainer(
-// tabInt: Int,
-// padding:PaddingValues,
-//) {
-// when(tabInt)
-// NavHost(
-// navController = navController,
-// startDestination = "statistics",
-// modifier = Modifier.padding(paddingValues = padding),
-// builder = {
-// composable("native") {
-// NativePage()
-//// BackHandler(false) {}
-// }
-// composable("settings") {
-// SettingsPage()
-// }
-// composable("statistics") {
-// StatisticsPage()
-// }
-// composable("subscription") {
-// SubscriptionPage()
-// }
-// }
-// )
-//}
-
-@Composable
-fun BottomNavigationBar(tabInt: Int, onTabChange: ((Int) -> Unit)? = null) {
- BottomNavigation(
- backgroundColor = Color.Transparent,
- elevation = 0.dp
- ) {
- BottomNavItems.forEachIndexed { i, navItem ->
- BottomNavigationItem(
- selected = i == tabInt,
- modifier = Modifier.background(Color.Transparent),
- onClick = {
- onTabChange?.invoke(i)
- },
- icon = {
- Icon(
- painter = painterResource(id = navItem.icon),
- contentDescription = navItem.label,
- modifier = Modifier.padding(2.dp)
- )
- },
- label = {
- Text(text = navItem.label)
- }
- )
- }
- }
-}
diff --git a/app/src/main/java/li/songe/gkd/ui/home/SettingsPage.kt b/app/src/main/java/li/songe/gkd/ui/home/SettingsPage.kt
deleted file mode 100644
index 052101010..000000000
--- a/app/src/main/java/li/songe/gkd/ui/home/SettingsPage.kt
+++ /dev/null
@@ -1,267 +0,0 @@
-package li.songe.gkd.ui.home
-
-import android.app.Activity
-import android.content.Context
-import android.content.Intent
-import android.content.pm.PackageManager
-import android.media.projection.MediaProjectionManager
-import android.net.Uri
-import android.provider.Settings
-import androidx.activity.ComponentActivity
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.Button
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.unit.dp
-import androidx.core.content.ContextCompat
-import com.blankj.utilcode.util.LogUtils
-import com.blankj.utilcode.util.ToastUtils
-import com.dylanc.activityresult.launcher.launchForResult
-import kotlinx.coroutines.delay
-import li.songe.gkd.MainActivity
-import li.songe.gkd.accessibility.GkdAbService
-import li.songe.gkd.debug.FloatingService
-import li.songe.gkd.debug.HttpService
-import li.songe.gkd.debug.ScreenshotService
-import li.songe.gkd.ui.component.TextSwitch
-import li.songe.gkd.utils.Ext
-import li.songe.gkd.utils.LocalLauncher
-import li.songe.gkd.utils.LocalNavController
-import li.songe.gkd.utils.Storage
-import li.songe.gkd.utils.launchAsFn
-import li.songe.gkd.utils.usePollState
-import li.songe.gkd.utils.useTask
-import rikka.shizuku.Shizuku
-import com.ramcosta.composedestinations.navigation.navigate
-import li.songe.gkd.shizuku.shizukuIsSafeOK
-import li.songe.gkd.ui.destinations.AboutPageDestination
-import li.songe.gkd.ui.destinations.SnapshotPageDestination
-
-@Composable
-fun SettingsPage() {
- val context = LocalContext.current as MainActivity
- val launcher = LocalLauncher.current
- val scope = rememberCoroutineScope()
-
- val navController = LocalNavController.current
-
- Column(
- modifier = Modifier
- .verticalScroll(
- state = rememberScrollState()
- )
- .padding(20.dp, 0.dp)
- ) {
- val gkdAccessRunning by usePollState { GkdAbService.isRunning() }
- TextSwitch("无障碍授权",
- "用于获取屏幕信息,点击屏幕上的控件",
- gkdAccessRunning,
- onCheckedChange = scope.launchAsFn {
- if (!it) return@launchAsFn
- ToastUtils.showShort("请先启动无障碍服务")
- delay(500)
- val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
- intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
- context.startActivity(intent)
- })
-
-
- val shizukuIsOk by usePollState { shizukuIsSafeOK() }
- TextSwitch("Shizuku授权",
- "高级运行模式,能更准确识别界面活动ID",
- shizukuIsOk,
- onCheckedChange = scope.launchAsFn {
- if (!it) return@launchAsFn
- try {
- Shizuku.requestPermission(Activity.RESULT_OK)
- } catch (e: Exception) {
- ToastUtils.showShort("Shizuku可能没有运行")
- }
- })
-
-
- val canDrawOverlays by usePollState {
- Settings.canDrawOverlays(context)
- }
- Spacer(modifier = Modifier.height(5.dp))
- TextSwitch("悬浮窗授权",
- "用于后台提示,主动保存快照等功能",
- canDrawOverlays,
- onCheckedChange = scope.launchAsFn {
- if (!Settings.canDrawOverlays(context)) {
- val intent = Intent(
- Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
- Uri.parse("package:$context.packageName")
- )
- launcher.launch(intent) { resultCode, _ ->
- if (resultCode != ComponentActivity.RESULT_OK) return@launch
- if (!Settings.canDrawOverlays(context)) return@launch
- val intent1 = Intent(context, FloatingService::class.java)
- ContextCompat.startForegroundService(context, intent1)
- }
- }
- })
-
- Spacer(modifier = Modifier.height(15.dp))
-
- val httpServerRunning by usePollState { HttpService.isRunning() }
- TextSwitch("HTTP服务",
- "开启HTTP服务, 以便在同一局域网下传递数据" + if (httpServerRunning) "\n${
- Ext.getIpAddressInLocalNetwork()
- .map { host -> "http://${host}:${Storage.settings.httpServerPort}" }
- .joinToString(",")
- }" else "\n暂无地址",
- httpServerRunning) {
- if (it) {
- HttpService.start()
- } else {
- HttpService.stop()
- }
- }
-
- Spacer(modifier = Modifier.height(5.dp))
-
- val screenshotRunning by usePollState { ScreenshotService.isRunning() }
- TextSwitch("截屏服务",
- "生成快照需要截取屏幕,Android>=11无需开启",
- screenshotRunning,
- scope.launchAsFn {
- if (it) {
- val mediaProjectionManager =
- context.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
- val activityResult =
- launcher.launchForResult(mediaProjectionManager.createScreenCaptureIntent())
- if (activityResult.resultCode == Activity.RESULT_OK && activityResult.data != null) {
- ScreenshotService.start(intent = activityResult.data!!)
- }
- } else {
- ScreenshotService.stop()
- }
- })
-
- Spacer(modifier = Modifier.height(5.dp))
-
- val floatingRunning by usePollState {
- FloatingService.isRunning()
- }
- TextSwitch("悬浮窗服务", "便于用户主动保存快照", floatingRunning) {
- if (it) {
- if (Settings.canDrawOverlays(context)) {
- val intent = Intent(context, FloatingService::class.java)
- ContextCompat.startForegroundService(context, intent)
- } else {
- val intent = Intent(
- Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
- Uri.parse("package:$context.packageName")
- )
- launcher.launch(intent) { resultCode, _ ->
- if (resultCode != ComponentActivity.RESULT_OK) return@launch
- if (!Settings.canDrawOverlays(context)) return@launch
- val intent1 = Intent(context, FloatingService::class.java)
- ContextCompat.startForegroundService(context, intent1)
- }
- }
- } else {
- FloatingService.stop(context)
- }
- }
-
-
- Spacer(modifier = Modifier.height(15.dp))
-
- var enableService by remember { mutableStateOf(Storage.settings.enableService) }
-
- Spacer(modifier = Modifier.height(5.dp))
- TextSwitch(name = "服务开启",
- desc = "保持服务开启,根据订阅规则匹配屏幕目标节点",
- checked = enableService,
- onCheckedChange = {
- enableService = it
- Storage.settings.commit {
- this.enableService = it
- }
- })
-
- Spacer(modifier = Modifier.height(5.dp))
-
- var excludeFromRecents by remember { mutableStateOf(Storage.settings.excludeFromRecents) }
- TextSwitch(name = "隐藏后台",
- desc = "在[最近任务]界面中隐藏本应用",
- checked = excludeFromRecents,
- onCheckedChange = {
- excludeFromRecents = it
- Storage.settings.commit {
- this.excludeFromRecents = it
- }
- })
-
- Spacer(modifier = Modifier.height(5.dp))
-
- var enableConsoleLogOut by remember { mutableStateOf(Storage.settings.enableConsoleLogOut) }
- TextSwitch(name = "日志输出",
- desc = "保持日志输出到控制台",
- checked = enableConsoleLogOut,
- onCheckedChange = {
- enableConsoleLogOut = it
- LogUtils.getConfig().setConsoleSwitch(it)
- Storage.settings.commit {
- this.enableConsoleLogOut = it
- }
- })
-
- Spacer(modifier = Modifier.height(5.dp))
-
- var notificationVisible by remember { mutableStateOf(Storage.settings.notificationVisible) }
- TextSwitch(name = "通知栏显示",
- desc = "通知栏显示可以降低系统杀后台的概率",
- checked = notificationVisible,
- onCheckedChange = {
- notificationVisible = it
- Storage.settings.commit {
- this.notificationVisible = it
- }
- })
- Spacer(modifier = Modifier.height(5.dp))
-
- var enableScreenshot by remember {
- mutableStateOf(Storage.settings.enableCaptureSystemScreenshot)
- }
- Spacer(modifier = Modifier.height(5.dp))
- TextSwitch(
- "自动快照", "当用户截屏时,自动保存当前界面的快照,目前仅支持miui", enableScreenshot
- ) {
- enableScreenshot = it
- Storage.settings.commit {
- enableCaptureSystemScreenshot = it
- }
- }
-
- Spacer(modifier = Modifier.height(5.dp))
- Button(onClick = scope.useTask().launchAsFn {
- navController.navigate(SnapshotPageDestination)
- }) {
- Text(text = "查看快照记录")
- }
-
- Spacer(modifier = Modifier.height(5.dp))
-
- Button(onClick = scope.useTask().launchAsFn {
- navController.navigate(AboutPageDestination)
- }) {
- Text(text = "查看关于")
- }
-
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/ui/home/StatisticsPage.kt b/app/src/main/java/li/songe/gkd/ui/home/StatisticsPage.kt
deleted file mode 100644
index 54cc2caad..000000000
--- a/app/src/main/java/li/songe/gkd/ui/home/StatisticsPage.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package li.songe.gkd.ui.home
-
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-
-@Composable
-fun StatisticsPage() {
- Column(
- modifier = Modifier
- .padding(20.dp, 0.dp)
- ) {
- Text(text = "Statistics")
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/ui/theme/Theme.kt b/app/src/main/java/li/songe/gkd/ui/theme/Theme.kt
index 78e52eb47..f3404ce07 100644
--- a/app/src/main/java/li/songe/gkd/ui/theme/Theme.kt
+++ b/app/src/main/java/li/songe/gkd/ui/theme/Theme.kt
@@ -5,10 +5,12 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.material.darkColors
import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
private val DarkColorPalette = darkColors()
-private val LightColorPalette = lightColors()
+private val LightColorPalette = lightColors(
+)
@Composable
fun AppTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) {
@@ -19,9 +21,6 @@ fun AppTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable()
}
MaterialTheme(
- colors = colors,
- typography = Typography,
- shapes = Shapes,
- content = content
+ colors = colors, typography = Typography, shapes = Shapes, content = content
)
}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/utils/ComposeExt.kt b/app/src/main/java/li/songe/gkd/util/ComposeExt.kt
similarity index 88%
rename from app/src/main/java/li/songe/gkd/utils/ComposeExt.kt
rename to app/src/main/java/li/songe/gkd/util/ComposeExt.kt
index a8581a660..78e529bda 100644
--- a/app/src/main/java/li/songe/gkd/utils/ComposeExt.kt
+++ b/app/src/main/java/li/songe/gkd/util/ComposeExt.kt
@@ -1,4 +1,4 @@
-package li.songe.gkd.utils
+package li.songe.gkd.util
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -10,8 +10,10 @@ import com.blankj.utilcode.util.ToastUtils
import com.dylanc.activityresult.launcher.StartActivityLauncher
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
+import kotlinx.coroutines.withContext
val LocalLauncher =
@@ -36,7 +38,9 @@ fun LaunchedEffectTry(
) {
LaunchedEffect(key1) {
try {
- block()
+ withContext(IO){
+ block()
+ }
} catch (e: CancellationException) {
e.printStackTrace()
} catch (e: Exception) {
diff --git a/app/src/main/java/li/songe/gkd/utils/CoroutineExt.kt b/app/src/main/java/li/songe/gkd/util/CoroutineExt.kt
similarity index 97%
rename from app/src/main/java/li/songe/gkd/utils/CoroutineExt.kt
rename to app/src/main/java/li/songe/gkd/util/CoroutineExt.kt
index f1167c0c3..10e04a185 100644
--- a/app/src/main/java/li/songe/gkd/utils/CoroutineExt.kt
+++ b/app/src/main/java/li/songe/gkd/util/CoroutineExt.kt
@@ -1,6 +1,5 @@
-package li.songe.gkd.utils
+package li.songe.gkd.util
-import com.blankj.utilcode.util.LogUtils
import com.blankj.utilcode.util.ToastUtils
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
diff --git a/app/src/main/java/li/songe/gkd/utils/DrawableDsl.kt b/app/src/main/java/li/songe/gkd/util/DrawableDsl.kt
similarity index 95%
rename from app/src/main/java/li/songe/gkd/utils/DrawableDsl.kt
rename to app/src/main/java/li/songe/gkd/util/DrawableDsl.kt
index c1d430869..6f5dcbad3 100644
--- a/app/src/main/java/li/songe/gkd/utils/DrawableDsl.kt
+++ b/app/src/main/java/li/songe/gkd/util/DrawableDsl.kt
@@ -1,10 +1,9 @@
-package li.songe.gkd.utils
+package li.songe.gkd.util
import android.graphics.Path
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.RectShape
import androidx.compose.material.icons.materialIcon
-import androidx.compose.material.icons.materialPath
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.addPathNodes
import androidx.compose.ui.unit.Dp
diff --git a/app/src/main/java/li/songe/gkd/utils/Ext.kt b/app/src/main/java/li/songe/gkd/util/Ext.kt
similarity index 67%
rename from app/src/main/java/li/songe/gkd/utils/Ext.kt
rename to app/src/main/java/li/songe/gkd/util/Ext.kt
index aa4baf413..5a46a9876 100644
--- a/app/src/main/java/li/songe/gkd/utils/Ext.kt
+++ b/app/src/main/java/li/songe/gkd/util/Ext.kt
@@ -1,4 +1,4 @@
-package li.songe.gkd.utils
+package li.songe.gkd.util
import android.app.NotificationChannel
import android.app.NotificationManager
@@ -16,14 +16,10 @@ import android.os.Looper
import androidx.compose.runtime.*
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
-import androidx.core.graphics.drawable.IconCompat
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.suspendCancellableCoroutine
-import li.songe.gkd.App
import li.songe.gkd.MainActivity
-import li.songe.gkd.R
import li.songe.gkd.db.DbSet
-import li.songe.gkd.icon.AddIcon
import java.net.NetworkInterface
import kotlin.coroutines.resume
@@ -35,26 +31,15 @@ object Ext {
): ApplicationInfo {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
getApplicationInfo(
- packageName,
- PackageManager.ApplicationInfoFlags.of(value.toLong())
+ packageName, PackageManager.ApplicationInfoFlags.of(value.toLong())
)
} else {
@Suppress("DEPRECATION") getApplicationInfo(
- packageName,
- value
+ packageName, value
)
}
}
- fun getAppName(appId: String? = null): String? {
- appId ?: return null
- return App.context.packageManager.getApplicationLabel(
- App.context.packageManager.getApplicationInfoExt(
- appId
- )
- ).toString()
- }
-
fun Bitmap.isEmptyBitmap(): Boolean {
val emptyBitmap = Bitmap.createBitmap(width, height, config)
return this.sameAs(emptyBitmap)
@@ -73,12 +58,9 @@ object Ext {
val pixelStride = planes[0].pixelStride
val rowStride = planes[0].rowStride
val rowPadding: Int = rowStride - pixelStride * screenWidth
- var bitmap =
- Bitmap.createBitmap(
- screenWidth + rowPadding / pixelStride,
- screenHeight,
- Bitmap.Config.ARGB_8888
- )
+ var bitmap = Bitmap.createBitmap(
+ screenWidth + rowPadding / pixelStride, screenHeight, Bitmap.Config.ARGB_8888
+ )
bitmap.copyPixelsFromBuffer(buffer)
bitmap = Bitmap.createBitmap(bitmap, 0, 0, screenWidth, screenHeight)
image.close()
@@ -99,20 +81,13 @@ object Ext {
}
val pendingIntent: PendingIntent = PendingIntent.getActivity(
- context,
- 0,
- intent,
- PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ context, 0, intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
- val builder = NotificationCompat.Builder(context, channelId)
- .setSmallIcon(SafeR.ic_launcher)
- .setContentTitle("调试模式")
- .setContentText("正在录制您的屏幕内容")
- .setContentIntent(pendingIntent)
- .setPriority(NotificationCompat.PRIORITY_DEFAULT)
- .setOngoing(true)
- .setAutoCancel(false)
+ val builder = NotificationCompat.Builder(context, channelId).setSmallIcon(SafeR.ic_launcher)
+ .setContentTitle("调试模式").setContentText("正在录制您的屏幕内容")
+ .setContentIntent(pendingIntent).setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ .setOngoing(true).setAutoCancel(false)
val name = "调试模式"
val descriptionText = "屏幕录制"
@@ -125,9 +100,7 @@ object Ext {
val notification = builder.build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
context.startForeground(
- notificationId,
- notification,
- ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
+ notificationId, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
)
} else {
context.startForeground(notificationId, notification)
@@ -147,55 +120,52 @@ object Ext {
suspend fun getSubsFileLastModified(): Long {
return DbSet.subsItemDao.query().first().map { it.subsFile }
- .filter { it.isFile && it.exists() }
- .maxOfOrNull { it.lastModified() } ?: -1L
+ .filter { it.isFile && it.exists() }.maxOfOrNull { it.lastModified() } ?: -1L
}
- @SuppressWarnings("fallthrough")
fun createNotificationChannel(context: Service) {
- val channelId = "无障碍后台服务"
+// 通知渠道
+ val channelId = "无障碍服务"
val name = "无障碍服务"
- val descriptionText = "无障碍服务保持活跃"
+ val desc = "显示无障碍服务状态"
+
val importance = NotificationManager.IMPORTANCE_DEFAULT
- val channel = NotificationChannel(channelId, name, importance).apply {
- description = descriptionText
- }
+ val channel = NotificationChannel(channelId, name, importance)
+ channel.description = desc
val notificationManager = NotificationManagerCompat.from(context)
notificationManager.createNotificationChannel(channel)
- val serviceId = 100
+ val icon = SafeR.ic_launcher
+ val title = "搞快点"
+ val text = "无障碍正在运行"
+ val id = 100
+ val ongoing = true
+ val autoCancel = false
+
+ notificationManager.cancel(id)
+
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent: PendingIntent = PendingIntent.getActivity(
- context,
- 0,
- intent,
- PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ context, 0, intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
- val builder = NotificationCompat.Builder(context, channelId)
- .setSmallIcon(SafeR.ic_add)
- .setContentTitle("搞快点")
- .setContentText("无障碍正在运行")
- .setContentIntent(pendingIntent)
- .setPriority(NotificationCompat.PRIORITY_DEFAULT)
- .setOngoing(true)
- .setAutoCancel(false)
+ val builder =
+ NotificationCompat.Builder(context, channelId).setSmallIcon(icon).setContentTitle(title)
+ .setContentText(text).setContentIntent(pendingIntent)
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT).setOngoing(ongoing)
+ .setAutoCancel(autoCancel)
val notification = builder.build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
context.startForeground(
- serviceId,
- notification,
- ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST
+ id, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST
)
} else {
- context.startForeground(serviceId, notification)
+ context.startForeground(id, notification)
}
- }
-
-
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/utils/FolderExt.kt b/app/src/main/java/li/songe/gkd/util/FolderExt.kt
similarity index 54%
rename from app/src/main/java/li/songe/gkd/utils/FolderExt.kt
rename to app/src/main/java/li/songe/gkd/util/FolderExt.kt
index e8ccceea3..724a19bd3 100644
--- a/app/src/main/java/li/songe/gkd/utils/FolderExt.kt
+++ b/app/src/main/java/li/songe/gkd/util/FolderExt.kt
@@ -1,13 +1,18 @@
-package li.songe.gkd.utils
+package li.songe.gkd.util
-import com.blankj.utilcode.util.PathUtils
+import com.blankj.utilcode.util.LogUtils
+import li.songe.gkd.app
import java.io.File
object FolderExt {
private fun createFolder(name: String): File {
- return File(PathUtils.getExternalAppFilesPath().plus("/$name")).apply {
+ return File(
+ app.getExternalFilesDir(name)?.absolutePath
+ ?: app.filesDir.absolutePath.plus(name)
+ ).apply {
if (!exists()) {
mkdirs()
+ LogUtils.d("mkdirs", absolutePath)
}
}
}
diff --git a/app/src/main/java/li/songe/gkd/utils/HookExt.kt b/app/src/main/java/li/songe/gkd/util/HookExt.kt
similarity index 69%
rename from app/src/main/java/li/songe/gkd/utils/HookExt.kt
rename to app/src/main/java/li/songe/gkd/util/HookExt.kt
index c037668b1..1fcbaa222 100644
--- a/app/src/main/java/li/songe/gkd/utils/HookExt.kt
+++ b/app/src/main/java/li/songe/gkd/util/HookExt.kt
@@ -1,4 +1,4 @@
-package li.songe.gkd.utils
+package li.songe.gkd.util
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.runtime.Composable
@@ -6,19 +6,15 @@ import androidx.compose.runtime.remember
import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanIntentResult
import com.journeyapps.barcodescanner.ScanOptions
-import li.songe.gkd.data.Value
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@Composable
fun useNavigateForQrcodeResult(): suspend () -> ScanIntentResult {
- val resolve = remember {
- Value { _: ScanIntentResult -> }
+ var resolve: ((ScanIntentResult) -> Unit)? = null
+ val scanLauncher = rememberLauncherForActivityResult(ScanContract()) { result ->
+ resolve?.invoke(result)
}
- val scanLauncher =
- rememberLauncherForActivityResult(ScanContract()) { result ->
- resolve.value(result)
- }
return remember {
suspend {
scanLauncher.launch(ScanOptions().apply {
@@ -26,7 +22,7 @@ fun useNavigateForQrcodeResult(): suspend () -> ScanIntentResult {
setBeepEnabled(false)
})
suspendCoroutine { continuation ->
- resolve.value = { s -> continuation.resume(s) }
+ resolve = { s -> continuation.resume(s) }
}
}
}
diff --git a/app/src/main/java/li/songe/gkd/utils/ModifierExt.kt b/app/src/main/java/li/songe/gkd/util/ModifierExt.kt
similarity index 94%
rename from app/src/main/java/li/songe/gkd/utils/ModifierExt.kt
rename to app/src/main/java/li/songe/gkd/util/ModifierExt.kt
index 594fba8fa..c193940dc 100644
--- a/app/src/main/java/li/songe/gkd/utils/ModifierExt.kt
+++ b/app/src/main/java/li/songe/gkd/util/ModifierExt.kt
@@ -1,4 +1,4 @@
-package li.songe.gkd.utils
+package li.songe.gkd.util
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
diff --git a/app/src/main/java/li/songe/gkd/util/Multiprocess.kt b/app/src/main/java/li/songe/gkd/util/Multiprocess.kt
new file mode 100644
index 000000000..be2484689
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/util/Multiprocess.kt
@@ -0,0 +1,6 @@
+package li.songe.gkd.util
+
+import com.blankj.utilcode.util.ProcessUtils
+
+
+val isMainProcess by lazy { ProcessUtils.isMainProcess() }
diff --git a/app/src/main/java/li/songe/gkd/utils/NavExt.kt b/app/src/main/java/li/songe/gkd/util/NavExt.kt
similarity index 88%
rename from app/src/main/java/li/songe/gkd/utils/NavExt.kt
rename to app/src/main/java/li/songe/gkd/util/NavExt.kt
index e82c119ce..c5f9b41ac 100644
--- a/app/src/main/java/li/songe/gkd/utils/NavExt.kt
+++ b/app/src/main/java/li/songe/gkd/util/NavExt.kt
@@ -1,4 +1,4 @@
-package li.songe.gkd.utils
+package li.songe.gkd.util
import androidx.compose.runtime.compositionLocalOf
import androidx.navigation.NavHostController
diff --git a/app/src/main/java/li/songe/gkd/utils/SafeR.kt b/app/src/main/java/li/songe/gkd/util/SafeR.kt
similarity index 82%
rename from app/src/main/java/li/songe/gkd/utils/SafeR.kt
rename to app/src/main/java/li/songe/gkd/util/SafeR.kt
index fdf769063..0fd80af91 100644
--- a/app/src/main/java/li/songe/gkd/utils/SafeR.kt
+++ b/app/src/main/java/li/songe/gkd/util/SafeR.kt
@@ -1,14 +1,10 @@
-package li.songe.gkd.utils
+package li.songe.gkd.util
import li.songe.gkd.R
-
-/**
- * ![image](https://github.com/lisonge/gkd/assets/38517192/545c4fce-77b2-4003-8e22-a21b48ef3d98)
- */
@Suppress("UNRESOLVED_REFERENCE")
object SafeR {
- val capture: Int = R.drawable.capture
+ val ic_capture: Int = R.drawable.ic_capture
val ic_add: Int = R.drawable.ic_add
val ic_app_2: Int = R.drawable.ic_app_2
val ic_apps: Int = R.drawable.ic_apps
@@ -25,4 +21,5 @@ object SafeR {
val ic_menu: Int = R.drawable.ic_menu
val ic_refresh: Int = R.drawable.ic_refresh
val ic_share: Int = R.drawable.ic_share
+ val ic_home: Int = R.drawable.ic_home
}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/utils/ScreenshotUtil.kt b/app/src/main/java/li/songe/gkd/util/ScreenshotUtil.kt
similarity index 98%
rename from app/src/main/java/li/songe/gkd/utils/ScreenshotUtil.kt
rename to app/src/main/java/li/songe/gkd/util/ScreenshotUtil.kt
index 55d313d12..1381556f6 100644
--- a/app/src/main/java/li/songe/gkd/utils/ScreenshotUtil.kt
+++ b/app/src/main/java/li/songe/gkd/util/ScreenshotUtil.kt
@@ -1,4 +1,4 @@
-package li.songe.gkd.utils
+package li.songe.gkd.util
import android.annotation.SuppressLint
import android.app.Activity
@@ -16,7 +16,7 @@ import android.media.projection.MediaProjectionManager
import android.os.Handler
import android.os.Looper
import com.blankj.utilcode.util.ScreenUtils
-import li.songe.gkd.utils.Ext.isEmptyBitmap
+import li.songe.gkd.util.Ext.isEmptyBitmap
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
diff --git a/app/src/main/java/li/songe/gkd/utils/Singleton.kt b/app/src/main/java/li/songe/gkd/util/Singleton.kt
similarity index 94%
rename from app/src/main/java/li/songe/gkd/utils/Singleton.kt
rename to app/src/main/java/li/songe/gkd/util/Singleton.kt
index 9a794662f..29f5c464a 100644
--- a/app/src/main/java/li/songe/gkd/utils/Singleton.kt
+++ b/app/src/main/java/li/songe/gkd/util/Singleton.kt
@@ -1,10 +1,9 @@
-package li.songe.gkd.utils
+package li.songe.gkd.util
import blue.endless.jankson.Jankson
import com.journeyapps.barcodescanner.BarcodeEncoder
import io.ktor.client.HttpClient
import io.ktor.client.engine.android.Android
-import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.http.ContentType
import io.ktor.serialization.kotlinx.json.json
diff --git a/app/src/main/java/li/songe/gkd/utils/StateCache.kt b/app/src/main/java/li/songe/gkd/util/StateCache.kt
similarity index 97%
rename from app/src/main/java/li/songe/gkd/utils/StateCache.kt
rename to app/src/main/java/li/songe/gkd/util/StateCache.kt
index a85546dca..9c269c213 100644
--- a/app/src/main/java/li/songe/gkd/utils/StateCache.kt
+++ b/app/src/main/java/li/songe/gkd/util/StateCache.kt
@@ -1,4 +1,4 @@
-package li.songe.gkd.utils
+package li.songe.gkd.util
import android.os.Bundle
import androidx.compose.runtime.Composable
@@ -10,7 +10,6 @@ import androidx.compose.runtime.remember
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.NavHostController
-import com.blankj.utilcode.util.LogUtils
data class StateCache(
diff --git a/app/src/main/java/li/songe/gkd/utils/Status.kt b/app/src/main/java/li/songe/gkd/util/Status.kt
similarity index 91%
rename from app/src/main/java/li/songe/gkd/utils/Status.kt
rename to app/src/main/java/li/songe/gkd/util/Status.kt
index 6d452be70..575c8dea3 100644
--- a/app/src/main/java/li/songe/gkd/utils/Status.kt
+++ b/app/src/main/java/li/songe/gkd/util/Status.kt
@@ -1,4 +1,4 @@
-package li.songe.gkd.utils
+package li.songe.gkd.util
sealed class Status {
object Empty : Status()
diff --git a/app/src/main/java/li/songe/gkd/util/Store.kt b/app/src/main/java/li/songe/gkd/util/Store.kt
new file mode 100644
index 000000000..0e7b407a1
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/util/Store.kt
@@ -0,0 +1,64 @@
+package li.songe.gkd.util
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.Parcelable
+import com.blankj.utilcode.util.LogUtils
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+import kotlinx.parcelize.Parcelize
+import li.songe.gkd.app
+import li.songe.gkd.appScope
+
+private const val STORE_KEY = "store-v1"
+private const val EVENT_KEY = "updateStore"
+
+/**
+ * 属性不可删除,注释弃用即可
+ * 属性声明顺序不可变动
+ * 新增属性必须在尾部声明
+ * 否则导致序列化错误
+ */
+@Parcelize
+data class Store(
+ val enableService: Boolean = true,
+ val excludeFromRecents: Boolean = true,
+ val enableConsoleLogOut: Boolean = true,
+ val enableCaptureScreenshot: Boolean = true,
+ val httpServerPort: Int = 8888,
+) : Parcelable
+
+private fun getStore(): Store {
+ return kv.decodeParcelable(STORE_KEY, Store::class.java) ?: Store()
+}
+
+val storeFlow by lazy> {
+ val state = MutableStateFlow(getStore())
+ val receiver=object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ intent?.extras?.getString(EVENT_KEY) ?: return
+ state.value = getStore()
+ }
+ }
+ app.registerReceiver(receiver, IntentFilter(app.packageName))
+// app.unregisterReceiver(receiver)
+ appScope.launch {
+ LogUtils.getConfig().setConsoleSwitch(state.value.enableConsoleLogOut)
+ state.collect {
+ LogUtils.getConfig().setConsoleSwitch(state.value.enableConsoleLogOut)
+ }
+ }
+ state
+}
+
+fun updateStore(newStore: Store) {
+ if (storeFlow.value == newStore) return
+ kv.encode(STORE_KEY, newStore)
+ app.sendBroadcast(Intent(app.packageName).apply {
+ putExtra(EVENT_KEY, EVENT_KEY)
+ })
+}
+
diff --git a/app/src/main/java/li/songe/gkd/utils/TaskExt.kt b/app/src/main/java/li/songe/gkd/util/TaskExt.kt
similarity index 80%
rename from app/src/main/java/li/songe/gkd/utils/TaskExt.kt
rename to app/src/main/java/li/songe/gkd/util/TaskExt.kt
index 83e8375f7..126fc50c8 100644
--- a/app/src/main/java/li/songe/gkd/utils/TaskExt.kt
+++ b/app/src/main/java/li/songe/gkd/util/TaskExt.kt
@@ -1,22 +1,22 @@
-package li.songe.gkd.utils
+package li.songe.gkd.util
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
-import androidx.compose.material.AlertDialog
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
-import com.blankj.utilcode.util.LogUtils
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.delay
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
private val emptyFn = {}
@@ -27,18 +27,19 @@ data class TaskState(
private val scope: CoroutineScope,
val loading: Boolean = false,
private val miniInterval: Long = 0,
- var innerLoading: Boolean = false,
+ private var innerLoading: Boolean = false,
val onChangeLoading: (Boolean) -> Unit = emptyFn2,
) {
fun launchAsFn(
- changeLoading: Boolean = false,
- block: suspend TaskState.() -> Unit
+ context: CoroutineContext = EmptyCoroutineContext,
+ start: CoroutineStart = CoroutineStart.DEFAULT,
+ block: suspend TaskState.() -> Unit,
): () -> Unit {
if (loading) return emptyFn
- return scope.launchAsFn {
+ return scope.launchAsFn(context, start) {
if (innerLoading) return@launchAsFn
innerLoading = true
- onChangeLoading(changeLoading)
+ onChangeLoading(true)
val start = System.currentTimeMillis()
try {
try {
@@ -55,14 +56,15 @@ data class TaskState(
}
fun launchAsFn(
- changeLoading: Boolean = false,
- block: suspend TaskState.(T) -> Unit
+ context: CoroutineContext = EmptyCoroutineContext,
+ start: CoroutineStart = CoroutineStart.DEFAULT,
+ block: suspend TaskState.(T) -> Unit,
): (T) -> Unit {
if (loading) return emptyFnT1
- return scope.launchAsFn {
+ return scope.launchAsFn(context, start) {
if (innerLoading) return@launchAsFn
innerLoading = true
- onChangeLoading(changeLoading)
+ onChangeLoading(true)
val start = System.currentTimeMillis()
try {
try {
diff --git a/app/src/main/java/li/songe/gkd/util/TimeExt.kt b/app/src/main/java/li/songe/gkd/util/TimeExt.kt
new file mode 100644
index 000000000..8898ca181
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/util/TimeExt.kt
@@ -0,0 +1,37 @@
+package li.songe.gkd.util
+
+import java.text.SimpleDateFormat
+import java.util.Locale
+import java.util.concurrent.TimeUnit
+
+fun formatTimeAgo(timestamp: Long): String {
+ val currentTime = System.currentTimeMillis()
+ val timeDifference = currentTime - timestamp
+
+ val minutes = TimeUnit.MILLISECONDS.toMinutes(timeDifference)
+ val hours = TimeUnit.MILLISECONDS.toHours(timeDifference)
+ val days = TimeUnit.MILLISECONDS.toDays(timeDifference)
+ val weeks = days / 7
+ val months = (days / 30)
+ val years = (days / 365)
+ return when {
+ years > 0 -> "${years}年前"
+ months > 0 -> "${months}月前"
+ weeks > 0 -> "${weeks}周前"
+ days > 0 -> "${days}天前"
+ hours > 0 -> "${hours}小时前"
+ minutes > 0 -> "${minutes}分钟前"
+ else -> "刚刚"
+ }
+}
+
+private val formatDateMap = mutableMapOf()
+
+fun Long.format(formatStr: String): String {
+ var df = formatDateMap[formatStr]
+ if (df == null) {
+ df = SimpleDateFormat(formatStr, Locale.getDefault())
+ formatDateMap[formatStr] = df
+ }
+ return df.format(this)
+}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/util/kv.kt b/app/src/main/java/li/songe/gkd/util/kv.kt
new file mode 100644
index 000000000..490248eb2
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/util/kv.kt
@@ -0,0 +1,6 @@
+package li.songe.gkd.util
+
+import com.tencent.mmkv.MMKV
+
+
+val kv by lazy { MMKV.mmkvWithID("kv", MMKV.MULTI_PROCESS_MODE)!! }
diff --git a/app/src/main/java/li/songe/gkd/utils/AppSettings.kt b/app/src/main/java/li/songe/gkd/utils/AppSettings.kt
deleted file mode 100644
index d6699c919..000000000
--- a/app/src/main/java/li/songe/gkd/utils/AppSettings.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-package li.songe.gkd.utils
-
-import android.os.Parcelable
-import androidx.compose.runtime.collectAsState
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.parcelize.Parcelize
-
-/**
- * 备注: 添加字段一定要在末尾添加,不能中间插入字段,否则之后值将会错乱
- */
-@Parcelize
-data class AppSettings(
- var enableService: Boolean = true,
- var excludeFromRecents: Boolean = true,
- var notificationVisible: Boolean = true,
- var enableConsoleLogOut: Boolean = true,
- var enableCaptureSystemScreenshot: Boolean = true,
- var httpServerPort: Int = 8888,
-) : Parcelable {
- fun commit(block: AppSettings.() -> Unit) {
- val backup = copy()
- block.invoke(this)
- if (this != backup) {
- Storage.kv.encode(saveKey, this)
- }
- }
-
- companion object {
- const val saveKey = "settings-v2"
- }
-}
-
-val appSettingsFlow by lazy { MutableStateFlow(AppSettings()) }
diff --git a/app/src/main/java/li/songe/gkd/utils/Storage.kt b/app/src/main/java/li/songe/gkd/utils/Storage.kt
deleted file mode 100644
index 2ba43aef5..000000000
--- a/app/src/main/java/li/songe/gkd/utils/Storage.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package li.songe.gkd.utils
-
-import com.tencent.mmkv.MMKV
-
-object Storage {
-
- val settings by lazy {
- kv.decodeParcelable(
- AppSettings.saveKey,
- AppSettings::class.java,
- null
- ) ?: AppSettings()
- }
-
- val kv: MMKV by lazy { MMKV.defaultMMKV() }
-}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/capture.xml b/app/src/main/res/drawable/ic_capture.xml
similarity index 100%
rename from app/src/main/res/drawable/capture.xml
rename to app/src/main/res/drawable/ic_capture.xml
diff --git a/app/src/main/res/drawable/ic_home.xml b/app/src/main/res/drawable/ic_home.xml
new file mode 100644
index 000000000..1d1d8b78f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_home.xml
@@ -0,0 +1,11 @@
+
+
+
+
diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml
index 1c8a9205d..588dd40c7 100644
--- a/app/src/main/res/values-zh/strings.xml
+++ b/app/src/main/res/values-zh/strings.xml
@@ -2,5 +2,5 @@
搞快点
搞快点
- 基于规则匹配的无障碍速点服务
+ 基于规则匹配的无障碍速点服务\n强大的自定义规则能帮助你实现点击关闭各种广告, 自定义快捷操作等高级功能
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index 4ecac3780..d887c9dd0 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -20,6 +20,7 @@ buildscript {
)
plugins {
alias(libs.plugins.google.ksp) apply false
+ alias(libs.plugins.google.hilt) apply false
alias(libs.plugins.rikka.refine) apply false
}
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 17eedbf2b..6ae892264 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
#Wed Oct 13 10:13:24 CST 2021
distributionBase=GRADLE_USER_HOME
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
diff --git a/selector/build.gradle.kts b/selector/build.gradle.kts
index fe87c3ab5..7b562b78c 100644
--- a/selector/build.gradle.kts
+++ b/selector/build.gradle.kts
@@ -1,6 +1,6 @@
plugins {
- kotlin("multiplatform")
- kotlin("plugin.serialization")
+ id("org.jetbrains.kotlin.multiplatform")
+ id("org.jetbrains.kotlin.plugin.serialization")
}
kotlin {
@@ -12,17 +12,13 @@ kotlin {
// https://kotlinlang.org/docs/js-to-kotlin-interop.html#kotlin-types-in-javascript
js(IR) {
binaries.executable()
-// useEsModules() many bugs
+// useEsModules()
// bug example kotlin CharSequence.contains(char: Char) not work with js string.includes(string)
generateTypeScriptDefinitions()
browser {}
}
- sourceSets {
- val commonMain by getting {
- dependencies {
- implementation(kotlin("stdlib-common"))
- }
- }
+ sourceSets["commonMain"].dependencies {
+ implementation(libs.kotlin.stdlib.common)
}
sourceSets["jvmTest"].dependencies {
implementation(libs.kotlinx.serialization.json)
diff --git a/selector/src/commonMain/kotlin/li/songe/selector/NodeFc.kt b/selector/src/commonMain/kotlin/li/songe/selector/NodeFc.kt
index 8e3ac7057..d6f0dbadf 100644
--- a/selector/src/commonMain/kotlin/li/songe/selector/NodeFc.kt
+++ b/selector/src/commonMain/kotlin/li/songe/selector/NodeFc.kt
@@ -1,5 +1,9 @@
package li.songe.selector
+import kotlin.js.ExperimentalJsExport
+import kotlin.js.JsExport
+import kotlin.random.Random
+
internal interface NodeMatchFc {
operator fun invoke(node: T, transform: Transform): Boolean
}
@@ -11,4 +15,3 @@ internal interface NodeSequenceFc {
internal interface NodeTraversalFc {
operator fun invoke(node: T, transform: Transform): Sequence
}
-
diff --git a/selector/src/commonMain/kotlin/li/songe/selector/Selector.kt b/selector/src/commonMain/kotlin/li/songe/selector/Selector.kt
index 5e520984b..cc795c20e 100644
--- a/selector/src/commonMain/kotlin/li/songe/selector/Selector.kt
+++ b/selector/src/commonMain/kotlin/li/songe/selector/Selector.kt
@@ -14,7 +14,6 @@ class Selector internal constructor(private val propertyWrapper: PropertyWrapper
while (true) {
list.add(list.last().to?.to ?: break)
}
- list.reverse()
list.map { p -> p.propertySegment.tracked }.toTypedArray()
}
@@ -36,7 +35,6 @@ class Selector internal constructor(private val propertyWrapper: PropertyWrapper
transform: Transform,
trackNodes: MutableList = mutableListOf(),
): List? {
- trackNodes.clear()
return propertyWrapper.matchTracks(node, transform, trackNodes)
}
diff --git a/selector/src/commonMain/kotlin/li/songe/selector/Version.kt b/selector/src/commonMain/kotlin/li/songe/selector/Version.kt
index e09cb8b29..caadbbb94 100644
--- a/selector/src/commonMain/kotlin/li/songe/selector/Version.kt
+++ b/selector/src/commonMain/kotlin/li/songe/selector/Version.kt
@@ -5,4 +5,4 @@ import kotlin.js.JsExport
@OptIn(ExperimentalJsExport::class)
@JsExport
-const val version = "0.0.4"
+const val version = "0.0.5"
diff --git a/selector/src/commonMain/kotlin/li/songe/selector/data/BinaryExpression.kt b/selector/src/commonMain/kotlin/li/songe/selector/data/BinaryExpression.kt
index 3937d7df8..95c044686 100644
--- a/selector/src/commonMain/kotlin/li/songe/selector/data/BinaryExpression.kt
+++ b/selector/src/commonMain/kotlin/li/songe/selector/data/BinaryExpression.kt
@@ -2,8 +2,9 @@ package li.songe.selector.data
import li.songe.selector.Transform
-data class BinaryExpression(val name: String, val operator: CompareOperator, val value: Any?) {
- fun match(node: T, transform: Transform) =
+data class BinaryExpression(val name: String, val operator: CompareOperator, val value: Any?) :
+ Expression() {
+ override fun match(node: T, transform: Transform) =
operator.compare(transform.getAttr(node, name), value)
override fun toString() = "${name}${operator}${
diff --git a/selector/src/commonMain/kotlin/li/songe/selector/data/CompareOperator.kt b/selector/src/commonMain/kotlin/li/songe/selector/data/CompareOperator.kt
index 3671b38e9..02e31081f 100644
--- a/selector/src/commonMain/kotlin/li/songe/selector/data/CompareOperator.kt
+++ b/selector/src/commonMain/kotlin/li/songe/selector/data/CompareOperator.kt
@@ -2,7 +2,7 @@ package li.songe.selector.data
sealed class CompareOperator(val key: String) {
override fun toString() = key
- abstract fun compare(a: Any?, b: Any?): Boolean
+ abstract fun compare(left: Any?, right: Any?): Boolean
companion object {
val allSubClasses = listOf(
@@ -22,66 +22,66 @@ sealed class CompareOperator(val key: String) {
}
object Equal : CompareOperator("=") {
- override fun compare(a: Any?, b: Any?): Boolean {
- return if (a is CharSequence && b is CharSequence) a.contentEquals(b) else a == b
+ override fun compare(left: Any?, right: Any?): Boolean {
+ return if (left is CharSequence && right is CharSequence) left.contentEquals(right) else left == right
}
}
object NotEqual : CompareOperator("!=") {
- override fun compare(a: Any?, b: Any?) = !Equal.compare(a, b)
+ override fun compare(left: Any?, right: Any?) = !Equal.compare(left, right)
}
object Start : CompareOperator("^=") {
- override fun compare(a: Any?, b: Any?): Boolean {
- return if (a is CharSequence && b is CharSequence) a.startsWith(b) else false
+ override fun compare(left: Any?, right: Any?): Boolean {
+ return if (left is CharSequence && right is CharSequence) left.startsWith(right) else false
}
}
object NotStart : CompareOperator("!^=") {
- override fun compare(a: Any?, b: Any?) = !Start.compare(a, b)
+ override fun compare(left: Any?, right: Any?) = !Start.compare(left, right)
}
object Include : CompareOperator("*=") {
- override fun compare(a: Any?, b: Any?): Boolean {
- return if (a is CharSequence && b is CharSequence) a.contains(b) else false
+ override fun compare(left: Any?, right: Any?): Boolean {
+ return if (left is CharSequence && right is CharSequence) left.contains(right) else false
}
}
object NotInclude : CompareOperator("!*=") {
- override fun compare(a: Any?, b: Any?) = !Include.compare(a, b)
+ override fun compare(left: Any?, right: Any?) = !Include.compare(left, right)
}
object End : CompareOperator("$=") {
- override fun compare(a: Any?, b: Any?): Boolean {
- return if (a is CharSequence && b is CharSequence) a.endsWith(b) else false
+ override fun compare(left: Any?, right: Any?): Boolean {
+ return if (left is CharSequence && right is CharSequence) left.endsWith(right) else false
}
}
object NotEnd : CompareOperator("!$=") {
- override fun compare(a: Any?, b: Any?) = !End.compare(a, b)
+ override fun compare(left: Any?, right: Any?) = !End.compare(left, right)
}
object Less : CompareOperator("<") {
- override fun compare(a: Any?, b: Any?): Boolean {
- return if (a is Int && b is Int) a < b else false
+ override fun compare(left: Any?, right: Any?): Boolean {
+ return if (left is Int && right is Int) left < right else false
}
}
object LessEqual : CompareOperator("<=") {
- override fun compare(a: Any?, b: Any?): Boolean {
- return if (a is Int && b is Int) a <= b else false
+ override fun compare(left: Any?, right: Any?): Boolean {
+ return if (left is Int && right is Int) left <= right else false
}
}
object More : CompareOperator(">") {
- override fun compare(a: Any?, b: Any?): Boolean {
- return if (a is Int && b is Int) a > b else false
+ override fun compare(left: Any?, right: Any?): Boolean {
+ return if (left is Int && right is Int) left > right else false
}
}
object MoreEqual : CompareOperator(">=") {
- override fun compare(a: Any?, b: Any?): Boolean {
- return if (a is Int && b is Int) a >= b else false
+ override fun compare(left: Any?, right: Any?): Boolean {
+ return if (left is Int && right is Int) left >= right else false
}
}
diff --git a/selector/src/commonMain/kotlin/li/songe/selector/data/Expression.kt b/selector/src/commonMain/kotlin/li/songe/selector/data/Expression.kt
new file mode 100644
index 000000000..5971d7461
--- /dev/null
+++ b/selector/src/commonMain/kotlin/li/songe/selector/data/Expression.kt
@@ -0,0 +1,7 @@
+package li.songe.selector.data
+
+import li.songe.selector.Transform
+
+sealed class Expression {
+ abstract fun match(node: T, transform: Transform): Boolean
+}
diff --git a/selector/src/commonMain/kotlin/li/songe/selector/data/LogicalExpression.kt b/selector/src/commonMain/kotlin/li/songe/selector/data/LogicalExpression.kt
new file mode 100644
index 000000000..6d31910d3
--- /dev/null
+++ b/selector/src/commonMain/kotlin/li/songe/selector/data/LogicalExpression.kt
@@ -0,0 +1,27 @@
+package li.songe.selector.data
+
+import li.songe.selector.Transform
+
+data class LogicalExpression(
+ val left: Expression,
+ val operator: LogicalOperator,
+ val right: Expression,
+) : Expression() {
+ override fun match(node: T, transform: Transform): Boolean {
+ return operator.compare(node, transform, left, right)
+ }
+
+ override fun toString(): String {
+ val leftStr = if (left is LogicalExpression && left.operator != operator) {
+ "($left)"
+ } else {
+ left.toString()
+ }
+ val rightStr = if (right is LogicalExpression && right.operator != operator) {
+ "($right)"
+ } else {
+ right.toString()
+ }
+ return "$leftStr\u0020$operator\u0020$rightStr"
+ }
+}
\ No newline at end of file
diff --git a/selector/src/commonMain/kotlin/li/songe/selector/data/LogicalOperator.kt b/selector/src/commonMain/kotlin/li/songe/selector/data/LogicalOperator.kt
new file mode 100644
index 000000000..e254cb3a1
--- /dev/null
+++ b/selector/src/commonMain/kotlin/li/songe/selector/data/LogicalOperator.kt
@@ -0,0 +1,41 @@
+package li.songe.selector.data
+
+import li.songe.selector.Transform
+
+sealed class LogicalOperator(val key: String) {
+ companion object {
+ val allSubClasses = listOf(
+ AndOperator, OrOperator
+ ).sortedBy { -it.key.length }
+ }
+
+ override fun toString() = key
+ abstract fun compare(
+ node: T,
+ transform: Transform,
+ left: Expression,
+ right: Expression,
+ ): Boolean
+
+ object AndOperator : LogicalOperator("&&") {
+ override fun compare(
+ node: T,
+ transform: Transform,
+ left: Expression,
+ right: Expression,
+ ): Boolean {
+ return left.match(node, transform) && right.match(node, transform)
+ }
+ }
+
+ object OrOperator : LogicalOperator("||") {
+ override fun compare(
+ node: T,
+ transform: Transform,
+ left: Expression,
+ right: Expression,
+ ): Boolean {
+ return left.match(node, transform) || right.match(node, transform)
+ }
+ }
+}
diff --git a/selector/src/commonMain/kotlin/li/songe/selector/data/OrExpression.kt b/selector/src/commonMain/kotlin/li/songe/selector/data/OrExpression.kt
deleted file mode 100644
index b9196fd84..000000000
--- a/selector/src/commonMain/kotlin/li/songe/selector/data/OrExpression.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package li.songe.selector.data
-
-import li.songe.selector.Transform
-
-data class OrExpression(val expressions: List) {
- override fun toString(): String {
- if (expressions.isEmpty()) return ""
- return "[" + expressions.joinToString("||") + "]"
- }
-
- fun match(node: T, transform: Transform): Boolean {
- return expressions.any { ex -> ex.match(node, transform) }
- }
-}
diff --git a/selector/src/commonMain/kotlin/li/songe/selector/data/PropertySegment.kt b/selector/src/commonMain/kotlin/li/songe/selector/data/PropertySegment.kt
index 66c543a97..b05de9b21 100644
--- a/selector/src/commonMain/kotlin/li/songe/selector/data/PropertySegment.kt
+++ b/selector/src/commonMain/kotlin/li/songe/selector/data/PropertySegment.kt
@@ -10,11 +10,11 @@ data class PropertySegment(
*/
val tracked: Boolean,
val name: String,
- val expressions: List,
+ val expressions: List,
) {
override fun toString(): String {
val matchTag = if (tracked) "@" else ""
- return matchTag + name + expressions.joinToString("")
+ return matchTag + name + expressions.joinToString("") { "[$it]" }
}
private val matchName = if (name.isBlank() || name == "*") {
diff --git a/selector/src/commonMain/kotlin/li/songe/selector/parser/Parser.kt b/selector/src/commonMain/kotlin/li/songe/selector/parser/Parser.kt
index 6332b2da2..f067b3894 100644
--- a/selector/src/commonMain/kotlin/li/songe/selector/parser/Parser.kt
+++ b/selector/src/commonMain/kotlin/li/songe/selector/parser/Parser.kt
@@ -1,6 +1,6 @@
package li.songe.selector.parser
-internal open class Parser(
+internal data class Parser(
val prefix: String = "",
private val temp: (source: String, offset: Int, prefix: String) -> ParserResult
) {
diff --git a/selector/src/commonMain/kotlin/li/songe/selector/parser/ParserSet.kt b/selector/src/commonMain/kotlin/li/songe/selector/parser/ParserSet.kt
index d775c258c..70d1090b1 100644
--- a/selector/src/commonMain/kotlin/li/songe/selector/parser/ParserSet.kt
+++ b/selector/src/commonMain/kotlin/li/songe/selector/parser/ParserSet.kt
@@ -7,7 +7,9 @@ import li.songe.selector.data.CompareOperator
import li.songe.selector.data.ConnectOperator
import li.songe.selector.data.ConnectSegment
import li.songe.selector.data.ConnectWrapper
-import li.songe.selector.data.OrExpression
+import li.songe.selector.data.Expression
+import li.songe.selector.data.LogicalExpression
+import li.songe.selector.data.LogicalOperator
import li.songe.selector.data.PolynomialExpression
import li.songe.selector.data.PropertySegment
import li.songe.selector.data.PropertyWrapper
@@ -59,8 +61,7 @@ internal object ParserSet {
Parser(ConnectOperator.allSubClasses.joinToString("") { it.key }) { source, offset, _ ->
val operator = ConnectOperator.allSubClasses.find { subOperator ->
source.startsWith(
- subOperator.key,
- offset
+ subOperator.key, offset
)
} ?: ExtSyntaxError.throwError(source, offset, "ConnectOperator")
ParserResult(operator, operator.key.length)
@@ -70,7 +71,7 @@ internal object ParserSet {
var i = offset
ExtSyntaxError.assert(source, i, prefix, "number")
var s = ""
- while (prefix.contains(source[i])) {
+ while (i < source.length && prefix.contains(source[i])) {
s += source[i]
i++
}
@@ -101,14 +102,13 @@ internal object ParserSet {
i += whiteCharParser(source, i).length
// [a][n[^b]]
ExtSyntaxError.assert(source, i, integerParser.prefix + "n")
- val coefficient =
- if (integerParser.prefix.contains(source[i])) {
- val coefficientResult = integerParser(source, i)
- i += coefficientResult.length
- coefficientResult.data
- } else {
- 1
- } * signal
+ val coefficient = if (integerParser.prefix.contains(source[i])) {
+ val coefficientResult = integerParser(source, i)
+ i += coefficientResult.length
+ coefficientResult.data
+ } else {
+ 1
+ } * signal
// [n[^b]]
if (i < source.length && source[i] == 'n') {
i++
@@ -126,7 +126,7 @@ internal object ParserSet {
}
// ([+-][a][n[^b]] [+-][a][n[^b]])
- val expressionParser = Parser("(0123456789n") { source, offset, prefix ->
+ val polynomialExpressionParser = Parser("(0123456789n") { source, offset, prefix ->
var i = offset
ExtSyntaxError.assert(source, i, prefix)
val monomialResultList = mutableListOf>>()
@@ -175,22 +175,21 @@ internal object ParserSet {
val operatorResult = combinatorOperatorParser(source, i)
i += operatorResult.length
var expressionResult: ParserResult? = null
- if (i < source.length && expressionParser.prefix.contains(source[i])) {
- expressionResult = expressionParser(source, i)
+ if (i < source.length && polynomialExpressionParser.prefix.contains(source[i])) {
+ expressionResult = polynomialExpressionParser(source, i)
i += expressionResult.length
}
ParserResult(
ConnectSegment(
- operatorResult.data,
- expressionResult?.data ?: PolynomialExpression()
+ operatorResult.data, expressionResult?.data ?: PolynomialExpression()
), i - offset
)
}
val attrOperatorParser =
Parser(CompareOperator.allSubClasses.joinToString("") { it.key }) { source, offset, _ ->
- val operator = CompareOperator.allSubClasses.find { SubOperator ->
- source.startsWith(SubOperator.key, offset)
+ val operator = CompareOperator.allSubClasses.find { compareOperator ->
+ source.startsWith(compareOperator.key, offset)
} ?: ExtSyntaxError.throwError(source, offset, "CompareOperator")
ParserResult(operator, operator.key.length)
}
@@ -223,21 +222,22 @@ internal object ParserSet {
ParserResult(data, i - offset)
}
- val propertyParser =
- Parser("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_") { source, offset, prefix ->
- var i = offset
- ExtSyntaxError.assert(source, i, prefix)
- var data = source[i].toString()
- i++
- while (i < source.length) {
- if (!prefix.contains(source[i])) {
- break
- }
- data += source[i]
- i++
+ private val varPrefix = "_" + ('a'..'z').joinToString("") + ('A'..'Z').joinToString("")
+ private val varStr = varPrefix + '.' + ('0'..'9').joinToString("")
+ val propertyParser = Parser(varPrefix) { source, offset, prefix ->
+ var i = offset
+ ExtSyntaxError.assert(source, i, prefix)
+ var data = source[i].toString()
+ i++
+ while (i < source.length && varStr.contains(source[i])) {
+ if (source[i] == '.') {
+ ExtSyntaxError.assert(source, i + 1, prefix)
}
- ParserResult(data, i - offset)
+ data += source[i]
+ i++
}
+ ParserResult(data, i - offset)
+ }
val valueParser =
Parser("tfn" + stringParser.prefix + integerParser.prefix) { source, offset, prefix ->
@@ -290,7 +290,7 @@ internal object ParserSet {
ParserResult(value, i - offset)
}
- val attrParser = Parser("") { source, offset, _ ->
+ val binaryExpressionParser = Parser { source, offset, _ ->
var i = offset
val parserResult = propertyParser(source, i)
i += parserResult.length
@@ -302,44 +302,117 @@ internal object ParserSet {
i += valueResult.length
ParserResult(
BinaryExpression(
- parserResult.data,
- operatorResult.data,
- valueResult.data
+ parserResult.data, operatorResult.data, valueResult.data
), i - offset
)
}
- val orParser = Parser("[") { source, offset, prefix ->
+ val logicalOperatorParser = Parser { source, offset, _ ->
var i = offset
- ExtSyntaxError.assert(source, i, prefix)
- i++
i += whiteCharParser(source, i).length
- val binaryExpressions = mutableListOf()
- while (i < source.length && source[i] != ']') {
- if (binaryExpressions.isNotEmpty()) {
- ExtSyntaxError.assert(source, i, "|")
- ExtSyntaxError.assert(source, i, "|")
- i += 2
- i += whiteCharParser(source, i).length
+ val operator = LogicalOperator.allSubClasses.find { logicalOperator ->
+ source.startsWith(logicalOperator.key, offset)
+ } ?: ExtSyntaxError.throwError(source, offset, "LogicalOperator")
+ ParserResult(operator, operator.key.length)
+ }
+
+
+ // a>1 && a>1 || a>1
+// (a>1 || a>1) && a>1
+ fun expressionParser(source: String, offset: Int): ParserResult {
+ var i = offset
+ i += whiteCharParser(source, i).length
+// [exp, ||, exp, &&, &&]
+ val parserResults = mutableListOf>()
+ while (i < source.length && source[i] != ']' && source[i] != ')') {
+ when (source[i]) {
+ '(' -> {
+ if (parserResults.isNotEmpty()) {
+ val lastToken = parserResults.first()
+ if (lastToken.data !is LogicalOperator) {
+ var count = 0
+ while (i - 1 >= count && source[i - 1 - count] in whiteCharParser.prefix) {
+ count++
+ }
+ ExtSyntaxError.throwError(
+ source, i - count - lastToken.length, "LogicalOperator"
+ )
+ }
+ }
+ i++
+ parserResults.add(expressionParser(source, i).apply { i += length })
+ ExtSyntaxError.assert(source, i, ")")
+ i++
+ }
+
+ in "|&" -> {
+ parserResults.add(logicalOperatorParser(source, i).apply { i += length })
+ i += whiteCharParser(source, i).length
+ ExtSyntaxError.assert(source, i, "(" + propertyParser.prefix)
+ }
+
+ else -> {
+ parserResults.add(binaryExpressionParser(source, i).apply { i += length })
+ }
}
- val attrResult = attrParser(source, i)
- i += attrResult.length
- binaryExpressions.add(attrResult.data)
i += whiteCharParser(source, i).length
}
- if (binaryExpressions.isEmpty()) {
- ExtSyntaxError.throwError(source, i, "binaryExpression")
+ if (parserResults.isEmpty()) {
+ ExtSyntaxError.throwError(
+ source, i - offset, "Expression"
+ )
+ }
+ if (parserResults.size == 1) {
+ return ParserResult(parserResults.first().data as Expression, i - offset)
+ }
+
+// 运算符优先级 && > ||
+// a && b || c -> ab || c
+// 0 1 2 3 4 -> 0 1 2
+ val tokens = parserResults.map { it.data }.toMutableList()
+ var index = 0
+ while (index < tokens.size) {
+ val token = tokens[index]
+ if (token == LogicalOperator.AndOperator) {
+ tokens[index] = LogicalExpression(
+ left = tokens[index - 1] as Expression,
+ operator = LogicalOperator.AndOperator,
+ right = tokens[index + 1] as Expression
+ )
+ tokens.removeAt(index - 1)
+ tokens.removeAt(index + 1 - 1)
+ } else {
+ index++
+ }
}
+ while (tokens.size > 1) {
+ tokens[1] = LogicalExpression(
+ left = tokens[0] as Expression,
+ operator = tokens[1] as LogicalOperator.OrOperator,
+ right = tokens[2] as Expression
+ )
+ tokens.removeAt(0)
+ tokens.removeAt(2 - 1)
+ }
+ return ParserResult(tokens.first() as Expression, i - offset)
+ }
+
+
+ val attrParser = Parser("[") { source, offset, prefix ->
+ var i = offset
+ ExtSyntaxError.assert(source, i, prefix)
+ i++
+ i += whiteCharParser(source, i).length
+ val exp = expressionParser(source, i)
+ i += exp.length
ExtSyntaxError.assert(source, i, "]")
i++
ParserResult(
- OrExpression(
- binaryExpressions,
- ),
- i - offset
+ exp.data, i - offset
)
}
+
val selectorUnitParser = Parser { source, offset, _ ->
var i = offset
var tracked = false
@@ -349,16 +422,16 @@ internal object ParserSet {
}
val nameResult = nameParser(source, i)
i += nameResult.length
- val orExpressions = mutableListOf()
+ val expressions = mutableListOf()
while (i < source.length && source[i] == '[') {
- val attrResult = orParser(source, i)
+ val attrResult = attrParser(source, i)
i += attrResult.length
- orExpressions.add(attrResult.data)
+ expressions.add(attrResult.data)
}
- if (nameResult.length == 0 && orExpressions.size == 0) {
+ if (nameResult.length == 0 && expressions.size == 0) {
ExtSyntaxError.throwError(source, i, "[")
}
- ParserResult(PropertySegment(tracked, nameResult.data, orExpressions), i - offset)
+ ParserResult(PropertySegment(tracked, nameResult.data, expressions), i - offset)
}
val connectSelectorParser = Parser { source, offset, _ ->
@@ -409,8 +482,7 @@ internal object ParserSet {
}
val wrapperList = mutableListOf(PropertyWrapper(propertySelectorList.first()))
combinatorSelectorList.forEachIndexed { index, combinatorSelector ->
- val combinatorSelectorWrapper =
- ConnectWrapper(combinatorSelector, wrapperList.last())
+ val combinatorSelectorWrapper = ConnectWrapper(combinatorSelector, wrapperList.last())
val propertySelectorWrapper =
PropertyWrapper(propertySelectorList[index + 1], combinatorSelectorWrapper)
wrapperList.add(propertySelectorWrapper)
diff --git a/selector/src/jvmTest/kotlin/li/songe/selector/ParserTest.kt b/selector/src/jvmTest/kotlin/li/songe/selector/ParserTest.kt
index fe12f9787..6ba8228f1 100644
--- a/selector/src/jvmTest/kotlin/li/songe/selector/ParserTest.kt
+++ b/selector/src/jvmTest/kotlin/li/songe/selector/ParserTest.kt
@@ -6,16 +6,24 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.booleanOrNull
import kotlinx.serialization.json.intOrNull
+import li.songe.selector.parser.ParserSet
import org.junit.Test
import java.io.File
class ParserTest {
+ @Test
+ fun test_expression() {
+ println(ParserSet.expressionParser("a>1&&b>1&&c>1||d>1", 0).data)
+ println(Selector.parse("View[a>1&&b>1&&c>1||d>1&&x^=1] > Button[a>1||b*='zz'||c^=1]"))
+ }
+
@Test
fun string_selector() {
- val text = "Button > View[a=1||b=2||c$=3][x=3] Image > Layout @*"
- println(Selector.parse(text))
+ val text =
+ "ImageView < @FrameLayout < LinearLayout < RelativeLayout RelativeLayout > TextView[text\$='广告']"
+ println("trackIndex: " + Selector.parse(text).trackIndex)
}
@Test
diff --git a/settings.gradle.kts b/settings.gradle.kts
index b348f29f9..c52311d78 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -33,12 +33,12 @@ dependencyResolutionManagement {
version("android.buildToolsVersion", "33.0.2")
version("android.minSdk", "26")
- library("android.gradle", "com.android.tools.build:gradle:8.0.2")
+ library("android.gradle", "com.android.tools.build:gradle:8.1.0")
// 当前 android 项目 kotlin 的版本
library("kotlin.gradle.plugin", "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.20")
library("kotlin.serialization", "org.jetbrains.kotlin:kotlin-serialization:1.8.20")
-// library("kotlin.stdlib", "org.jetbrains.kotlin:kotlin-stdlib:1.8.10")
+ library("kotlin.stdlib.common", "org.jetbrains.kotlin:kotlin-stdlib-common:1.8.20")
// compose 编译器的版本, 需要注意它与 compose 的版本没有关联
// https://mvnrepository.com/artifact/androidx.compose.compiler/compiler
@@ -86,8 +86,7 @@ dependencyResolutionManagement {
library("androidx.appcompat", "androidx.appcompat:appcompat:1.6.1")
library("androidx.core.ktx", "androidx.core:core-ktx:1.10.0")
library(
- "androidx.lifecycle.runtime.ktx",
- "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1"
+ "androidx.lifecycle.runtime.ktx", "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1"
)
library("androidx.junit", "androidx.test.ext:junit:1.1.5")
library("androidx.espresso", "androidx.test.espresso:espresso-core:3.5.1")
@@ -109,7 +108,10 @@ dependencyResolutionManagement {
)
// https://google.github.io/accompanist/systemuicontroller/
- library("google.accompanist.systemuicontroller", "com.google.accompanist:accompanist-systemuicontroller:0.30.1")
+ library(
+ "google.accompanist.systemuicontroller",
+ "com.google.accompanist:accompanist-systemuicontroller:0.30.1"
+ )
library("junit", "junit:junit:4.13.2")
@@ -118,21 +120,18 @@ dependencyResolutionManagement {
library("ktor.server.netty", "io.ktor:ktor-server-netty:2.3.1")
library("ktor.server.cors", "io.ktor:ktor-server-cors:2.3.1")
library(
- "ktor.server.content.negotiation",
- "io.ktor:ktor-server-content-negotiation:2.3.1"
+ "ktor.server.content.negotiation", "io.ktor:ktor-server-content-negotiation:2.3.1"
)
library("ktor.client.core", "io.ktor:ktor-client-core:2.3.1")
// library("ktor.client.okhttp", "io.ktor:ktor-client-okhttp:2.3.1")
// https://ktor.io/docs/http-client-engines.html#android android 平台使用 android 或者 okhttp 都行
library("ktor.client.android", "io.ktor:ktor-client-android:2.3.1")
library(
- "ktor.client.content.negotiation",
- "io.ktor:ktor-client-content-negotiation:2.3.1"
+ "ktor.client.content.negotiation", "io.ktor:ktor-client-content-negotiation:2.3.1"
)
library(
- "ktor.serialization.kotlinx.json",
- "io.ktor:ktor-serialization-kotlinx-json:2.2.3"
+ "ktor.serialization.kotlinx.json", "io.ktor:ktor-serialization-kotlinx-json:2.2.3"
)
library(
@@ -150,10 +149,21 @@ dependencyResolutionManagement {
plugin("google.ksp", "com.google.devtools.ksp").version("1.8.20-1.0.11")
+ plugin("google.hilt", "com.google.dagger.hilt.android").version("2.44")
+ library("google.hilt.android", "com.google.dagger:hilt-android:2.44")
+ library("google.hilt.android.compiler", "com.google.dagger:hilt-android-compiler:2.44")
+ library("androidx.hilt.navigation.compose", "androidx.hilt:hilt-navigation-compose:1.0.0")
+
// https://composedestinations.rafaelcosta.xyz/setup
- library("destinations.core", "io.github.raamcosta.compose-destinations:core:1.8.42-beta")
+ library(
+ "destinations.core",
+ "io.github.raamcosta.compose-destinations:core:1.8.42-beta"
+ )
library("destinations.ksp", "io.github.raamcosta.compose-destinations:ksp:1.8.42-beta")
- library("destinations.animations", "io.github.raamcosta.compose-destinations:animations-core:1.8.42-beta")
+ library(
+ "destinations.animations",
+ "io.github.raamcosta.compose-destinations:animations-core:1.8.42-beta"
+ )
}
}
}