feat: 使用 viewModel

This commit is contained in:
lisonge
2023-08-10 22:02:12 +08:00
parent ae378968a9
commit 5e2a55ca4f
105 changed files with 2473 additions and 1767 deletions

View File

@@ -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/>
-

View File

@@ -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)
}

View File

@@ -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>

View File

@@ -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) } }
}
}
}
}
}

View File

@@ -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
)
}
}
}

View File

@@ -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))
}
}
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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,
)
}
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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() {

View File

@@ -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)
}
}
}
}
}

View File

@@ -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

View File

@@ -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>>
}
}

View File

@@ -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?
}
}

View File

@@ -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)
})
}

View File

@@ -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>
}
}

View File

@@ -1,3 +0,0 @@
package li.songe.gkd.data
data class Value<T>(var value: T)

View File

@@ -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() }
}

View File

@@ -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()
}

View File

@@ -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(

View File

@@ -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))
}

View File

@@ -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))
}

View File

@@ -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)
}
}
}

View File

@@ -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))
}

View File

@@ -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 {

View File

@@ -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)
}

View 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)
}

View 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)
}

View 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
)
}

View 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 = "显示服务运行状态"
)
}

View 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)
}
}

View File

@@ -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

View File

@@ -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()
// }
}
}

View 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))
}
}
}

View File

@@ -1,4 +1,4 @@
package li.songe.gkd.accessibility
package li.songe.gkd.service
import li.songe.gkd.composition.CompositionService

View File

@@ -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)
}
)
})
}

View File

@@ -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 ->

View 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 }
}
}
}

View 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)
})
}
}
}

View 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)
}

View 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()
}
}
})
}

View File

@@ -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)
)
}
}
}

View File

@@ -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))
}
}
}
}

View 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 }
}
}
}

View 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))
}
}
}
}
}

View File

@@ -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

View 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())
}

View File

@@ -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 = "添加")
}

View 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("成功添加订阅")
}
}

View File

@@ -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))
}
}
}

View 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
}
}
}

View 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 = {})
}

View File

@@ -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) })
}

View File

@@ -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
)

View File

@@ -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",
)
)
}

View File

@@ -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)
}

View File

@@ -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"
),
)

View File

@@ -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() }
}
}
})
}

View File

@@ -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
}
)
}
}
}

View File

@@ -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)
}
)
}
}
}

View File

@@ -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 = "查看关于")
}
}
}

View File

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

View File

@@ -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
)
}

View File

@@ -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) {

View File

@@ -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

View File

@@ -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

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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) }
}
}
}

View File

@@ -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

View File

@@ -0,0 +1,6 @@
package li.songe.gkd.util
import com.blankj.utilcode.util.ProcessUtils
val isMainProcess by lazy { ProcessUtils.isMainProcess() }

View File

@@ -1,4 +1,4 @@
package li.songe.gkd.utils
package li.songe.gkd.util
import androidx.compose.runtime.compositionLocalOf
import androidx.navigation.NavHostController

View File

@@ -1,14 +1,10 @@
package li.songe.gkd.utils
package li.songe.gkd.util
import li.songe.gkd.R
/**
* ![image](https://github.com/lisonge/gkd/assets/38517192/545c4fce-77b2-4003-8e22-a21b48ef3d98)
*/
@Suppress("UNRESOLVED_REFERENCE")
object SafeR {
val capture: Int = R.drawable.capture
val ic_capture: Int = R.drawable.ic_capture
val ic_add: Int = R.drawable.ic_add
val ic_app_2: Int = R.drawable.ic_app_2
val ic_apps: Int = R.drawable.ic_apps
@@ -25,4 +21,5 @@ object SafeR {
val ic_menu: Int = R.drawable.ic_menu
val ic_refresh: Int = R.drawable.ic_refresh
val ic_share: Int = R.drawable.ic_share
val ic_home: Int = R.drawable.ic_home
}

View File

@@ -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

View File

@@ -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

View File

@@ -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(

View File

@@ -1,4 +1,4 @@
package li.songe.gkd.utils
package li.songe.gkd.util
sealed class Status<out T> {
object Empty : Status<Nothing>()

View 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)
})
}

View File

@@ -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 {

View 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)
}

View 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)!! }

View File

@@ -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()) }

View File

@@ -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() }
}

View 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>

View File

@@ -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>

View File

@@ -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
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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?>
}

View File

@@ -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)
}

View File

@@ -5,4 +5,4 @@ import kotlin.js.JsExport
@OptIn(ExperimentalJsExport::class)
@JsExport
const val version = "0.0.4"
const val version = "0.0.5"

View File

@@ -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}${

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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"
}
}

View File

@@ -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)
}
}
}

View File

@@ -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