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" + ) } } }