mirror of
https://mirror.skon.top/github.com/gkd-kit/gkd
synced 2026-04-20 21:00:12 +08:00
feat: 使用 viewModel
This commit is contained in:
@@ -21,4 +21,10 @@ replace default "Embedded JDK" with download jdk by `File->Project Structrue...-
|
||||
|
||||
## Layout Inspector not working
|
||||
|
||||
开发者选项 - 启用视图属性检查功能
|
||||
开发者选项 - 启用视图属性检查功能
|
||||
|
||||
## 开发辅助文档
|
||||
|
||||
- <https://foso.github.io/Jetpack-Compose-Playground/>
|
||||
- <https://google.github.io/accompanist/>
|
||||
-
|
||||
@@ -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)
|
||||
}
|
||||
@@ -51,55 +51,17 @@
|
||||
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="i"
|
||||
android:host="import-subs"
|
||||
android:path="/"
|
||||
android:scheme="gkd" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".accessibility.GkdAbService"
|
||||
android:exported="false"
|
||||
android:label="@string/ab_label"
|
||||
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
|
||||
android:process=":remote">
|
||||
<intent-filter>
|
||||
<action android:name="android.accessibilityservice.AccessibilityService" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.accessibilityservice"
|
||||
android:resource="@xml/ab_desc" />
|
||||
</service>
|
||||
<service
|
||||
android:name=".debug.ScreenshotService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="mediaProjection"
|
||||
android:process=":remote" />
|
||||
<service
|
||||
android:name=".debug.HttpService"
|
||||
android:exported="false"
|
||||
android:process=":remote" />
|
||||
<service
|
||||
android:name=".debug.FloatingService"
|
||||
android:exported="false"
|
||||
android:process=":remote" />
|
||||
<service
|
||||
android:name=".accessibility.KeepAliveService"
|
||||
android:exported="false"
|
||||
android:process=":remote" />
|
||||
<service
|
||||
android:name=".accessibility.ShizukuService"
|
||||
android:exported="false"
|
||||
android:process=":remote" />
|
||||
|
||||
<provider
|
||||
android:name="rikka.shizuku.ShizukuProvider"
|
||||
android:authorities="${applicationId}.shizuku"
|
||||
@@ -118,6 +80,43 @@
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
<service
|
||||
android:name=".service.GkdAbService"
|
||||
android:exported="false"
|
||||
android:label="@string/ab_label"
|
||||
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
|
||||
android:process=":remote">
|
||||
<intent-filter>
|
||||
<action android:name="android.accessibilityservice.AccessibilityService" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.accessibilityservice"
|
||||
android:resource="@xml/ab_desc" />
|
||||
</service>
|
||||
<service
|
||||
android:name=".service.ManageService"
|
||||
android:exported="false"
|
||||
android:process=":remote" />
|
||||
<service
|
||||
android:name=".debug.ScreenshotService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="mediaProjection"
|
||||
android:process=":remote" />
|
||||
<service
|
||||
android:name=".debug.HttpService"
|
||||
android:exported="false"
|
||||
android:process=":remote" />
|
||||
<service
|
||||
android:name=".debug.FloatingService"
|
||||
android:exported="false"
|
||||
android:process=":remote" />
|
||||
<service
|
||||
android:name=".service.ShizukuService"
|
||||
android:exported="false"
|
||||
android:process=":remote" />
|
||||
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -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) } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Snapshot>(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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<String, AppInfo>()
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<String> = emptySet(),
|
||||
val key: Int? = null,
|
||||
val preKeys: Set<Int> = emptySet(),
|
||||
val group: SubscriptionRaw.GroupRaw,
|
||||
val subsItem: SubsItem,
|
||||
) {
|
||||
private var triggerTime = 0L
|
||||
fun trigger() {
|
||||
|
||||
@@ -2,80 +2,13 @@ package li.songe.gkd.data
|
||||
|
||||
import li.songe.selector.Selector
|
||||
|
||||
class RuleManager(vararg subscriptionRawArray: SubscriptionRaw) {
|
||||
class RuleManager(subsItems: List<SubsItem> = 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<String, MutableList<Rule>>()
|
||||
|
||||
init {
|
||||
subscriptionRawArray.forEach { subscriptionRaw ->
|
||||
subscriptionRaw.apps.forEach { appRaw ->
|
||||
val ruleConfigList = appToRulesMap[appRaw.id] ?: mutableListOf()
|
||||
appToRulesMap[appRaw.id] = ruleConfigList
|
||||
appRaw.groups.forEach { groupRaw ->
|
||||
val ruleGroupList = mutableListOf<Rule>()
|
||||
groupRaw.rules.forEach ruleEach@{ ruleRaw ->
|
||||
if (ruleRaw.matches.isEmpty()) return@ruleEach
|
||||
val cd = Rule.defaultMiniCd.coerceAtLeast(
|
||||
ruleRaw.cd ?: groupRaw.cd ?: appRaw.cd ?: Rule.defaultMiniCd
|
||||
)
|
||||
val activityIds =
|
||||
(ruleRaw.activityIds ?: groupRaw.activityIds ?: appRaw.activityIds
|
||||
?: listOf("*")).map { activityId ->
|
||||
if (activityId.startsWith('.')) {
|
||||
// .a.b.c -> com.x.y.x.a.b.c
|
||||
return@map appRaw.id + activityId
|
||||
}
|
||||
activityId
|
||||
}.toSet()
|
||||
|
||||
|
||||
val excludeActivityIds =
|
||||
(ruleRaw.excludeActivityIds ?: groupRaw.excludeActivityIds
|
||||
?: appRaw.excludeActivityIds
|
||||
?: emptyList()).toSet()
|
||||
|
||||
|
||||
ruleGroupList.add(
|
||||
Rule(
|
||||
cd = cd,
|
||||
index = count,
|
||||
matches = ruleRaw.matches.map { Selector.parse(it) },
|
||||
excludeMatches = ruleRaw.excludeMatches.map {
|
||||
Selector.parse(
|
||||
it
|
||||
)
|
||||
},
|
||||
appId = appRaw.id,
|
||||
activityIds = activityIds,
|
||||
excludeActivityIds = excludeActivityIds,
|
||||
key = ruleRaw.key,
|
||||
preKeys = ruleRaw.preKeys.toSet(),
|
||||
)
|
||||
)
|
||||
}
|
||||
ruleGroupList.forEachIndexed { index, ruleConfig ->
|
||||
ruleGroupList[index] = ruleConfig.copy(
|
||||
preRules = ruleGroupList.filter {
|
||||
it.key != null && it.preKeys.contains(
|
||||
it.key
|
||||
)
|
||||
}.toSet()
|
||||
)
|
||||
}
|
||||
ruleConfigList.addAll(ruleGroupList)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private val triggerLogQueue = ArrayDeque<TriggerRecord>()
|
||||
|
||||
@@ -115,4 +48,68 @@ class RuleManager(vararg subscriptionRawArray: SubscriptionRaw) {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
init {
|
||||
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<Rule>()
|
||||
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
|
||||
)
|
||||
val activityIds =
|
||||
(ruleRaw.activityIds ?: groupRaw.activityIds ?: appRaw.activityIds
|
||||
?: listOf("*")).map { activityId ->
|
||||
if (activityId.startsWith('.')) { // .a.b.c -> com.x.y.x.a.b.c
|
||||
return@map appRaw.id + activityId
|
||||
}
|
||||
activityId
|
||||
}.toSet()
|
||||
|
||||
|
||||
val excludeActivityIds =
|
||||
(ruleRaw.excludeActivityIds ?: groupRaw.excludeActivityIds
|
||||
?: appRaw.excludeActivityIds ?: emptyList()).toSet()
|
||||
|
||||
ruleGroupList.add(
|
||||
Rule(
|
||||
cd = cd,
|
||||
index = ruleIndex,
|
||||
matches = ruleRaw.matches.map { Selector.parse(it) },
|
||||
excludeMatches = ruleRaw.excludeMatches.map {
|
||||
Selector.parse(
|
||||
it
|
||||
)
|
||||
},
|
||||
appId = appRaw.id,
|
||||
activityIds = activityIds,
|
||||
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
|
||||
)
|
||||
}.toSet()
|
||||
)
|
||||
}
|
||||
ruleConfigList.addAll(ruleGroupList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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<NodeInfo> = 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
|
||||
|
||||
@@ -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<List<SubsConfig>>
|
||||
|
||||
@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<SubsConfig>
|
||||
fun queryGroupTypeConfig(subsItemId: Long, appId: String): Flow<List<SubsConfig>>
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<List<SubsItem>>
|
||||
|
||||
@Query("SELECT * FROM subs_item WHERE id=:id")
|
||||
fun queryById(id: Long): SubsItem?
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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<String>? = null,
|
||||
@SerialName("excludeActivityIds") val excludeActivityIds: List<String>? = 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)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Long>
|
||||
suspend fun insert(vararg objects: TriggerLog): List<Long>
|
||||
|
||||
@Delete
|
||||
suspend fun delete(vararg users: TriggerLog): Int
|
||||
suspend fun delete(vararg objects: TriggerLog): Int
|
||||
|
||||
@Query("SELECT * FROM trigger_log")
|
||||
suspend fun query(): List<TriggerLog>
|
||||
@Query("SELECT * FROM trigger_log ORDER BY id DESC")
|
||||
fun query(): Flow<List<TriggerLog>>
|
||||
|
||||
@Query("SELECT COUNT(*) FROM trigger_log")
|
||||
fun count(): Flow<Int>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
package li.songe.gkd.data
|
||||
|
||||
data class Value<T>(var value: T)
|
||||
@@ -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 <T : RoomDatabase> getDb(
|
||||
klass: Class<T>, name: String
|
||||
klass: Class<T>, 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() }
|
||||
|
||||
}
|
||||
@@ -6,9 +6,9 @@ import li.songe.gkd.data.NodeInfo
|
||||
object IgnoreConverters {
|
||||
@TypeConverter
|
||||
@JvmStatic
|
||||
fun listToCol(list: List<NodeInfo>): String? = null
|
||||
fun listToCol(list: List<NodeInfo>): String = ""
|
||||
|
||||
@TypeConverter
|
||||
@JvmStatic
|
||||
fun colToList(value: String?): List<NodeInfo> = emptyList()
|
||||
fun colToList(value: String): List<NodeInfo> = emptyList()
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
22
app/src/main/java/li/songe/gkd/icon/ArrowIcon.kt
Normal file
22
app/src/main/java/li/songe/gkd/icon/ArrowIcon.kt
Normal file
@@ -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)
|
||||
}
|
||||
22
app/src/main/java/li/songe/gkd/icon/HomeIcon.kt
Normal file
22
app/src/main/java/li/songe/gkd/icon/HomeIcon.kt
Normal file
@@ -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)
|
||||
}
|
||||
26
app/src/main/java/li/songe/gkd/notif/Notif.kt
Normal file
26
app/src/main/java/li/songe/gkd/notif/Notif.kt
Normal file
@@ -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
|
||||
)
|
||||
}
|
||||
13
app/src/main/java/li/songe/gkd/notif/NotifChannel.kt
Normal file
13
app/src/main/java/li/songe/gkd/notif/NotifChannel.kt
Normal file
@@ -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 = "显示服务运行状态"
|
||||
)
|
||||
}
|
||||
48
app/src/main/java/li/songe/gkd/notif/NotifManager.kt
Normal file
48
app/src/main/java/li/songe/gkd/notif/NotifManager.kt
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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<AccessibilityNodeInfo>(
|
||||
"isFocused" -> node.isFocused
|
||||
"isFocusable" -> node.isFocusable
|
||||
"isVisibleToUser" -> node.isVisibleToUser
|
||||
""->node.isAccessibilityFocused
|
||||
|
||||
"left" -> node.getTempRect().left
|
||||
"top" -> node.getTempRect().top
|
||||
@@ -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()
|
||||
// }
|
||||
|
||||
}
|
||||
}
|
||||
26
app/src/main/java/li/songe/gkd/service/ManageService.kt
Normal file
26
app/src/main/java/li/songe/gkd/service/ManageService.kt
Normal file
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package li.songe.gkd.accessibility
|
||||
package li.songe.gkd.service
|
||||
|
||||
import li.songe.gkd.composition.CompositionService
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
}
|
||||
@@ -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<SubsConfig?>? by remember { mutableStateOf(null) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
val mutableSet = DbSet.subsConfigDao.queryGroupTypeConfig(subsConfig.subsItemId, subsApp.id)
|
||||
val list = mutableListOf<SubsConfig?>()
|
||||
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<AppItemVm>()
|
||||
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,
|
||||
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()
|
||||
)
|
||||
Text(
|
||||
text = group.desc ?: "-",
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
.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()
|
||||
)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = "-",
|
||||
modifier = Modifier
|
||||
.width(48.dp)
|
||||
.wrapContentHeight(),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
|
||||
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 ->
|
||||
|
||||
35
app/src/main/java/li/songe/gkd/ui/AppItemVm.kt
Normal file
35
app/src/main/java/li/songe/gkd/ui/AppItemVm.kt
Normal file
@@ -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<SubscriptionRaw.AppRaw?>(null)
|
||||
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val subscriptionRaw = SubsItem.getSubscriptionRaw(args.subsItemId) ?: return@launch
|
||||
subsAppFlow.value = subscriptionRaw.apps.find { it.id == args.appId }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
115
app/src/main/java/li/songe/gkd/ui/ControlPage.kt
Normal file
115
app/src/main/java/li/songe/gkd/ui/ControlPage.kt
Normal file
@@ -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<ControlVm>()
|
||||
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<Boolean> {
|
||||
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)
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
14
app/src/main/java/li/songe/gkd/ui/ControlVm.kt
Normal file
14
app/src/main/java/li/songe/gkd/ui/ControlVm.kt
Normal file
@@ -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)
|
||||
}
|
||||
80
app/src/main/java/li/songe/gkd/ui/HomePage.kt
Normal file
80
app/src/main/java/li/songe/gkd/ui/HomePage.kt
Normal file
@@ -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<HomePageVm>()
|
||||
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()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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<Bitmap?>(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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<RecordVm>()
|
||||
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<TriggerLog?>(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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
22
app/src/main/java/li/songe/gkd/ui/RecordVm.kt
Normal file
22
app/src/main/java/li/songe/gkd/ui/RecordVm.kt
Normal file
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
266
app/src/main/java/li/songe/gkd/ui/SettingsPage.kt
Normal file
266
app/src/main/java/li/songe/gkd/ui/SettingsPage.kt
Normal file
@@ -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<Boolean> {
|
||||
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<Boolean> {
|
||||
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<Boolean> {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<SnapshotVm>()
|
||||
val snapshots by vm.snapshotsState.collectAsState()
|
||||
|
||||
var snapshots by remember {
|
||||
mutableStateOf(listOf<Snapshot>())
|
||||
}
|
||||
var selectedSnapshot by remember {
|
||||
mutableStateOf<Snapshot?>(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
|
||||
|
||||
16
app/src/main/java/li/songe/gkd/ui/SnapshotVm.kt
Normal file
16
app/src/main/java/li/songe/gkd/ui/SnapshotVm.kt
Normal file
@@ -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())
|
||||
}
|
||||
@@ -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<SubsItem>()) }
|
||||
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<SubsManageVm>()
|
||||
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<List<String>> { 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<List<SubsItem>> { 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<Boolean> {
|
||||
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 = "添加")
|
||||
}
|
||||
63
app/src/main/java/li/songe/gkd/ui/SubsManageVm.kt
Normal file
63
app/src/main/java/li/songe/gkd/ui/SubsManageVm.kt
Normal file
@@ -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("成功添加订阅")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<SubsAppCardData>? by rememberCache { mutableStateOf(null) }
|
||||
val vm = hiltViewModel<SubsVm>()
|
||||
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)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
} 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]
|
||||
)
|
||||
}
|
||||
subsAppCards = newSubsAppCards
|
||||
}
|
||||
}
|
||||
|
||||
val openAppPage = scope.useTask().launchAsFn<SubsAppCardData> {
|
||||
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<SubsAppCardData> {
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
31
app/src/main/java/li/songe/gkd/ui/SubsVm.kt
Normal file
31
app/src/main/java/li/songe/gkd/ui/SubsVm.kt
Normal file
@@ -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<SubsItem?>(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
|
||||
}
|
||||
}
|
||||
}
|
||||
45
app/src/main/java/li/songe/gkd/ui/component/SettingItem.kt
Normal file
45
app/src/main/java/li/songe/gkd/ui/component/SettingItem.kt
Normal file
@@ -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 = {})
|
||||
}
|
||||
@@ -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) })
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
.size(30.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(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",
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"
|
||||
),
|
||||
)
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Boolean> {
|
||||
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<Boolean> {
|
||||
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<Boolean> {
|
||||
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<Boolean> {
|
||||
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 = "查看关于")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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) {
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
6
app/src/main/java/li/songe/gkd/util/Multiprocess.kt
Normal file
6
app/src/main/java/li/songe/gkd/util/Multiprocess.kt
Normal file
@@ -0,0 +1,6 @@
|
||||
package li.songe.gkd.util
|
||||
|
||||
import com.blankj.utilcode.util.ProcessUtils
|
||||
|
||||
|
||||
val isMainProcess by lazy { ProcessUtils.isMainProcess() }
|
||||
@@ -1,4 +1,4 @@
|
||||
package li.songe.gkd.utils
|
||||
package li.songe.gkd.util
|
||||
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.navigation.NavHostController
|
||||
@@ -1,14 +1,10 @@
|
||||
package li.songe.gkd.utils
|
||||
package li.songe.gkd.util
|
||||
|
||||
import li.songe.gkd.R
|
||||
|
||||
|
||||
/**
|
||||
* 
|
||||
*/
|
||||
@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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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(
|
||||
@@ -1,4 +1,4 @@
|
||||
package li.songe.gkd.utils
|
||||
package li.songe.gkd.util
|
||||
|
||||
sealed class Status<out T> {
|
||||
object Empty : Status<Nothing>()
|
||||
64
app/src/main/java/li/songe/gkd/util/Store.kt
Normal file
64
app/src/main/java/li/songe/gkd/util/Store.kt
Normal file
@@ -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<StateFlow<Store>> {
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 <T> 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<T> {
|
||||
return scope.launchAsFn<T>(context, start) {
|
||||
if (innerLoading) return@launchAsFn
|
||||
innerLoading = true
|
||||
onChangeLoading(changeLoading)
|
||||
onChangeLoading(true)
|
||||
val start = System.currentTimeMillis()
|
||||
try {
|
||||
try {
|
||||
37
app/src/main/java/li/songe/gkd/util/TimeExt.kt
Normal file
37
app/src/main/java/li/songe/gkd/util/TimeExt.kt
Normal file
@@ -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<String, SimpleDateFormat>()
|
||||
|
||||
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)
|
||||
}
|
||||
6
app/src/main/java/li/songe/gkd/util/kv.kt
Normal file
6
app/src/main/java/li/songe/gkd/util/kv.kt
Normal file
@@ -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)!! }
|
||||
@@ -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()) }
|
||||
@@ -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() }
|
||||
}
|
||||
11
app/src/main/res/drawable/ic_home.xml
Normal file
11
app/src/main/res/drawable/ic_home.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:autoMirrored="true"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="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" />
|
||||
</vector>
|
||||
@@ -2,5 +2,5 @@
|
||||
<resources>
|
||||
<string name="app_name">搞快点</string>
|
||||
<string name="ab_label">搞快点</string>
|
||||
<string name="ab_desc">基于规则匹配的无障碍速点服务</string>
|
||||
<string name="ab_desc">基于规则匹配的无障碍速点服务\n强大的自定义规则能帮助你实现点击关闭各种广告, 自定义快捷操作等高级功能</string>
|
||||
</resources>
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 <T> invoke(node: T, transform: Transform<T>): Boolean
|
||||
}
|
||||
@@ -11,4 +15,3 @@ internal interface NodeSequenceFc {
|
||||
internal interface NodeTraversalFc {
|
||||
operator fun <T> invoke(node: T, transform: Transform<T>): Sequence<T?>
|
||||
}
|
||||
|
||||
|
||||
@@ -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<T>,
|
||||
trackNodes: MutableList<T> = mutableListOf(),
|
||||
): List<T>? {
|
||||
trackNodes.clear()
|
||||
return propertyWrapper.matchTracks(node, transform, trackNodes)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,4 +5,4 @@ import kotlin.js.JsExport
|
||||
|
||||
@OptIn(ExperimentalJsExport::class)
|
||||
@JsExport
|
||||
const val version = "0.0.4"
|
||||
const val version = "0.0.5"
|
||||
|
||||
@@ -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 <T> match(node: T, transform: Transform<T>) =
|
||||
data class BinaryExpression(val name: String, val operator: CompareOperator, val value: Any?) :
|
||||
Expression() {
|
||||
override fun <T> match(node: T, transform: Transform<T>) =
|
||||
operator.compare(transform.getAttr(node, name), value)
|
||||
|
||||
override fun toString() = "${name}${operator}${
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package li.songe.selector.data
|
||||
|
||||
import li.songe.selector.Transform
|
||||
|
||||
sealed class Expression {
|
||||
abstract fun <T> match(node: T, transform: Transform<T>): Boolean
|
||||
}
|
||||
@@ -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 <T> match(node: T, transform: Transform<T>): 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"
|
||||
}
|
||||
}
|
||||
@@ -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 <T> compare(
|
||||
node: T,
|
||||
transform: Transform<T>,
|
||||
left: Expression,
|
||||
right: Expression,
|
||||
): Boolean
|
||||
|
||||
object AndOperator : LogicalOperator("&&") {
|
||||
override fun <T> compare(
|
||||
node: T,
|
||||
transform: Transform<T>,
|
||||
left: Expression,
|
||||
right: Expression,
|
||||
): Boolean {
|
||||
return left.match(node, transform) && right.match(node, transform)
|
||||
}
|
||||
}
|
||||
|
||||
object OrOperator : LogicalOperator("||") {
|
||||
override fun <T> compare(
|
||||
node: T,
|
||||
transform: Transform<T>,
|
||||
left: Expression,
|
||||
right: Expression,
|
||||
): Boolean {
|
||||
return left.match(node, transform) || right.match(node, transform)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package li.songe.selector.data
|
||||
|
||||
import li.songe.selector.Transform
|
||||
|
||||
data class OrExpression(val expressions: List<BinaryExpression>) {
|
||||
override fun toString(): String {
|
||||
if (expressions.isEmpty()) return ""
|
||||
return "[" + expressions.joinToString("||") + "]"
|
||||
}
|
||||
|
||||
fun <T> match(node: T, transform: Transform<T>): Boolean {
|
||||
return expressions.any { ex -> ex.match(node, transform) }
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user