mirror of
https://github.com/gkd-kit/gkd.git
synced 2024-11-16 03:32:38 +08:00
feat: multiplatform
This commit is contained in:
parent
fd999da16f
commit
007655206d
1
_assets/snapshot-1686629593092.json
Normal file
1
_assets/snapshot-1686629593092.json
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,12 +1,17 @@
|
||||||
|
import com.android.build.gradle.internal.cxx.json.jsonStringOf
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("kotlin-android")
|
|
||||||
id("kotlin-parcelize")
|
id("kotlin-parcelize")
|
||||||
id("kotlin-kapt")
|
kotlin("android")
|
||||||
id("org.jetbrains.kotlin.plugin.serialization")
|
kotlin("plugin.serialization")
|
||||||
id("org.jetbrains.kotlin.android")
|
id("com.google.devtools.ksp")
|
||||||
|
id("dev.rikka.tools.refine")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Suppress("UnstableApiUsage")
|
@Suppress("UnstableApiUsage")
|
||||||
android {
|
android {
|
||||||
namespace = "li.songe.gkd"
|
namespace = "li.songe.gkd"
|
||||||
|
@ -26,12 +31,17 @@ android {
|
||||||
useSupportLibrary = true
|
useSupportLibrary = true
|
||||||
}
|
}
|
||||||
|
|
||||||
kapt {
|
javaCompileOptions {
|
||||||
arguments {
|
annotationProcessorOptions {
|
||||||
// room 依赖每次构建的产物来执行自动迁移
|
arguments += mapOf(
|
||||||
arg("room.schemaLocation", "$projectDir/schemas")
|
"room.schemaLocation" to "$projectDir/schemas",
|
||||||
|
"room.incremental" to "true"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val nowTime = System.currentTimeMillis()
|
||||||
|
buildConfigField("Long", "BUILD_TIME", jsonStringOf(nowTime) + "L")
|
||||||
|
buildConfigField("String", "BUILD_DATE", jsonStringOf(SimpleDateFormat("yyyy-MM-dd HH:mm:ss ZZ", Locale.SIMPLIFIED_CHINESE).format(nowTime)))
|
||||||
}
|
}
|
||||||
|
|
||||||
lint {
|
lint {
|
||||||
|
@ -47,18 +57,8 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
|
||||||
sourceSets.debug {
|
|
||||||
kotlin.srcDir("build/generated/ksp/debug/kotlin")
|
|
||||||
}
|
|
||||||
sourceSets.release {
|
|
||||||
kotlin.srcDir("build/generated/ksp/release/kotlin")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
manifestPlaceholders += mapOf()
|
|
||||||
isMinifyEnabled = false
|
isMinifyEnabled = false
|
||||||
setProguardFiles(
|
setProguardFiles(
|
||||||
listOf(
|
listOf(
|
||||||
|
@ -67,31 +67,31 @@ android {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
signingConfig = signingConfigs.getByName("release")
|
signingConfig = signingConfigs.getByName("release")
|
||||||
manifestPlaceholders["appName"] = "搞快点"
|
manifestPlaceholders["appName"] = "GKD"
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
applicationIdSuffix = ".debug"
|
applicationIdSuffix = ".debug"
|
||||||
signingConfig = signingConfigs.getByName("release")
|
signingConfig = signingConfigs.getByName("release")
|
||||||
manifestPlaceholders["appName"] = "搞快点-dev"
|
manifestPlaceholders["appName"] = "GKD-debug"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "1.8"
|
jvmTarget = JavaVersion.VERSION_17.majorVersion
|
||||||
freeCompilerArgs = freeCompilerArgs + "-opt-in=kotlin.RequiresOptIn"
|
freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
|
||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
buildConfig = true
|
buildConfig = true
|
||||||
compose = true
|
compose = true
|
||||||
}
|
}
|
||||||
composeOptions {
|
composeOptions {
|
||||||
kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get()
|
kotlinCompilerExtensionVersion = libs.versions.compose.compilerVersion.get()
|
||||||
}
|
}
|
||||||
packagingOptions {
|
packaging {
|
||||||
resources {
|
resources {
|
||||||
// Due to https://github.com/Kotlin/kotlinx.coroutines/issues/2023
|
// Due to https://github.com/Kotlin/kotlinx.coroutines/issues/2023
|
||||||
excludes += "META-INF/INDEX.LIST"
|
excludes += "META-INF/INDEX.LIST"
|
||||||
|
@ -106,16 +106,20 @@ android {
|
||||||
exclude("org.jetbrains.kotlinx", "kotlinx-coroutines-debug")
|
exclude("org.jetbrains.kotlinx", "kotlinx-coroutines-debug")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ksp
|
||||||
|
sourceSets.configureEach {
|
||||||
|
kotlin.srcDir("$buildDir/generated/ksp/$name/kotlin/")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation(project(mapOf("path" to ":selector_core")))
|
|
||||||
implementation(project(mapOf("path" to ":router")))
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
|
||||||
|
implementation(project(mapOf("path" to ":selector")))
|
||||||
implementation(libs.androidx.appcompat)
|
implementation(libs.androidx.appcompat)
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
implementation(libs.androidx.localbroadcastmanager)
|
|
||||||
|
|
||||||
implementation(libs.compose.ui)
|
implementation(libs.compose.ui)
|
||||||
implementation(libs.compose.material)
|
implementation(libs.compose.material)
|
||||||
|
@ -128,15 +132,18 @@ dependencies {
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso)
|
androidTestImplementation(libs.androidx.espresso)
|
||||||
|
|
||||||
|
|
||||||
|
compileOnly(project(mapOf("path" to ":hidden_api")))
|
||||||
implementation(libs.rikka.shizuku.api)
|
implementation(libs.rikka.shizuku.api)
|
||||||
implementation(libs.rikka.shizuku.provider)
|
implementation(libs.rikka.shizuku.provider)
|
||||||
|
implementation(libs.lsposed.hiddenapibypass)
|
||||||
|
|
||||||
implementation(libs.tencent.bugly)
|
implementation(libs.tencent.bugly)
|
||||||
implementation(libs.tencent.mmkv)
|
implementation(libs.tencent.mmkv)
|
||||||
|
|
||||||
implementation(libs.androidx.room.runtime)
|
implementation(libs.androidx.room.runtime)
|
||||||
kapt(libs.androidx.room.compiler)
|
|
||||||
implementation(libs.androidx.room.ktx)
|
implementation(libs.androidx.room.ktx)
|
||||||
|
ksp(libs.androidx.room.compiler)
|
||||||
|
|
||||||
implementation(libs.ktor.server.core)
|
implementation(libs.ktor.server.core)
|
||||||
implementation(libs.ktor.server.netty)
|
implementation(libs.ktor.server.netty)
|
||||||
|
@ -144,12 +151,13 @@ dependencies {
|
||||||
implementation(libs.ktor.server.content.negotiation)
|
implementation(libs.ktor.server.content.negotiation)
|
||||||
|
|
||||||
implementation(libs.ktor.client.core)
|
implementation(libs.ktor.client.core)
|
||||||
implementation(libs.ktor.client.cio)
|
implementation(libs.ktor.client.android)
|
||||||
implementation(libs.ktor.client.content.negotiation)
|
implementation(libs.ktor.client.content.negotiation)
|
||||||
implementation(libs.ktor.serialization.kotlinx.json)
|
implementation(libs.ktor.serialization.kotlinx.json)
|
||||||
|
|
||||||
implementation(libs.google.accompanist.drawablepainter)
|
implementation(libs.google.accompanist.drawablepainter)
|
||||||
implementation(libs.google.accompanist.placeholder.material)
|
implementation(libs.google.accompanist.placeholder.material)
|
||||||
|
implementation(libs.google.accompanist.systemuicontroller)
|
||||||
|
|
||||||
implementation(libs.kotlinx.serialization.json)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
implementation(libs.kotlinx.collections.immutable)
|
implementation(libs.kotlinx.collections.immutable)
|
||||||
|
@ -160,4 +168,9 @@ dependencies {
|
||||||
implementation(libs.others.zxing.android.embedded)
|
implementation(libs.others.zxing.android.embedded)
|
||||||
implementation(libs.others.floating.bubble.view)
|
implementation(libs.others.floating.bubble.view)
|
||||||
|
|
||||||
|
implementation(libs.destinations.core)
|
||||||
|
implementation(libs.destinations.animations)
|
||||||
|
ksp(libs.destinations.ksp)
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -2,7 +2,7 @@
|
||||||
"formatVersion": 1,
|
"formatVersion": 1,
|
||||||
"database": {
|
"database": {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"identityHash": "2083d8585fffd897fde3733958e356f8",
|
"identityHash": "f3feda76127233f3416d7570fca1615f",
|
||||||
"entities": [
|
"entities": [
|
||||||
{
|
{
|
||||||
"tableName": "subs_item",
|
"tableName": "subs_item",
|
||||||
|
@ -149,12 +149,116 @@
|
||||||
},
|
},
|
||||||
"indices": [],
|
"indices": [],
|
||||||
"foreignKeys": []
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "snapshot",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `app_name` TEXT, `app_version_code` INTEGER, `app_version_name` TEXT, `screen_height` INTEGER NOT NULL, `screen_width` INTEGER NOT NULL, `is_landscape` INTEGER NOT NULL, `device` TEXT NOT NULL, `model` TEXT NOT NULL, `manufacturer` TEXT NOT NULL, `brand` TEXT NOT NULL, `sdk_int` INTEGER NOT NULL, `release` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "appId",
|
||||||
|
"columnName": "app_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "activityId",
|
||||||
|
"columnName": "activity_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "appName",
|
||||||
|
"columnName": "app_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "appVersionCode",
|
||||||
|
"columnName": "app_version_code",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "appVersionName",
|
||||||
|
"columnName": "app_version_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "screenHeight",
|
||||||
|
"columnName": "screen_height",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "screenWidth",
|
||||||
|
"columnName": "screen_width",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isLandscape",
|
||||||
|
"columnName": "is_landscape",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "device",
|
||||||
|
"columnName": "device",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "model",
|
||||||
|
"columnName": "model",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "manufacturer",
|
||||||
|
"columnName": "manufacturer",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "brand",
|
||||||
|
"columnName": "brand",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sdkInt",
|
||||||
|
"columnName": "sdk_int",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "release",
|
||||||
|
"columnName": "release",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"views": [],
|
"views": [],
|
||||||
"setupQueries": [
|
"setupQueries": [
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2083d8585fffd897fde3733958e356f8')"
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f3feda76127233f3416d7570fca1615f')"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
124
app/schemas/li.songe.gkd.db.SnapshotDb/1.json
Normal file
124
app/schemas/li.songe.gkd.db.SnapshotDb/1.json
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 1,
|
||||||
|
"identityHash": "296a7b78252c48246f24767e66441c22",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "snapshot",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `app_name` TEXT, `app_version_code` INTEGER, `app_version_name` TEXT, `screen_height` INTEGER NOT NULL, `screen_width` INTEGER NOT NULL, `is_landscape` INTEGER NOT NULL, `device` TEXT NOT NULL, `model` TEXT NOT NULL, `manufacturer` TEXT NOT NULL, `brand` TEXT NOT NULL, `sdk_int` INTEGER NOT NULL, `release` TEXT NOT NULL, `_1` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "appId",
|
||||||
|
"columnName": "app_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "activityId",
|
||||||
|
"columnName": "activity_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "appName",
|
||||||
|
"columnName": "app_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "appVersionCode",
|
||||||
|
"columnName": "app_version_code",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "appVersionName",
|
||||||
|
"columnName": "app_version_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "screenHeight",
|
||||||
|
"columnName": "screen_height",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "screenWidth",
|
||||||
|
"columnName": "screen_width",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isLandscape",
|
||||||
|
"columnName": "is_landscape",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "device",
|
||||||
|
"columnName": "device",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "model",
|
||||||
|
"columnName": "model",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "manufacturer",
|
||||||
|
"columnName": "manufacturer",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "brand",
|
||||||
|
"columnName": "brand",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sdkInt",
|
||||||
|
"columnName": "sdk_int",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "release",
|
||||||
|
"columnName": "release",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "nodes",
|
||||||
|
"columnName": "_1",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '296a7b78252c48246f24767e66441c22')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
70
app/schemas/li.songe.gkd.db.SubsConfigDb/1.json
Normal file
70
app/schemas/li.songe.gkd.db.SubsConfigDb/1.json
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 1,
|
||||||
|
"identityHash": "5ad1f90d8f2852410fde46463bf24322",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "subs_config",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `type` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `subs_item_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `group_key` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mtime",
|
||||||
|
"columnName": "mtime",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "type",
|
||||||
|
"columnName": "type",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "enable",
|
||||||
|
"columnName": "enable",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subsItemId",
|
||||||
|
"columnName": "subs_item_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "appId",
|
||||||
|
"columnName": "app_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "groupKey",
|
||||||
|
"columnName": "group_key",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5ad1f90d8f2852410fde46463bf24322')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
88
app/schemas/li.songe.gkd.db.SubsItemDb/1.json
Normal file
88
app/schemas/li.songe.gkd.db.SubsItemDb/1.json
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 1,
|
||||||
|
"identityHash": "b51332e64931ac0cef5774cb5df5b703",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "subs_item",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `enable_update` INTEGER NOT NULL, `order` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `version` INTEGER NOT NULL, `update_url` TEXT NOT NULL, `support_url` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mtime",
|
||||||
|
"columnName": "mtime",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "enable",
|
||||||
|
"columnName": "enable",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "enableUpdate",
|
||||||
|
"columnName": "enable_update",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "order",
|
||||||
|
"columnName": "order",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "author",
|
||||||
|
"columnName": "author",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "version",
|
||||||
|
"columnName": "version",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "updateUrl",
|
||||||
|
"columnName": "update_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "supportUrl",
|
||||||
|
"columnName": "support_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b51332e64931ac0cef5774cb5df5b703')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
34
app/schemas/li.songe.gkd.db.TriggerLogDb/1.json
Normal file
34
app/schemas/li.songe.gkd.db.TriggerLogDb/1.json
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 1,
|
||||||
|
"identityHash": "e565cbca157f8ba6cecb6e7cd7cc6304",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "trigger_log",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e565cbca157f8ba6cecb6e7cd7cc6304')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,16 +17,21 @@
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||||
|
|
||||||
|
<!--
|
||||||
|
APP 有两个进程, 主进程 + :remote 进程
|
||||||
|
主进程: 主要是 activity 的前端界面
|
||||||
|
remote进程: 主要是 service
|
||||||
|
优点: 在最近任务界面删除当前APP的窗口记录时,不会让 remote进程里的 service 停止
|
||||||
|
-->
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name="li.songe.gkd.App"
|
android:name="li.songe.gkd.App"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@drawable/ic_launcher"
|
||||||
android:label="${appName}"
|
android:label="${appName}"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@drawable/ic_launcher_round"
|
||||||
android:supportsRtl="false"
|
android:supportsRtl="false"
|
||||||
android:theme="@style/Theme.Gkd.NoActionBar">
|
android:theme="@style/AppTheme">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.journeyapps.barcodescanner.CaptureActivity"
|
android:name="com.journeyapps.barcodescanner.CaptureActivity"
|
||||||
|
@ -36,8 +41,7 @@
|
||||||
<activity
|
<activity
|
||||||
android:name="li.songe.gkd.MainActivity"
|
android:name="li.songe.gkd.MainActivity"
|
||||||
android:configChanges="uiMode|screenSize|orientation|keyboardHidden|touchscreen|smallestScreenSize|screenLayout|navigation|mnc|mcc|locale|layoutDirection|keyboard|fontWeightAdjustment|fontScale|density|colorMode"
|
android:configChanges="uiMode|screenSize|orientation|keyboardHidden|touchscreen|smallestScreenSize|screenLayout|navigation|mnc|mcc|locale|layoutDirection|keyboard|fontWeightAdjustment|fontScale|density|colorMode"
|
||||||
android:exported="true"
|
android:exported="true">
|
||||||
android:launchMode="singleInstance">
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
about android:configChanges
|
about android:configChanges
|
||||||
|
@ -63,29 +67,39 @@
|
||||||
<service
|
<service
|
||||||
android:name=".accessibility.GkdAbService"
|
android:name=".accessibility.GkdAbService"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:label="@string/accessibility_service_label"
|
android:label="@string/ab_label"
|
||||||
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
|
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
|
||||||
|
android:process=":remote">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.accessibilityservice.AccessibilityService" />
|
<action android:name="android.accessibilityservice.AccessibilityService" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.accessibilityservice"
|
android:name="android.accessibilityservice"
|
||||||
android:resource="@xml/accessibility_service_description" />
|
android:resource="@xml/ab_desc" />
|
||||||
</service>
|
</service>
|
||||||
<service
|
<service
|
||||||
android:name=".debug.ScreenshotService"
|
android:name=".debug.ScreenshotService"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="mediaProjection" />
|
android:foregroundServiceType="mediaProjection"
|
||||||
|
android:process=":remote" />
|
||||||
<service
|
<service
|
||||||
android:name=".debug.HttpService"
|
android:name=".debug.HttpService"
|
||||||
android:exported="false" />
|
android:exported="false"
|
||||||
|
android:process=":remote" />
|
||||||
<service
|
<service
|
||||||
android:name=".debug.FloatingService"
|
android:name=".debug.FloatingService"
|
||||||
android:exported="false" />
|
android:exported="false"
|
||||||
|
android:process=":remote" />
|
||||||
<service
|
<service
|
||||||
android:name=".accessibility.KeepAliveService"
|
android:name=".accessibility.KeepAliveService"
|
||||||
android:exported="false" /> <!-- This provider is required by Shizuku, remove this if your app only supports Sui -->
|
android:exported="false"
|
||||||
|
android:process=":remote" />
|
||||||
|
<service
|
||||||
|
android:name=".accessibility.ShizukuService"
|
||||||
|
android:exported="false"
|
||||||
|
android:process=":remote" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="rikka.shizuku.ShizukuProvider"
|
android:name="rikka.shizuku.ShizukuProvider"
|
||||||
android:authorities="${applicationId}.shizuku"
|
android:authorities="${applicationId}.shizuku"
|
||||||
|
@ -93,6 +107,17 @@
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:multiprocess="false"
|
android:multiprocess="false"
|
||||||
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.provider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_paths" />
|
||||||
|
</provider>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
|
@ -1,22 +1,35 @@
|
||||||
package li.songe.gkd
|
package li.songe.gkd
|
||||||
|
|
||||||
import android.app.Application
|
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.blankj.utilcode.util.LogUtils
|
||||||
import com.tencent.bugly.crashreport.CrashReport
|
import com.tencent.bugly.crashreport.CrashReport
|
||||||
import com.tencent.mmkv.MMKV
|
import com.tencent.mmkv.MMKV
|
||||||
import li.songe.gkd.util.Storage
|
import li.songe.gkd.utils.Storage
|
||||||
|
import org.lsposed.hiddenapibypass.HiddenApiBypass
|
||||||
|
import rikka.shizuku.ShizukuProvider
|
||||||
|
|
||||||
class App : Application() {
|
class App : Application() {
|
||||||
companion object {
|
companion object {
|
||||||
lateinit var context: Application
|
lateinit var context: Application
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun attachBaseContext(base: Context?) {
|
||||||
|
super.attachBaseContext(base)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
HiddenApiBypass.addHiddenApiExemptions("L")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
context = this
|
context = this
|
||||||
MMKV.initialize(this)
|
MMKV.initialize(this)
|
||||||
LogUtils.d(Storage.settings)
|
LogUtils.d(Storage.settings)
|
||||||
if (!Storage.settings.enableConsoleLogOut){
|
if (!Storage.settings.enableConsoleLogOut) {
|
||||||
LogUtils.d("关闭日志控制台输出")
|
LogUtils.d("关闭日志控制台输出")
|
||||||
}
|
}
|
||||||
LogUtils.getConfig().apply {
|
LogUtils.getConfig().apply {
|
||||||
|
@ -24,6 +37,7 @@ class App : Application() {
|
||||||
saveDays = 30
|
saveDays = 30
|
||||||
LogUtils.getConfig().setConsoleSwitch(Storage.settings.enableConsoleLogOut)
|
LogUtils.getConfig().setConsoleSwitch(Storage.settings.enableConsoleLogOut)
|
||||||
}
|
}
|
||||||
|
ShizukuProvider.enableMultiProcessSupport(true)
|
||||||
CrashReport.initCrashReport(applicationContext, "d0ce46b353", false)
|
CrashReport.initCrashReport(applicationContext, "d0ce46b353", false)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,18 +1,24 @@
|
||||||
package li.songe.gkd
|
package li.songe.gkd
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import android.os.Build
|
||||||
|
import android.view.WindowManager
|
||||||
import androidx.activity.compose.setContent
|
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.runtime.CompositionLocalProvider
|
||||||
import com.blankj.utilcode.util.LogUtils
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
|
||||||
import com.dylanc.activityresult.launcher.StartActivityLauncher
|
import com.dylanc.activityresult.launcher.StartActivityLauncher
|
||||||
|
import com.ramcosta.composedestinations.DestinationsNavHost
|
||||||
import li.songe.gkd.composition.CompositionActivity
|
import li.songe.gkd.composition.CompositionActivity
|
||||||
import li.songe.gkd.composition.CompositionExt.useLifeCycleLog
|
import li.songe.gkd.composition.CompositionExt.useLifeCycleLog
|
||||||
import li.songe.gkd.ui.home.HomePage
|
import li.songe.gkd.ui.NavGraphs
|
||||||
import li.songe.gkd.ui.theme.MainTheme
|
import li.songe.gkd.ui.theme.AppTheme
|
||||||
import li.songe.gkd.util.Ext.LocalLauncher
|
import li.songe.gkd.utils.LocalLauncher
|
||||||
import li.songe.gkd.util.Storage
|
import li.songe.gkd.utils.LocalNavController
|
||||||
import li.songe.gkd.util.UseHook
|
import li.songe.gkd.utils.StackCacheProvider
|
||||||
import li.songe.router.RouterHost
|
import li.songe.gkd.utils.Storage
|
||||||
|
|
||||||
|
|
||||||
class MainActivity : CompositionActivity({
|
class MainActivity : CompositionActivity({
|
||||||
|
@ -20,30 +26,63 @@ class MainActivity : CompositionActivity({
|
||||||
|
|
||||||
val launcher = StartActivityLauncher(this)
|
val launcher = StartActivityLauncher(this)
|
||||||
onFinish { fs ->
|
onFinish { fs ->
|
||||||
LogUtils.d(Storage.settings)
|
|
||||||
if (Storage.settings.excludeFromRecents) {
|
if (Storage.settings.excludeFromRecents) {
|
||||||
finishAndRemoveTask() // 会让miui桌面回退动画失效
|
finishAndRemoveTask() // 会让miui桌面回退动画失效
|
||||||
} else {
|
} else {
|
||||||
fs()
|
fs()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onConfigurationChanged { newConfig ->
|
|
||||||
LogUtils.d(newConfig)
|
// https://juejin.cn/post/7169147194400833572
|
||||||
UseHook.update(newConfig)
|
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)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// delay(5000)
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
BackHandler {
|
val navController = rememberNavController()
|
||||||
finish()
|
AppTheme(false) {
|
||||||
}
|
CompositionLocalProvider(
|
||||||
CompositionLocalProvider(LocalLauncher provides launcher) {
|
LocalLauncher provides launcher,
|
||||||
MainTheme(false) {
|
LocalNavController provides navController
|
||||||
RouterHost(HomePage)
|
) {
|
||||||
|
StackCacheProvider(navController = navController) {
|
||||||
|
DestinationsNavHost(
|
||||||
|
navGraph = NavGraphs.root,
|
||||||
|
navController = navController,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
128
app/src/main/java/li/songe/gkd/accessibility/AbExt.kt
Normal file
128
app/src/main/java/li/songe/gkd/accessibility/AbExt.kt
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
package li.songe.gkd.accessibility
|
||||||
|
|
||||||
|
import android.accessibilityservice.AccessibilityService
|
||||||
|
import android.accessibilityservice.GestureDescription
|
||||||
|
import android.graphics.Path
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.view.accessibility.AccessibilityNodeInfo
|
||||||
|
import li.songe.selector.Transform
|
||||||
|
import li.songe.selector.Selector
|
||||||
|
|
||||||
|
fun AccessibilityNodeInfo.getIndex(): Int {
|
||||||
|
parent?.forEachIndexed { index, accessibilityNodeInfo ->
|
||||||
|
if (accessibilityNodeInfo == this) {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun AccessibilityNodeInfo.forEachIndexed(action: (index: Int, childNode: AccessibilityNodeInfo?) -> Unit) {
|
||||||
|
var index = 0
|
||||||
|
val childCount = this.childCount
|
||||||
|
while (index < childCount) {
|
||||||
|
val child: AccessibilityNodeInfo? = getChild(index)
|
||||||
|
action(index, child)
|
||||||
|
index += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun AccessibilityNodeInfo.click(service: AccessibilityService) = when {
|
||||||
|
this.isClickable -> {
|
||||||
|
this.performAction(AccessibilityNodeInfo.ACTION_CLICK)
|
||||||
|
"self"
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
val react = Rect()
|
||||||
|
this.getBoundsInScreen(react)
|
||||||
|
val x = react.left + 50f / 100f * (react.right - react.left)
|
||||||
|
val y = react.top + 50f / 100f * (react.bottom - react.top)
|
||||||
|
if (x >= 0 && y >= 0) {
|
||||||
|
val gestureDescription = GestureDescription.Builder()
|
||||||
|
val path = Path()
|
||||||
|
path.moveTo(x, y)
|
||||||
|
gestureDescription.addStroke(GestureDescription.StrokeDescription(path, 0, 300))
|
||||||
|
service.dispatchGesture(gestureDescription.build(), null, null)
|
||||||
|
"(50%, 50%)"
|
||||||
|
} else {
|
||||||
|
"($x, $y) no click"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun AccessibilityNodeInfo.getDepth(): Int {
|
||||||
|
var p: AccessibilityNodeInfo? = this
|
||||||
|
var depth = 0
|
||||||
|
while (true) {
|
||||||
|
val p2 = p?.parent
|
||||||
|
if (p2 != null) {
|
||||||
|
p = p2
|
||||||
|
depth++
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return depth
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun AccessibilityNodeInfo.querySelector(selector: Selector) =
|
||||||
|
abTransform.querySelector(this, selector)
|
||||||
|
|
||||||
|
fun AccessibilityNodeInfo.querySelectorAll(selector: Selector) =
|
||||||
|
abTransform.querySelectorAll(this, selector)
|
||||||
|
|
||||||
|
// 不可以在 多线程/不同协程作用域 里同时使用
|
||||||
|
private val tempRect = Rect()
|
||||||
|
private fun AccessibilityNodeInfo.getTempRect(): Rect {
|
||||||
|
getBoundsInScreen(tempRect)
|
||||||
|
return tempRect
|
||||||
|
}
|
||||||
|
|
||||||
|
val abTransform = Transform<AccessibilityNodeInfo>(
|
||||||
|
getAttr = { node, name ->
|
||||||
|
when (name) {
|
||||||
|
"id" -> node.viewIdResourceName
|
||||||
|
"name" -> node.className
|
||||||
|
"text" -> node.text
|
||||||
|
"textLen" -> node.text?.length
|
||||||
|
"desc" -> node.contentDescription
|
||||||
|
"descLen" -> node.contentDescription?.length
|
||||||
|
"childCount" -> node.childCount
|
||||||
|
|
||||||
|
"isEnabled" -> node.isEnabled
|
||||||
|
"isClickable" -> node.isClickable
|
||||||
|
"isChecked" -> node.isChecked
|
||||||
|
"isCheckable" -> node.isCheckable
|
||||||
|
"isFocused" -> node.isFocused
|
||||||
|
"isFocusable" -> node.isFocusable
|
||||||
|
"isVisibleToUser" -> node.isVisibleToUser
|
||||||
|
|
||||||
|
"left" -> node.getTempRect().left
|
||||||
|
"top" -> node.getTempRect().top
|
||||||
|
"right" -> node.getTempRect().right
|
||||||
|
"bottom" -> node.getTempRect().bottom
|
||||||
|
|
||||||
|
"width" -> node.getTempRect().width()
|
||||||
|
"height" -> node.getTempRect().height()
|
||||||
|
|
||||||
|
"index" -> node.getIndex()
|
||||||
|
"depth" -> node.getDepth()
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getName = { node -> node.className },
|
||||||
|
getChildren = { node ->
|
||||||
|
sequence {
|
||||||
|
repeat(node.childCount) { i ->
|
||||||
|
yield(node.getChild(i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getChild = { node, index -> node.getChild(index) },
|
||||||
|
getParent = { node -> node.parent }
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,31 +1,40 @@
|
||||||
package li.songe.gkd.accessibility
|
package li.songe.gkd.accessibility
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.os.Build
|
||||||
|
import android.view.Display
|
||||||
import android.view.accessibility.AccessibilityEvent
|
import android.view.accessibility.AccessibilityEvent
|
||||||
|
import android.view.accessibility.AccessibilityNodeInfo
|
||||||
import com.blankj.utilcode.util.LogUtils
|
import com.blankj.utilcode.util.LogUtils
|
||||||
import com.blankj.utilcode.util.NetworkUtils
|
import com.blankj.utilcode.util.NetworkUtils
|
||||||
import com.blankj.utilcode.util.ScreenUtils
|
import com.blankj.utilcode.util.ScreenUtils
|
||||||
import com.blankj.utilcode.util.ServiceUtils
|
import com.blankj.utilcode.util.ServiceUtils
|
||||||
|
import com.blankj.utilcode.util.ToastUtils
|
||||||
import io.ktor.client.request.get
|
import io.ktor.client.request.get
|
||||||
import io.ktor.client.statement.bodyAsText
|
import io.ktor.client.statement.bodyAsText
|
||||||
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.delay
|
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.CompositionAbService
|
||||||
import li.songe.gkd.composition.CompositionExt.useLifeCycleLog
|
import li.songe.gkd.composition.CompositionExt.useLifeCycleLog
|
||||||
import li.songe.gkd.composition.CompositionExt.useScope
|
import li.songe.gkd.composition.CompositionExt.useScope
|
||||||
|
import li.songe.gkd.data.NodeInfo
|
||||||
|
import li.songe.gkd.data.Rule
|
||||||
import li.songe.gkd.data.RuleManager
|
import li.songe.gkd.data.RuleManager
|
||||||
import li.songe.gkd.data.SubscriptionRaw
|
import li.songe.gkd.data.SubscriptionRaw
|
||||||
import li.songe.gkd.db.table.SubsItem
|
import li.songe.gkd.db.DbSet
|
||||||
import li.songe.gkd.db.util.RoomX
|
import li.songe.gkd.debug.SnapshotExt
|
||||||
import li.songe.gkd.debug.NodeSnapshot
|
import li.songe.gkd.shizuku.activityTaskManager
|
||||||
import li.songe.gkd.selector.click
|
import li.songe.gkd.shizuku.shizukuIsSafeOK
|
||||||
import li.songe.gkd.selector.querySelectorAll
|
import li.songe.gkd.utils.Singleton
|
||||||
import li.songe.gkd.util.Ext.buildRuleManager
|
import li.songe.gkd.utils.Storage
|
||||||
import li.songe.gkd.util.Ext.getActivityIdByShizuku
|
import li.songe.gkd.utils.launchTry
|
||||||
import li.songe.gkd.util.Ext.getSubsFileLastModified
|
import li.songe.gkd.utils.launchWhile
|
||||||
import li.songe.gkd.util.Ext.launchWhile
|
import li.songe.gkd.utils.launchWhileTry
|
||||||
import li.songe.gkd.util.Singleton
|
import kotlin.coroutines.resume
|
||||||
import li.songe.gkd.util.Storage
|
import kotlin.coroutines.suspendCoroutine
|
||||||
import li.songe.selector_core.Selector
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class GkdAbService : CompositionAbService({
|
class GkdAbService : CompositionAbService({
|
||||||
useLifeCycleLog()
|
useLifeCycleLog()
|
||||||
|
@ -35,7 +44,11 @@ class GkdAbService : CompositionAbService({
|
||||||
val scope = useScope()
|
val scope = useScope()
|
||||||
|
|
||||||
service = context
|
service = context
|
||||||
onDestroy { service = null }
|
onDestroy {
|
||||||
|
service = null
|
||||||
|
currentAppId = null
|
||||||
|
currentActivityId = null
|
||||||
|
}
|
||||||
|
|
||||||
KeepAliveService.start(context)
|
KeepAliveService.start(context)
|
||||||
onDestroy {
|
onDestroy {
|
||||||
|
@ -46,100 +59,98 @@ class GkdAbService : CompositionAbService({
|
||||||
onServiceConnected { serviceConnected = true }
|
onServiceConnected { serviceConnected = true }
|
||||||
onInterrupt { serviceConnected = false }
|
onInterrupt { serviceConnected = false }
|
||||||
|
|
||||||
onAccessibilityEvent { event ->
|
onAccessibilityEvent { event -> // 根据事件获取 activityId, 概率不准确
|
||||||
val activityId = event?.className?.toString() ?: return@onAccessibilityEvent
|
when (event?.eventType) {
|
||||||
val rootAppId = rootInActiveWindow?.packageName?.toString() ?: return@onAccessibilityEvent
|
|
||||||
when (event.eventType) {
|
|
||||||
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, AccessibilityEvent.TYPE_WINDOWS_CHANGED -> {
|
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, AccessibilityEvent.TYPE_WINDOWS_CHANGED -> {
|
||||||
// 在桌面和应用之间来回切换, 大概率导致识别失败
|
val activityId = event.className?.toString() ?: return@onAccessibilityEvent
|
||||||
if (!activityId.startsWith("android.") &&
|
if (activityId == "com.miui.home.launcher.Launcher") { // 小米桌面 bug
|
||||||
!activityId.startsWith("androidx.") &&
|
val appId =
|
||||||
!activityId.startsWith("com.android.")
|
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 == "com.miui.home.launcher.Launcher" && rootAppId != "com.miui.home")) {
|
return@onAccessibilityEvent
|
||||||
// 小米手机 上滑手势, 导致 活动名 不属于包名
|
|
||||||
// 另外 微信扫码登录第三方网站 也会导致失败
|
|
||||||
} else {
|
|
||||||
if (activityId != nodeSnapshot.activityId) {
|
|
||||||
nodeSnapshot = nodeSnapshot.copy(
|
|
||||||
activityId = activityId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
currentActivityId = activityId
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scope.launchWhile {
|
onAccessibilityEvent { event -> // 小米手机监听截屏保存快照
|
||||||
delay(300)
|
if (!Storage.settings.enableCaptureSystemScreenshot) return@onAccessibilityEvent
|
||||||
val activityId = getActivityIdByShizuku() ?: return@launchWhile
|
if (event?.packageName == null || event.className == null) return@onAccessibilityEvent
|
||||||
if (activityId != nodeSnapshot.activityId) {
|
if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED && event.contentChangeTypes == AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED && event.packageName.contentEquals(
|
||||||
nodeSnapshot = nodeSnapshot.copy(
|
"com.miui.screenshot"
|
||||||
activityId = activityId
|
) && event.className!!.startsWith("android.") // android.widget.RelativeLayout
|
||||||
)
|
) {
|
||||||
|
scope.launchTry {
|
||||||
|
val snapshot = SnapshotExt.captureSnapshot()
|
||||||
|
ToastUtils.showShort("保存快照成功")
|
||||||
|
LogUtils.d("截屏:保存快照", snapshot.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var subsFileLastModified = 0L
|
scope.launchWhile { // 屏幕无障碍信息轮询
|
||||||
scope.launchWhile { // 根据本地文件最新写入时间 决定 是否 更新数据
|
delay(200)
|
||||||
val t = getSubsFileLastModified()
|
|
||||||
if (t > subsFileLastModified) {
|
|
||||||
subsFileLastModified = t
|
|
||||||
ruleManager = buildRuleManager()
|
|
||||||
LogUtils.d("读取本地规则")
|
|
||||||
}
|
|
||||||
delay(10_000)
|
|
||||||
}
|
|
||||||
|
|
||||||
scope.launchWhile {
|
|
||||||
delay(50)
|
|
||||||
if (!serviceConnected) return@launchWhile
|
if (!serviceConnected) return@launchWhile
|
||||||
if (!Storage.settings.enableService || ScreenUtils.isScreenLock()) return@launchWhile
|
if (!Storage.settings.enableService || ScreenUtils.isScreenLock()) return@launchWhile
|
||||||
|
|
||||||
nodeSnapshot = nodeSnapshot.copy(
|
currentAppId = rootInActiveWindow?.packageName?.toString()
|
||||||
root = rootInActiveWindow,
|
var tempRules = rules
|
||||||
)
|
var i = 0
|
||||||
val shot = nodeSnapshot
|
while (i < tempRules.size) {
|
||||||
if (shot.root == null) return@launchWhile
|
val rule = tempRules[i]
|
||||||
for (rule in ruleManager.match(shot.appId, shot.activityId)) {
|
i++
|
||||||
val target = rule.query(shot.root) ?: continue
|
if (!ruleManager.ruleIsAvailable(rule)) continue
|
||||||
|
val frozenNode = rootInActiveWindow
|
||||||
|
val target = rule.query(frozenNode)
|
||||||
|
if (target != null) {
|
||||||
val clickResult = target.click(context)
|
val clickResult = target.click(context)
|
||||||
ruleManager.trigger(rule)
|
ruleManager.trigger(rule)
|
||||||
LogUtils.d(
|
LogUtils.d(
|
||||||
*rule.matches.toTypedArray(),
|
*rule.matches.toTypedArray(), NodeInfo.abNodeToNode(target), clickResult
|
||||||
NodeSnapshot.abNodeToNode(target),
|
|
||||||
clickResult
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
delay(150)
|
delay(50)
|
||||||
|
currentAppId = rootInActiveWindow?.packageName?.toString()
|
||||||
|
if (tempRules != rules) {
|
||||||
|
tempRules = rules
|
||||||
|
i = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scope.launchWhile {
|
scope.launchWhile { // 自动从网络更新订阅文件
|
||||||
delay(5000)
|
delay(5000)
|
||||||
RoomX.select<SubsItem>().map { subsItem ->
|
if (!NetworkUtils.isAvailable()) return@launchWhile
|
||||||
if (!NetworkUtils.isAvailable()) return@map
|
DbSet.subsItemDao.query().first().forEach { subsItem ->
|
||||||
try {
|
try {
|
||||||
val text = Singleton.client.get(subsItem.updateUrl).bodyAsText()
|
val text = Singleton.client.get(subsItem.updateUrl).bodyAsText()
|
||||||
val subscriptionRaw = SubscriptionRaw.parse5(text)
|
val subscriptionRaw = SubscriptionRaw.parse5(text)
|
||||||
if (subscriptionRaw.version <= subsItem.version) {
|
if (subscriptionRaw.version <= subsItem.version) {
|
||||||
return@map
|
return@forEach
|
||||||
}
|
}
|
||||||
val newItem = subsItem.copy(
|
val newItem = subsItem.copy(
|
||||||
updateUrl = subscriptionRaw.updateUrl
|
updateUrl = subscriptionRaw.updateUrl ?: subsItem.updateUrl,
|
||||||
?: subsItem.updateUrl,
|
|
||||||
name = subscriptionRaw.name,
|
name = subscriptionRaw.name,
|
||||||
mtime = System.currentTimeMillis()
|
mtime = System.currentTimeMillis()
|
||||||
)
|
)
|
||||||
RoomX.update(newItem)
|
newItem.subsFile.writeText(
|
||||||
File(newItem.filePath).writeText(
|
|
||||||
SubscriptionRaw.stringify(
|
SubscriptionRaw.stringify(
|
||||||
subscriptionRaw
|
subscriptionRaw
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
LogUtils.d("更新订阅文件:${subsItem.name}")
|
DbSet.subsItemDao.update(newItem)
|
||||||
|
LogUtils.d("更新磁盘订阅文件:${subsItem.name}")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
@ -147,29 +158,96 @@ class GkdAbService : CompositionAbService({
|
||||||
delay(30 * 60_000)
|
delay(30 * 60_000)
|
||||||
}
|
}
|
||||||
|
|
||||||
}) {
|
scope.launchTry {
|
||||||
private var nodeSnapshot = NodeSnapshot()
|
DbSet.subsItemDao.query().flowOn(IO).collect {
|
||||||
set(value) {
|
val subscriptionRawArray = withContext(IO) {
|
||||||
if (field.appId != value.appId || field.activityId != value.activityId) {
|
it.filter { s -> s.enable }
|
||||||
LogUtils.d(
|
.mapNotNull { s -> s.subscriptionRaw }
|
||||||
value.appId,
|
}
|
||||||
value.activityId,
|
ruleManager = RuleManager(*subscriptionRawArray.toTypedArray())
|
||||||
*ruleManager.match(value.appId, value.activityId).toList().toTypedArray()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
field = value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var ruleManager = RuleManager()
|
scope.launchWhileTry(interval = 400) {
|
||||||
|
if (shizukuIsSafeOK()) {
|
||||||
|
val topActivity =
|
||||||
|
activityTaskManager.getTasks(1, false, true)?.firstOrNull()?.topActivity
|
||||||
|
if (topActivity != null) {
|
||||||
|
currentAppId = topActivity.packageName
|
||||||
|
currentActivityId = topActivity.className
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private var service: GkdAbService? = null
|
||||||
fun isRunning() = ServiceUtils.isServiceRunning(GkdAbService::class.java)
|
fun isRunning() = ServiceUtils.isServiceRunning(GkdAbService::class.java)
|
||||||
fun currentNodeSnapshot() = service?.nodeSnapshot
|
|
||||||
fun match(selector: String) {
|
private var ruleManager = RuleManager()
|
||||||
val rootAbNode = service?.rootInActiveWindow ?: return
|
set(value) {
|
||||||
val list = rootAbNode.querySelectorAll(Selector.parse(selector)).map { it.value }.toList()
|
field = value
|
||||||
|
rules = value.match(currentAppId, currentActivityId).toList()
|
||||||
|
}
|
||||||
|
private var rules = listOf<Rule>()
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
LogUtils.d(
|
||||||
|
"currentAppId: $currentAppId",
|
||||||
|
"currentActivityId: $currentActivityId",
|
||||||
|
*value.toTypedArray()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var service: GkdAbService? = null
|
var currentActivityId: String? = null
|
||||||
|
set(value) {
|
||||||
|
val oldValue = field
|
||||||
|
field = value
|
||||||
|
if (value != oldValue) {
|
||||||
|
rules = ruleManager.match(currentAppId, value).toList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private var currentAppId: String? = null
|
||||||
|
set(value) {
|
||||||
|
val oldValue = field
|
||||||
|
field = value
|
||||||
|
if (value != oldValue) {
|
||||||
|
rules = ruleManager.match(value, currentActivityId).toList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val currentAbNode: AccessibilityNodeInfo?
|
||||||
|
get() {
|
||||||
|
return service?.rootInActiveWindow
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun currentScreenshot() = service?.run {
|
||||||
|
suspendCoroutine {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
takeScreenshot(Display.DEFAULT_DISPLAY,
|
||||||
|
application.mainExecutor,
|
||||||
|
object : TakeScreenshotCallback {
|
||||||
|
override fun onSuccess(screenshot: ScreenshotResult) {
|
||||||
|
it.resume(
|
||||||
|
Bitmap.wrapHardwareBuffer(
|
||||||
|
screenshot.hardwareBuffer, screenshot.colorSpace
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(errorCode: Int) = it.resume(null)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
it.resume(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fun match(selector: String) {
|
||||||
|
// val rootAbNode = service?.rootInActiveWindow ?: return
|
||||||
|
// val list =
|
||||||
|
// rootAbNode.querySelectorAll(Selector.parse(selector)).map { it.value }.toList()
|
||||||
|
// }
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -6,9 +6,8 @@ import kotlinx.coroutines.delay
|
||||||
import li.songe.gkd.App
|
import li.songe.gkd.App
|
||||||
import li.songe.gkd.composition.CompositionService
|
import li.songe.gkd.composition.CompositionService
|
||||||
import li.songe.gkd.composition.CompositionExt.useScope
|
import li.songe.gkd.composition.CompositionExt.useScope
|
||||||
import li.songe.gkd.util.Ext.createNotificationChannel
|
import li.songe.gkd.utils.launchWhile
|
||||||
import li.songe.gkd.util.Ext.launchWhile
|
import li.songe.gkd.utils.Ext.createNotificationChannel
|
||||||
|
|
||||||
|
|
||||||
class KeepAliveService : CompositionService({
|
class KeepAliveService : CompositionService({
|
||||||
createNotificationChannel(this)
|
createNotificationChannel(this)
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
package li.songe.gkd.accessibility
|
|
||||||
|
|
||||||
import android.view.accessibility.AccessibilityNodeInfo
|
|
||||||
|
|
||||||
data class NodeSnapshot(
|
|
||||||
val root: AccessibilityNodeInfo? = null,
|
|
||||||
val activityId: String? = null,
|
|
||||||
) {
|
|
||||||
val appId by lazy { root?.packageName?.toString() }
|
|
||||||
}
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
package li.songe.gkd.accessibility
|
||||||
|
|
||||||
|
import li.songe.gkd.composition.CompositionService
|
||||||
|
|
||||||
|
class ShizukuService: CompositionService({
|
||||||
|
|
||||||
|
})
|
|
@ -6,14 +6,13 @@ import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
|
||||||
import com.blankj.utilcode.util.LogUtils
|
import com.blankj.utilcode.util.LogUtils
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import li.songe.gkd.util.Singleton
|
import li.songe.gkd.utils.Singleton
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
object CompositionExt {
|
object CompositionExt {
|
||||||
|
@ -40,15 +39,14 @@ object CompositionExt {
|
||||||
}
|
}
|
||||||
val filter = IntentFilter(packageName)
|
val filter = IntentFilter(packageName)
|
||||||
|
|
||||||
val broadcastManager = LocalBroadcastManager.getInstance(this)
|
registerReceiver(receiver, filter)
|
||||||
broadcastManager.registerReceiver(receiver, filter)
|
|
||||||
val sendMessage: (InvokeMessage) -> Unit = { message ->
|
val sendMessage: (InvokeMessage) -> Unit = { message ->
|
||||||
broadcastManager.sendBroadcast(Intent(packageName).apply {
|
sendBroadcast(Intent(packageName).apply {
|
||||||
putExtra("__invoke", Singleton.json.encodeToString(message))
|
putExtra("__invoke", Singleton.json.encodeToString(message))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
onDestroy {
|
onDestroy {
|
||||||
broadcastManager.unregisterReceiver(receiver)
|
unregisterReceiver(receiver)
|
||||||
}
|
}
|
||||||
val setter: ((InvokeMessage) -> Unit) -> Unit = { onMessage = it }
|
val setter: ((InvokeMessage) -> Unit) -> Unit = { onMessage = it }
|
||||||
return (setter to sendMessage)
|
return (setter to sendMessage)
|
||||||
|
|
35
app/src/main/java/li/songe/gkd/data/AppInfo.kt
Normal file
35
app/src/main/java/li/songe/gkd/data/AppInfo.kt
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
data class AppInfo(
|
||||||
|
val id: String,
|
||||||
|
val name: String? = null,
|
||||||
|
val icon: Drawable? = null,
|
||||||
|
val installed: Boolean = true
|
||||||
|
)
|
||||||
|
|
||||||
|
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(
|
||||||
|
id, PackageManager.GET_META_DATA
|
||||||
|
)
|
||||||
|
AppInfo(
|
||||||
|
id = id,
|
||||||
|
name = packageManager.getApplicationLabel(rawInfo).toString(),
|
||||||
|
icon = packageManager.getApplicationIcon(rawInfo),
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
return AppInfo(id = id, installed = false)
|
||||||
|
}
|
||||||
|
appInfoCache[id] = info
|
||||||
|
return info
|
||||||
|
}
|
|
@ -1,13 +1,13 @@
|
||||||
package li.songe.gkd.debug
|
package li.songe.gkd.data
|
||||||
|
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.view.accessibility.AccessibilityNodeInfo
|
import android.view.accessibility.AccessibilityNodeInfo
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import li.songe.gkd.selector.getDepth
|
import li.songe.gkd.accessibility.getDepth
|
||||||
import li.songe.gkd.selector.getIndex
|
import li.songe.gkd.accessibility.getIndex
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class AttrSnapshot(
|
data class AttrInfo(
|
||||||
val id: String? = null,
|
val id: String? = null,
|
||||||
val name: String? = null,
|
val name: String? = null,
|
||||||
val text: String? = null,
|
val text: String? = null,
|
||||||
|
@ -30,9 +30,9 @@ data class AttrSnapshot(
|
||||||
private val rect = Rect()
|
private val rect = Rect()
|
||||||
fun info2data(
|
fun info2data(
|
||||||
nodeInfo: AccessibilityNodeInfo,
|
nodeInfo: AccessibilityNodeInfo,
|
||||||
): AttrSnapshot {
|
): AttrInfo {
|
||||||
nodeInfo.getBoundsInScreen(rect)
|
nodeInfo.getBoundsInScreen(rect)
|
||||||
return AttrSnapshot(
|
return AttrInfo(
|
||||||
id = nodeInfo.viewIdResourceName,
|
id = nodeInfo.viewIdResourceName,
|
||||||
name = nodeInfo.className?.toString(),
|
name = nodeInfo.className?.toString(),
|
||||||
text = nodeInfo.text?.toString(),
|
text = nodeInfo.text?.toString(),
|
|
@ -1,25 +1,18 @@
|
||||||
package li.songe.gkd.debug
|
package li.songe.gkd.data
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class DeviceSnapshot(
|
data class DeviceInfo(
|
||||||
@SerialName("device")
|
|
||||||
val device: String = Build.DEVICE,
|
val device: String = Build.DEVICE,
|
||||||
@SerialName("model")
|
|
||||||
val model: String = Build.MODEL,
|
val model: String = Build.MODEL,
|
||||||
@SerialName("manufacturer")
|
|
||||||
val manufacturer: String = Build.MANUFACTURER,
|
val manufacturer: String = Build.MANUFACTURER,
|
||||||
@SerialName("brand")
|
|
||||||
val brand: String = Build.BRAND,
|
val brand: String = Build.BRAND,
|
||||||
@SerialName("sdkInt")
|
|
||||||
val sdkInt: Int = Build.VERSION.SDK_INT,
|
val sdkInt: Int = Build.VERSION.SDK_INT,
|
||||||
@SerialName("release")
|
|
||||||
val release: String = Build.VERSION.RELEASE,
|
val release: String = Build.VERSION.RELEASE,
|
||||||
){
|
){
|
||||||
companion object{
|
companion object{
|
||||||
val instance by lazy { DeviceSnapshot() }
|
val instance by lazy { DeviceInfo() }
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,24 +1,17 @@
|
||||||
package li.songe.gkd.debug
|
package li.songe.gkd.data
|
||||||
|
|
||||||
import android.view.accessibility.AccessibilityNodeInfo
|
import android.view.accessibility.AccessibilityNodeInfo
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import li.songe.gkd.selector.forEachIndexed
|
import li.songe.gkd.accessibility.forEachIndexed
|
||||||
import java.util.ArrayDeque
|
import java.util.ArrayDeque
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* api/node 返回列表
|
|
||||||
*/
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class NodeSnapshot(
|
data class NodeInfo(
|
||||||
val id: Int,
|
val id: Int, val pid: Int, val index: Int,
|
||||||
val pid: Int,
|
|
||||||
val index: Int,
|
|
||||||
/**
|
/**
|
||||||
* null: when getChild(i) return null
|
* null: when getChild(i) return null
|
||||||
*/
|
*/
|
||||||
val attr: AttrSnapshot?
|
val attr: AttrInfo?
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
fun abNodeToNode(
|
fun abNodeToNode(
|
||||||
|
@ -26,22 +19,17 @@ data class NodeSnapshot(
|
||||||
id: Int = 0,
|
id: Int = 0,
|
||||||
pid: Int = -1,
|
pid: Int = -1,
|
||||||
index: Int = 0,
|
index: Int = 0,
|
||||||
): NodeSnapshot {
|
): NodeInfo {
|
||||||
return NodeSnapshot(
|
return NodeInfo(id, pid, index, nodeInfo?.let { AttrInfo.info2data(nodeInfo) })
|
||||||
id,
|
|
||||||
pid,
|
|
||||||
index,
|
|
||||||
nodeInfo?.let { AttrSnapshot.info2data(nodeInfo) }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun info2nodeList(nodeInfo: AccessibilityNodeInfo?): List<NodeSnapshot> {
|
fun info2nodeList(nodeInfo: AccessibilityNodeInfo?): List<NodeInfo> {
|
||||||
if (nodeInfo == null) {
|
if (nodeInfo == null) {
|
||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
val stack = ArrayDeque<Pair<Int, AccessibilityNodeInfo?>>()
|
val stack = ArrayDeque<Pair<Int, AccessibilityNodeInfo?>>()
|
||||||
stack.push(0 to nodeInfo)
|
stack.push(0 to nodeInfo)
|
||||||
val list = mutableListOf<NodeSnapshot>()
|
val list = mutableListOf<NodeInfo>()
|
||||||
list.add(abNodeToNode(nodeInfo, index = 0))
|
list.add(abNodeToNode(nodeInfo, index = 0))
|
||||||
while (stack.isNotEmpty()) {
|
while (stack.isNotEmpty()) {
|
||||||
val top = stack.pop()
|
val top = stack.pop()
|
|
@ -1,4 +1,4 @@
|
||||||
package li.songe.gkd.debug
|
package li.songe.gkd.data
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable
|
||||||
data class RpcError(
|
data class RpcError(
|
||||||
override val message: String = "unknown error",
|
override val message: String = "unknown error",
|
||||||
val code: Int = 0,
|
val code: Int = 0,
|
||||||
val X_Rpc_Result: Boolean = true
|
val X_Rpc_Result:String = "error"
|
||||||
) : Exception(message) {
|
) : Exception(message) {
|
||||||
companion object {
|
companion object {
|
||||||
const val HeaderKey = "X_Rpc_Result"
|
const val HeaderKey = "X_Rpc_Result"
|
|
@ -1,8 +1,8 @@
|
||||||
package li.songe.gkd.data
|
package li.songe.gkd.data
|
||||||
|
|
||||||
import android.view.accessibility.AccessibilityNodeInfo
|
import android.view.accessibility.AccessibilityNodeInfo
|
||||||
import li.songe.gkd.selector.querySelector
|
import li.songe.gkd.accessibility.querySelector
|
||||||
import li.songe.selector_core.Selector
|
import li.songe.selector.Selector
|
||||||
|
|
||||||
data class Rule(
|
data class Rule(
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package li.songe.gkd.data
|
package li.songe.gkd.data
|
||||||
|
|
||||||
import li.songe.selector_core.Selector
|
import li.songe.selector.Selector
|
||||||
|
|
||||||
class RuleManager(vararg subscriptionRawArray: SubscriptionRaw) {
|
class RuleManager(vararg subscriptionRawArray: SubscriptionRaw) {
|
||||||
|
|
||||||
|
@ -88,24 +88,31 @@ class RuleManager(vararg subscriptionRawArray: SubscriptionRaw) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun match(appId: String? = null, activityId: String? = null) = sequence {
|
fun match(appId: String? = null, activityId: String? = null) = sequence {
|
||||||
if (appId == null) return@sequence
|
if (appId == null) return@sequence
|
||||||
val rules = appToRulesMap[appId] ?: return@sequence
|
val rules = appToRulesMap[appId] ?: return@sequence
|
||||||
rules.forEach { rule ->
|
if (activityId == null) {
|
||||||
if (!rule.active) return@forEach // 处于冷却时间
|
yieldAll(rules)
|
||||||
|
return@sequence
|
||||||
if (rule.excludeActivityIds.contains(activityId)) return@forEach // 是被排除的 界面 id
|
|
||||||
|
|
||||||
if (rule.preRules.isNotEmpty()) { // 需要提前触发某个规则
|
|
||||||
val record = triggerLogQueue.lastOrNull() ?: return@forEach
|
|
||||||
if (!rule.preRules.any { it == record.rule }) return@forEach // 上一个触发的规则不在当前需要触发的列表
|
|
||||||
}
|
}
|
||||||
|
rules.forEach { rule ->
|
||||||
|
if (rule.excludeActivityIds.any { activityId.startsWith(it) }) return@forEach // 是被排除的 界面 id
|
||||||
|
|
||||||
if (activityId == null || rule.matchAnyActivity // 全匹配
|
if (rule.matchAnyActivity || rule.activityIds.any { activityId.startsWith(it) } // 在匹配列表
|
||||||
|| rule.activityIds.contains(activityId) // 在匹配列表
|
|
||||||
) {
|
) {
|
||||||
yield(rule)
|
yield(rule)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun ruleIsAvailable(rule: Rule): Boolean {
|
||||||
|
if (!rule.active) return false // 处于冷却时间
|
||||||
|
if (rule.preKeys.isNotEmpty()) { // 需要提前触发某个规则
|
||||||
|
if (rule.preRules.isEmpty()) return false // 声明了 preKeys 但是没有在当前列表找到
|
||||||
|
val record = triggerLogQueue.lastOrNull() ?: return false
|
||||||
|
if (!rule.preRules.any { it == record.rule }) return false // 上一个触发的规则不在当前需要触发的列表
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
82
app/src/main/java/li/songe/gkd/data/Snapshot.kt
Normal file
82
app/src/main/java/li/songe/gkd/data/Snapshot.kt
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
package li.songe.gkd.data
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.TypeConverters
|
||||||
|
import androidx.room.Update
|
||||||
|
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.db.IgnoreConverters
|
||||||
|
import li.songe.gkd.utils.Ext
|
||||||
|
|
||||||
|
@TypeConverters(IgnoreConverters::class)
|
||||||
|
@Entity(
|
||||||
|
tableName = "snapshot",
|
||||||
|
)
|
||||||
|
@Serializable
|
||||||
|
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_version_code") val appVersionCode: Int? = appId?.let {
|
||||||
|
AppUtils.getAppVersionCode(
|
||||||
|
appId
|
||||||
|
)
|
||||||
|
},
|
||||||
|
@ColumnInfo(name = "app_version_name") val appVersionName: String? = appId?.let {
|
||||||
|
AppUtils.getAppVersionName(
|
||||||
|
appId
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
@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,
|
||||||
|
@ColumnInfo(name = "model") val model: String = DeviceInfo.instance.model,
|
||||||
|
@ColumnInfo(name = "manufacturer") val manufacturer: String = DeviceInfo.instance.manufacturer,
|
||||||
|
@ColumnInfo(name = "brand") val brand: String = DeviceInfo.instance.brand,
|
||||||
|
@ColumnInfo(name = "sdk_int") val sdkInt: Int = DeviceInfo.instance.sdkInt,
|
||||||
|
@ColumnInfo(name = "release") val release: String = DeviceInfo.instance.release,
|
||||||
|
|
||||||
|
@ColumnInfo(name = "_1") val nodes: List<NodeInfo> = emptyList(),
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun current(includeNode: Boolean = true): Snapshot {
|
||||||
|
val currentAbNode = GkdAbService.currentAbNode
|
||||||
|
val appId = currentAbNode?.packageName?.toString()
|
||||||
|
val currentActivityId = GkdAbService.currentActivityId
|
||||||
|
return Snapshot(
|
||||||
|
appId = appId,
|
||||||
|
activityId = currentActivityId,
|
||||||
|
nodes = if (includeNode) NodeInfo.info2nodeList(currentAbNode) else emptyList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
@TypeConverters(IgnoreConverters::class)
|
||||||
|
interface SnapshotDao {
|
||||||
|
@Update
|
||||||
|
suspend fun update(vararg objects: Snapshot): Int
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
suspend fun insert(vararg users: Snapshot): List<Long>
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
suspend fun delete(vararg users: Snapshot): Int
|
||||||
|
|
||||||
|
@Query("SELECT * FROM snapshot")
|
||||||
|
fun query(): Flow<List<Snapshot>>
|
||||||
|
}
|
||||||
|
}
|
64
app/src/main/java/li/songe/gkd/data/SubsConfig.kt
Normal file
64
app/src/main/java/li/songe/gkd/data/SubsConfig.kt
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
package li.songe.gkd.data
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Update
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "subs_config",
|
||||||
|
)
|
||||||
|
@Parcelize
|
||||||
|
data class SubsConfig(
|
||||||
|
@PrimaryKey @ColumnInfo(name = "id") val id: Long = System.currentTimeMillis(),
|
||||||
|
@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 = "app_id") val appId: String = "",
|
||||||
|
@ColumnInfo(name = "group_key") val groupKey: Int = -1,
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val SubsType = 0
|
||||||
|
const val AppType = 1
|
||||||
|
const val GroupType = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface SubsConfigDao {
|
||||||
|
|
||||||
|
@Update
|
||||||
|
suspend fun update(vararg objects: SubsConfig): Int
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insert(vararg users: SubsConfig): List<Long>
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
suspend fun delete(vararg users: SubsConfig): Int
|
||||||
|
|
||||||
|
@Query("DELETE FROM subs_config WHERE subs_item_id=:subsItemId")
|
||||||
|
suspend fun deleteSubs(subsItemId: Long): Int
|
||||||
|
|
||||||
|
@Query("SELECT * FROM subs_config")
|
||||||
|
fun query(): Flow<List<SubsConfig>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM subs_config WHERE type=${SubsType}")
|
||||||
|
fun querySubsTypeConfig(): Flow<List<SubsConfig>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM subs_config WHERE type=${AppType} and subs_item_id=:subsItemId")
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
79
app/src/main/java/li/songe/gkd/data/SubsItem.kt
Normal file
79
app/src/main/java/li/songe/gkd/data/SubsItem.kt
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
package li.songe.gkd.data
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Update
|
||||||
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
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 java.io.File
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "subs_item",
|
||||||
|
)
|
||||||
|
@Parcelize
|
||||||
|
data class SubsItem(
|
||||||
|
@PrimaryKey @ColumnInfo(name = "id") val id: Long = System.currentTimeMillis(),
|
||||||
|
@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 = "name") val name: String = "",
|
||||||
|
@ColumnInfo(name = "author") val author: String = "",
|
||||||
|
@ColumnInfo(name = "version") val version: Int = 0,
|
||||||
|
@ColumnInfo(name = "update_url") val updateUrl: String = "",
|
||||||
|
@ColumnInfo(name = "support_url") val supportUrl: String = "",
|
||||||
|
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
@IgnoredOnParcel
|
||||||
|
val subsFile by lazy {
|
||||||
|
File(FolderExt.subsFolder.absolutePath.plus("/${id}.json"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@IgnoredOnParcel
|
||||||
|
val subscriptionRaw by lazy {
|
||||||
|
try {
|
||||||
|
SubscriptionRaw.parse5(subsFile.readText())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun removeAssets() {
|
||||||
|
DbSet.subsItemDao.delete(this)
|
||||||
|
withContext(IO) {
|
||||||
|
subsFile.exists() && subsFile.delete()
|
||||||
|
}
|
||||||
|
DbSet.subsConfigDao.deleteSubs(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface SubsItemDao {
|
||||||
|
@Update
|
||||||
|
suspend fun update(vararg objects: SubsItem): Int
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
suspend fun insert(vararg users: SubsItem): List<Long>
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
suspend fun delete(vararg users: SubsItem): Int
|
||||||
|
|
||||||
|
@Query("SELECT * FROM subs_item ORDER BY `order`")
|
||||||
|
fun query(): Flow<List<SubsItem>>
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,8 +6,8 @@ import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.json.*
|
||||||
import li.songe.gkd.util.Singleton
|
import li.songe.gkd.utils.Singleton
|
||||||
import li.songe.selector_core.Selector
|
import li.songe.selector.Selector
|
||||||
|
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
@ -35,6 +35,7 @@ data class SubscriptionRaw(
|
||||||
@Serializable
|
@Serializable
|
||||||
data class GroupRaw(
|
data class GroupRaw(
|
||||||
@SerialName("name") val name: String? = null,
|
@SerialName("name") val name: String? = null,
|
||||||
|
@SerialName("desc") val desc: String? = null,
|
||||||
@SerialName("key") val key: Int? = null,
|
@SerialName("key") val key: Int? = null,
|
||||||
@SerialName("cd") val cd: Long? = null,
|
@SerialName("cd") val cd: Long? = null,
|
||||||
@SerialName("activityIds") val activityIds: List<String>? = null,
|
@SerialName("activityIds") val activityIds: List<String>? = null,
|
||||||
|
@ -78,13 +79,13 @@ data class SubscriptionRaw(
|
||||||
JsonNull, null -> null
|
JsonNull, null -> null
|
||||||
is JsonArray -> element.map {
|
is JsonArray -> element.map {
|
||||||
when (it) {
|
when (it) {
|
||||||
is JsonObject, is JsonArray, JsonNull -> error("Element ${this::class} is not a int")
|
is JsonObject, is JsonArray, JsonNull -> error("Element $it is not a int")
|
||||||
is JsonPrimitive -> it.int
|
is JsonPrimitive -> it.int
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is JsonPrimitive -> listOf(element.int)
|
is JsonPrimitive -> listOf(element.int)
|
||||||
else -> error("")
|
else -> error("Element $element is not a Array")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,11 +96,11 @@ data class SubscriptionRaw(
|
||||||
if (p.isString) {
|
if (p.isString) {
|
||||||
p.content
|
p.content
|
||||||
} else {
|
} else {
|
||||||
error("")
|
error("Element $p is not a string")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> error("")
|
else -> error("Element $p is not a string")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("SameParameterValue")
|
@Suppress("SameParameterValue")
|
||||||
|
@ -110,7 +111,7 @@ data class SubscriptionRaw(
|
||||||
p.long
|
p.long
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> error("")
|
else -> error("Element $p is not a long")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getInt(json: JsonObject? = null, key: String = ""): Int? =
|
private fun getInt(json: JsonObject? = null, key: String = ""): Int? =
|
||||||
|
@ -120,7 +121,7 @@ data class SubscriptionRaw(
|
||||||
p.int
|
p.int
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> error("")
|
else -> error("Element $p is not a int")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun jsonToRuleRaw(rulesRawJson: JsonElement): RuleRaw {
|
private fun jsonToRuleRaw(rulesRawJson: JsonElement): RuleRaw {
|
||||||
|
@ -134,12 +135,10 @@ data class SubscriptionRaw(
|
||||||
excludeActivityIds = getStringIArray(rulesJson, "excludeActivityIds"),
|
excludeActivityIds = getStringIArray(rulesJson, "excludeActivityIds"),
|
||||||
cd = getLong(rulesJson, "cd"),
|
cd = getLong(rulesJson, "cd"),
|
||||||
matches = (getStringIArray(
|
matches = (getStringIArray(
|
||||||
rulesJson,
|
rulesJson, "matches"
|
||||||
"matches"
|
|
||||||
) ?: emptyList()).onEach { Selector.parse(it) },
|
) ?: emptyList()).onEach { Selector.parse(it) },
|
||||||
excludeMatches = (getStringIArray(
|
excludeMatches = (getStringIArray(
|
||||||
rulesJson,
|
rulesJson, "excludeMatches"
|
||||||
"excludeMatches"
|
|
||||||
) ?: emptyList()).onEach { Selector.parse(it) },
|
) ?: emptyList()).onEach { Selector.parse(it) },
|
||||||
key = getInt(rulesJson, "key"),
|
key = getInt(rulesJson, "key"),
|
||||||
name = getString(rulesJson, "name"),
|
name = getString(rulesJson, "name"),
|
||||||
|
@ -154,11 +153,11 @@ data class SubscriptionRaw(
|
||||||
is JsonObject -> groupsRawJson
|
is JsonObject -> groupsRawJson
|
||||||
is JsonPrimitive, is JsonArray -> JsonObject(mapOf("rules" to groupsRawJson))
|
is JsonPrimitive, is JsonArray -> JsonObject(mapOf("rules" to groupsRawJson))
|
||||||
}
|
}
|
||||||
return GroupRaw(
|
return GroupRaw(activityIds = getStringIArray(groupsJson, "activityIds"),
|
||||||
activityIds = getStringIArray(groupsJson, "activityIds"),
|
|
||||||
excludeActivityIds = getStringIArray(groupsJson, "excludeActivityIds"),
|
excludeActivityIds = getStringIArray(groupsJson, "excludeActivityIds"),
|
||||||
cd = getLong(groupsJson, "cd"),
|
cd = getLong(groupsJson, "cd"),
|
||||||
name = getString(groupsJson, "name"),
|
name = getString(groupsJson, "name"),
|
||||||
|
desc = getString(groupsJson, "desc"),
|
||||||
key = getInt(groupsJson, "key"),
|
key = getInt(groupsJson, "key"),
|
||||||
rules = when (val rulesJson = groupsJson["rules"]) {
|
rules = when (val rulesJson = groupsJson["rules"]) {
|
||||||
null, JsonNull -> emptyList()
|
null, JsonNull -> emptyList()
|
||||||
|
@ -166,13 +165,11 @@ data class SubscriptionRaw(
|
||||||
is JsonArray -> rulesJson
|
is JsonArray -> rulesJson
|
||||||
}.map {
|
}.map {
|
||||||
jsonToRuleRaw(it)
|
jsonToRuleRaw(it)
|
||||||
}
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun jsonToAppRaw(appsJson: JsonObject): AppRaw {
|
private fun jsonToAppRaw(appsJson: JsonObject): AppRaw {
|
||||||
return AppRaw(
|
return AppRaw(activityIds = getStringIArray(appsJson, "activityIds"),
|
||||||
activityIds = getStringIArray(appsJson, "activityIds"),
|
|
||||||
excludeActivityIds = getStringIArray(appsJson, "excludeActivityIds"),
|
excludeActivityIds = getStringIArray(appsJson, "excludeActivityIds"),
|
||||||
cd = getLong(appsJson, "cd"),
|
cd = getLong(appsJson, "cd"),
|
||||||
id = getString(appsJson, "id") ?: error(""),
|
id = getString(appsJson, "id") ?: error(""),
|
||||||
|
@ -182,25 +179,22 @@ data class SubscriptionRaw(
|
||||||
is JsonArray -> groupsJson
|
is JsonArray -> groupsJson
|
||||||
}).map {
|
}).map {
|
||||||
jsonToGroupRaw(it)
|
jsonToGroupRaw(it)
|
||||||
}
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun jsonToSubscriptionRaw(rootJson: JsonObject): SubscriptionRaw {
|
private fun jsonToSubscriptionRaw(rootJson: JsonObject): SubscriptionRaw {
|
||||||
return SubscriptionRaw(
|
return SubscriptionRaw(name = getString(rootJson, "name") ?: error(""),
|
||||||
name = getString(rootJson, "name") ?: error(""),
|
|
||||||
version = getInt(rootJson, "version") ?: error(""),
|
version = getInt(rootJson, "version") ?: error(""),
|
||||||
author = getString(rootJson, "author"),
|
author = getString(rootJson, "author"),
|
||||||
updateUrl = getString(rootJson, "updateUrl"),
|
updateUrl = getString(rootJson, "updateUrl"),
|
||||||
supportUrl = getString(rootJson, "supportUrl"),
|
supportUrl = getString(rootJson, "supportUrl"),
|
||||||
apps = rootJson["apps"]?.jsonArray?.map { jsonToAppRaw(it.jsonObject) }
|
apps = rootJson["apps"]?.jsonArray?.map { jsonToAppRaw(it.jsonObject) }
|
||||||
?: emptyList()
|
?: emptyList())
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stringify(source: SubscriptionRaw) = Singleton.json.encodeToString(source)
|
fun stringify(source: SubscriptionRaw) = Singleton.json.encodeToString(source)
|
||||||
|
|
||||||
fun parse(source: String): SubscriptionRaw {
|
private fun parse(source: String): SubscriptionRaw {
|
||||||
return jsonToSubscriptionRaw(Singleton.json.parseToJsonElement(source).jsonObject)
|
return jsonToSubscriptionRaw(Singleton.json.parseToJsonElement(source).jsonObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
54
app/src/main/java/li/songe/gkd/data/TriggerLog.kt
Normal file
54
app/src/main/java/li/songe/gkd/data/TriggerLog.kt
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
package li.songe.gkd.data
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Update
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
@ColumnInfo(name = "subs_id") val subsId: Long,
|
||||||
|
/**
|
||||||
|
* 触发的组 id
|
||||||
|
*/
|
||||||
|
@ColumnInfo(name = "group_key") val groupKey: Int,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触发的选择器
|
||||||
|
*/
|
||||||
|
@ColumnInfo(name = "match") val match: String,
|
||||||
|
|
||||||
|
) : Parcelable {
|
||||||
|
@Dao
|
||||||
|
interface TriggerLogDao {
|
||||||
|
|
||||||
|
@Update
|
||||||
|
suspend fun update(vararg objects: TriggerLog): Int
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
suspend fun insert(vararg users: TriggerLog): List<Long>
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
suspend fun delete(vararg users: TriggerLog): Int
|
||||||
|
|
||||||
|
@Query("SELECT * FROM trigger_log")
|
||||||
|
suspend fun query(): List<TriggerLog>
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,42 +0,0 @@
|
||||||
package li.songe.gkd.db
|
|
||||||
|
|
||||||
import androidx.room.Database
|
|
||||||
import androidx.room.Room
|
|
||||||
import androidx.room.RoomDatabase
|
|
||||||
import com.blankj.utilcode.util.PathUtils
|
|
||||||
import li.songe.gkd.App
|
|
||||||
import li.songe.gkd.db.table.SubsConfig
|
|
||||||
import li.songe.gkd.db.table.SubsItem
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
@Database(
|
|
||||||
version = 1,
|
|
||||||
entities = [SubsItem::class, SubsConfig::class],
|
|
||||||
autoMigrations = [
|
|
||||||
// AutoMigration(from = 1, to = 2),
|
|
||||||
// AutoMigration(from = 2, to = 3),
|
|
||||||
],
|
|
||||||
// 自动迁移 https://developer.android.com/training/data-storage/room/migrating-db-versions#automated
|
|
||||||
)
|
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
|
||||||
abstract fun subsItemRoomDao(): SubsItem.RoomDao
|
|
||||||
abstract fun subsConfigRoomDao(): SubsConfig.RoomDao
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val db by lazy {
|
|
||||||
File(PathUtils.getExternalAppFilesPath().plus("/db/")).apply {
|
|
||||||
if (!exists()) {
|
|
||||||
mkdir()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val name = PathUtils.getExternalAppFilesPath().plus("/db/database.db")
|
|
||||||
Room.databaseBuilder(
|
|
||||||
App.context,
|
|
||||||
AppDatabase::class.java,
|
|
||||||
name
|
|
||||||
)
|
|
||||||
.fallbackToDestructiveMigration()
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
package li.songe.gkd.db
|
|
||||||
|
|
||||||
import androidx.room.Delete
|
|
||||||
import androidx.room.Insert
|
|
||||||
import androidx.room.RawQuery
|
|
||||||
import androidx.room.Update
|
|
||||||
import androidx.sqlite.db.SupportSQLiteQuery
|
|
||||||
|
|
||||||
interface BaseDao<T : Any> {
|
|
||||||
@Insert
|
|
||||||
suspend fun insert(vararg objects: T): List<Long>
|
|
||||||
|
|
||||||
@Delete
|
|
||||||
suspend fun delete(vararg objects: T): Int
|
|
||||||
|
|
||||||
@Update
|
|
||||||
suspend fun update(vararg objects: T): Int
|
|
||||||
|
|
||||||
@RawQuery
|
|
||||||
suspend fun query(sqLiteQuery: SupportSQLiteQuery): List<T>
|
|
||||||
|
|
||||||
@RawQuery
|
|
||||||
suspend fun delete(sqLiteQuery: SupportSQLiteQuery): List<Int>
|
|
||||||
|
|
||||||
// https://developer.android.com/training/data-storage/room/async-queries#kotlin
|
|
||||||
// you must set observedEntities in sub interface
|
|
||||||
// @RawQuery
|
|
||||||
// fun queryFlow(sqLiteQuery: SupportSQLiteQuery): Flow<List<T>>
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
package li.songe.gkd.db
|
|
||||||
|
|
||||||
interface BaseTable {
|
|
||||||
val id: Long
|
|
||||||
val ctime: Long
|
|
||||||
val mtime: Long
|
|
||||||
}
|
|
32
app/src/main/java/li/songe/gkd/db/DbSet.kt
Normal file
32
app/src/main/java/li/songe/gkd/db/DbSet.kt
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
object DbSet {
|
||||||
|
|
||||||
|
|
||||||
|
private fun <T : RoomDatabase> getDb(
|
||||||
|
klass: Class<T>, name: String
|
||||||
|
): T {
|
||||||
|
return Room.databaseBuilder(
|
||||||
|
App.context, 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 subsItemDao by lazy { subsItemDb.subsItemDao() }
|
||||||
|
val subsConfigDao by lazy { subsConfigDb.subsConfigDao() }
|
||||||
|
val snapshotDao by lazy { snapshotDb.snapshotDao() }
|
||||||
|
val triggerLogDao by lazy { triggerLogDb.triggerLogDao() }
|
||||||
|
}
|
14
app/src/main/java/li/songe/gkd/db/IgnoreConverters.kt
Normal file
14
app/src/main/java/li/songe/gkd/db/IgnoreConverters.kt
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
package li.songe.gkd.db
|
||||||
|
|
||||||
|
import androidx.room.TypeConverter
|
||||||
|
import li.songe.gkd.data.NodeInfo
|
||||||
|
|
||||||
|
object IgnoreConverters {
|
||||||
|
@TypeConverter
|
||||||
|
@JvmStatic
|
||||||
|
fun listToCol(list: List<NodeInfo>): String? = null
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
@JvmStatic
|
||||||
|
fun colToList(value: String?): List<NodeInfo> = emptyList()
|
||||||
|
}
|
|
@ -1,35 +0,0 @@
|
||||||
package li.songe.gkd.db
|
|
||||||
|
|
||||||
import androidx.room.Database
|
|
||||||
import androidx.room.Room
|
|
||||||
import androidx.room.RoomDatabase
|
|
||||||
import com.blankj.utilcode.util.PathUtils
|
|
||||||
import li.songe.gkd.App
|
|
||||||
import li.songe.gkd.db.table.TriggerLog
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
@Database(
|
|
||||||
version = 1,
|
|
||||||
entities = [TriggerLog::class],
|
|
||||||
)
|
|
||||||
abstract class LogDatabase : RoomDatabase() {
|
|
||||||
abstract fun triggerLogRoomDao(): TriggerLog.RoomDao
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val logDb by lazy {
|
|
||||||
File(PathUtils.getExternalAppFilesPath().plus("/db/")).apply {
|
|
||||||
if (!exists()) {
|
|
||||||
mkdir()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val name = PathUtils.getExternalAppFilesPath().plus("/db/log.db")
|
|
||||||
Room.databaseBuilder(
|
|
||||||
App.context,
|
|
||||||
LogDatabase::class.java,
|
|
||||||
name
|
|
||||||
)
|
|
||||||
.fallbackToDestructiveMigration()
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
13
app/src/main/java/li/songe/gkd/db/SnapshotDb.kt
Normal file
13
app/src/main/java/li/songe/gkd/db/SnapshotDb.kt
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package li.songe.gkd.db
|
||||||
|
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import li.songe.gkd.data.Snapshot
|
||||||
|
|
||||||
|
@Database(
|
||||||
|
version = 1,
|
||||||
|
entities = [Snapshot::class],
|
||||||
|
)
|
||||||
|
abstract class SnapshotDb: RoomDatabase() {
|
||||||
|
abstract fun snapshotDao(): Snapshot.SnapshotDao
|
||||||
|
}
|
13
app/src/main/java/li/songe/gkd/db/SubsConfigDb.kt
Normal file
13
app/src/main/java/li/songe/gkd/db/SubsConfigDb.kt
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package li.songe.gkd.db
|
||||||
|
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import li.songe.gkd.data.SubsConfig
|
||||||
|
|
||||||
|
@Database(
|
||||||
|
version = 1,
|
||||||
|
entities = [SubsConfig::class],
|
||||||
|
)
|
||||||
|
abstract class SubsConfigDb: RoomDatabase() {
|
||||||
|
abstract fun subsConfigDao(): SubsConfig.SubsConfigDao
|
||||||
|
}
|
13
app/src/main/java/li/songe/gkd/db/SubsItemDb.kt
Normal file
13
app/src/main/java/li/songe/gkd/db/SubsItemDb.kt
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package li.songe.gkd.db
|
||||||
|
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import li.songe.gkd.data.SubsItem
|
||||||
|
|
||||||
|
@Database(
|
||||||
|
version = 1,
|
||||||
|
entities = [SubsItem::class],
|
||||||
|
)
|
||||||
|
abstract class SubsItemDb: RoomDatabase() {
|
||||||
|
abstract fun subsItemDao(): SubsItem.SubsItemDao
|
||||||
|
}
|
13
app/src/main/java/li/songe/gkd/db/TriggerLogDb.kt
Normal file
13
app/src/main/java/li/songe/gkd/db/TriggerLogDb.kt
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package li.songe.gkd.db
|
||||||
|
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import li.songe.gkd.data.TriggerLog
|
||||||
|
|
||||||
|
@Database(
|
||||||
|
version = 1,
|
||||||
|
entities = [TriggerLog::class],
|
||||||
|
)
|
||||||
|
abstract class TriggerLogDb : RoomDatabase() {
|
||||||
|
abstract fun triggerLogDao(): TriggerLog.TriggerLogDao
|
||||||
|
}
|
|
@ -1,43 +0,0 @@
|
||||||
package li.songe.gkd.db.table
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import androidx.room.ColumnInfo
|
|
||||||
import androidx.room.Dao
|
|
||||||
import androidx.room.Entity
|
|
||||||
import androidx.room.PrimaryKey
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
import li.songe.gkd.db.BaseDao
|
|
||||||
import li.songe.gkd.db.BaseTable
|
|
||||||
|
|
||||||
@Entity(
|
|
||||||
tableName = "subs_config",
|
|
||||||
)
|
|
||||||
@Parcelize
|
|
||||||
data class SubsConfig(
|
|
||||||
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") override val id: Long = 0,
|
|
||||||
@ColumnInfo(name = "ctime") override val ctime: Long = System.currentTimeMillis(),
|
|
||||||
@ColumnInfo(name = "mtime") override val mtime: Long = System.currentTimeMillis(),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 0 - app
|
|
||||||
* 1 - group
|
|
||||||
* 2 - rule
|
|
||||||
*/
|
|
||||||
@ColumnInfo(name = "type") val type: Int = 0,
|
|
||||||
@ColumnInfo(name = "enable") val enable: Boolean = true,
|
|
||||||
|
|
||||||
@ColumnInfo(name = "subs_item_id") val subsItemId: Long = -1,
|
|
||||||
@ColumnInfo(name = "app_id") val appId: String = "",
|
|
||||||
@ColumnInfo(name = "group_key") val groupKey: Int = -1,
|
|
||||||
@ColumnInfo(name = "rule_key") val ruleKey: Int = -1,
|
|
||||||
) : BaseTable, Parcelable {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val AppType = 0
|
|
||||||
const val GroupType = 1
|
|
||||||
const val RuleType = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
@Dao
|
|
||||||
interface RoomDao : BaseDao<SubsConfig>
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
package li.songe.gkd.db.table
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import androidx.room.ColumnInfo
|
|
||||||
import androidx.room.Dao
|
|
||||||
import androidx.room.Entity
|
|
||||||
import androidx.room.Index
|
|
||||||
import androidx.room.PrimaryKey
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
import li.songe.gkd.db.BaseDao
|
|
||||||
import li.songe.gkd.db.BaseTable
|
|
||||||
|
|
||||||
@Entity(
|
|
||||||
tableName = "subs_item",
|
|
||||||
indices = [Index(value = ["update_url"], unique = true)]
|
|
||||||
)
|
|
||||||
@Parcelize
|
|
||||||
data class SubsItem(
|
|
||||||
/**
|
|
||||||
* 当主键是0时,autoGenerate将覆盖此字段,插入数据库后 需要用返回值手动更新此字段
|
|
||||||
*/
|
|
||||||
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") override val id: Long = 0,
|
|
||||||
@ColumnInfo(name = "ctime") override val ctime: Long = System.currentTimeMillis(),
|
|
||||||
@ColumnInfo(name = "mtime") override val mtime: Long = System.currentTimeMillis(),
|
|
||||||
|
|
||||||
@ColumnInfo(name = "enable") val enable: Boolean = true,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 订阅文件 name 属性
|
|
||||||
*/
|
|
||||||
@ColumnInfo(name = "name") val name: String = "",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 订阅文件下载地址,也是更新链接
|
|
||||||
*/
|
|
||||||
@ColumnInfo(name = "update_url") val updateUrl: String = "",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 订阅文件下载地址,也是更新链接
|
|
||||||
*/
|
|
||||||
@ColumnInfo(name = "version") val version: Int = 0,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 订阅文件下载后存放的路径
|
|
||||||
*/
|
|
||||||
@ColumnInfo(name = "file_path") val filePath: String = "",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 顺序
|
|
||||||
*/
|
|
||||||
@ColumnInfo(name = "index") val index: Int = 0,
|
|
||||||
|
|
||||||
|
|
||||||
) : Parcelable, BaseTable {
|
|
||||||
@Dao
|
|
||||||
interface RoomDao : BaseDao<SubsItem>
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
package li.songe.gkd.db.table
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import androidx.room.ColumnInfo
|
|
||||||
import androidx.room.Dao
|
|
||||||
import androidx.room.Entity
|
|
||||||
import androidx.room.PrimaryKey
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
import li.songe.gkd.db.BaseDao
|
|
||||||
import li.songe.gkd.db.BaseTable
|
|
||||||
|
|
||||||
@Entity(
|
|
||||||
tableName = "trigger_log",
|
|
||||||
)
|
|
||||||
@Parcelize
|
|
||||||
data class TriggerLog(
|
|
||||||
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") override val id: Long = 0,
|
|
||||||
@ColumnInfo(name = "ctime") override val ctime: Long = System.currentTimeMillis(),
|
|
||||||
@ColumnInfo(name = "mtime") override val mtime: Long = System.currentTimeMillis(),
|
|
||||||
@ColumnInfo(name = "app_id") val appId: String? = null,
|
|
||||||
@ColumnInfo(name = "activity_id") val activityId: String? = null,
|
|
||||||
@ColumnInfo(name = "selector") val selector: String = ""
|
|
||||||
) : Parcelable, BaseTable {
|
|
||||||
@Dao
|
|
||||||
interface RoomDao : BaseDao<TriggerLog>
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
package li.songe.gkd.db.util
|
|
||||||
|
|
||||||
import android.database.DatabaseUtils
|
|
||||||
import kotlin.reflect.KClass
|
|
||||||
|
|
||||||
data class Expression<L : Any, R, T : Any>(
|
|
||||||
val left: L,
|
|
||||||
val operator: String,
|
|
||||||
val right: R,
|
|
||||||
val tableClass: KClass<T>
|
|
||||||
) {
|
|
||||||
fun stringify(): String {
|
|
||||||
val nameText = when (left) {
|
|
||||||
is String -> left.toString()
|
|
||||||
is Expression<*, *, *> -> left.stringify()
|
|
||||||
else -> throw Exception("not support type : $left")
|
|
||||||
}
|
|
||||||
val valueText = when (right) {
|
|
||||||
null -> "NULL"
|
|
||||||
is Boolean -> (if (right) 0 else 1).toString()
|
|
||||||
is String -> DatabaseUtils.sqlEscapeString(right.toString())
|
|
||||||
is Byte, is UByte, is Short, is UShort, is Int, is UInt, is Long, is ULong, is Float, is Double -> right.toString()
|
|
||||||
is List<*> -> "(" + right.joinToString(",\u0020") {
|
|
||||||
if (it is String) {
|
|
||||||
DatabaseUtils.sqlEscapeString(it)
|
|
||||||
} else {
|
|
||||||
it?.toString() ?: "NULL"
|
|
||||||
}
|
|
||||||
} + ")"
|
|
||||||
is GlobString -> right.stringify()
|
|
||||||
is LikeString -> right.stringify()
|
|
||||||
is Expression<*, *, *> -> "(${right.stringify()})"
|
|
||||||
else -> throw Exception("not support type : $right")
|
|
||||||
}
|
|
||||||
return "$nameText $operator $valueText"
|
|
||||||
}
|
|
||||||
|
|
||||||
infix fun <L2 : Any, R2> and(other: Expression<L2, R2, T>) =
|
|
||||||
Expression(this, "AND", other, tableClass)
|
|
||||||
|
|
||||||
infix fun <L2 : Any, R2> or(other: Expression<L2, R2, T>) =
|
|
||||||
Expression(this, "OR", other, tableClass)
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
package li.songe.gkd.db.util
|
|
||||||
|
|
||||||
import android.database.DatabaseUtils
|
|
||||||
|
|
||||||
data class GlobString(val sqlString: String = "") {
|
|
||||||
fun one() = GlobString("$sqlString?")
|
|
||||||
fun any() = GlobString("$sqlString*")
|
|
||||||
infix fun one(s: String) = GlobString("$sqlString?").str(s)
|
|
||||||
infix fun any(s: String) = GlobString("$sqlString*").str(s)
|
|
||||||
infix fun str(s: String) = GlobString(
|
|
||||||
sqlString + s.replace("\\", "\\\\")
|
|
||||||
.replace("*", "\\*")
|
|
||||||
.replace("?", "\\?")
|
|
||||||
)
|
|
||||||
|
|
||||||
fun stringify() = "${DatabaseUtils.sqlEscapeString(sqlString)} ESCAPE '\\'"
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun globString(value: String = "") = GlobString().str(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
package li.songe.gkd.db.util
|
|
||||||
|
|
||||||
import android.database.DatabaseUtils
|
|
||||||
|
|
||||||
|
|
||||||
data class LikeString (val sqlString: String = "") {
|
|
||||||
fun one() = LikeString("$sqlString?")
|
|
||||||
fun any() = LikeString("$sqlString*")
|
|
||||||
infix fun one(s: String) = LikeString("${sqlString}_").str(s)
|
|
||||||
infix fun any(s: String) = LikeString("$sqlString%").str(s)
|
|
||||||
infix fun str(s: String) = LikeString(
|
|
||||||
sqlString + s.replace("\\", "\\\\")
|
|
||||||
.replace("_", "\\_")
|
|
||||||
.replace("%", "\\%")
|
|
||||||
)
|
|
||||||
|
|
||||||
fun stringify() = "${DatabaseUtils.sqlEscapeString(sqlString)} ESCAPE '\\'"
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun likeString(value: String = "") = LikeString().str(value)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,66 +0,0 @@
|
||||||
package li.songe.gkd.db.util
|
|
||||||
|
|
||||||
import kotlin.reflect.KMutableProperty1
|
|
||||||
import kotlin.reflect.KProperty1
|
|
||||||
|
|
||||||
object Operator {
|
|
||||||
infix fun <L1 : Any, R1, L2 : Any, R2, T : Any> Expression<L1, R1, T>.and(other: Expression<L2, R2, T>) =
|
|
||||||
Expression(this, "AND", other, tableClass)
|
|
||||||
|
|
||||||
infix fun <L1 : Any, R1, L2 : Any, R2, T : Any> Expression<L1, R1, T>.or(other: Expression<L2, R2, T>) =
|
|
||||||
Expression(this, "OR", other, tableClass)
|
|
||||||
|
|
||||||
|
|
||||||
// TODO 当同时设置 Property1 时, 代码失效
|
|
||||||
// 还需要写 Int, Long, String, Boolean 等多种类型的重载, 这种重复性很高,工作量指数级增长的工作确实需要联合类型
|
|
||||||
inline fun <reified T : Any, V, V2> KMutableProperty1<T, V>.baseOperator(
|
|
||||||
value: V2,
|
|
||||||
operator: String,
|
|
||||||
) =
|
|
||||||
Expression(
|
|
||||||
RoomAnnotation.getColumnName(T::class, name),
|
|
||||||
operator,
|
|
||||||
value,
|
|
||||||
T::class
|
|
||||||
)
|
|
||||||
|
|
||||||
inline infix fun <reified T : Any, reified V> KProperty1<T, V>.eq(value: V) =
|
|
||||||
baseOperator(value, "==")
|
|
||||||
|
|
||||||
inline infix fun <reified T : Any, reified V> KProperty1<T, V>.neq(value: V) =
|
|
||||||
baseOperator(value, "!=")
|
|
||||||
|
|
||||||
inline infix fun <reified T : Any, reified V> KProperty1<T, V>.less(value: V) =
|
|
||||||
baseOperator(value, "<")
|
|
||||||
|
|
||||||
inline infix fun <reified T : Any, reified V> KProperty1<T, V>.lessEq(value: V) =
|
|
||||||
baseOperator(value, "<=")
|
|
||||||
|
|
||||||
inline infix fun <reified T : Any, reified V> KProperty1<T, V>.greater(value: V) =
|
|
||||||
baseOperator(value, ">")
|
|
||||||
|
|
||||||
inline infix fun <reified T : Any, reified V> KProperty1<T, V>.greaterEq(value: V) =
|
|
||||||
baseOperator(value, ">=")
|
|
||||||
|
|
||||||
inline infix fun <reified T : Any, reified V> KProperty1<T, V>.inList(value: List<V>) =
|
|
||||||
baseOperator(value, "IN")
|
|
||||||
|
|
||||||
inline infix fun <reified T : Any, reified V> KProperty1<T, V>.glob(value: GlobString) =
|
|
||||||
baseOperator(value, "GLOB")
|
|
||||||
|
|
||||||
inline infix fun <reified T : Any, reified V> KProperty1<T, V>.like(value: LikeString) =
|
|
||||||
baseOperator(value, "LIKE")
|
|
||||||
|
|
||||||
inline fun <reified T : Any, V, V2> KProperty1<T, V>.baseOperator(
|
|
||||||
value: V2,
|
|
||||||
operator: String,
|
|
||||||
) =
|
|
||||||
Expression(
|
|
||||||
RoomAnnotation.getColumnName(T::class, name),
|
|
||||||
operator,
|
|
||||||
value,
|
|
||||||
T::class
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
package li.songe.gkd.db.util
|
|
||||||
|
|
||||||
import li.songe.gkd.db.table.SubsConfig
|
|
||||||
import li.songe.gkd.db.table.SubsItem
|
|
||||||
import li.songe.gkd.db.table.TriggerLog
|
|
||||||
import kotlin.reflect.KClass
|
|
||||||
|
|
||||||
object RoomAnnotation {
|
|
||||||
|
|
||||||
fun getTableName(cls: KClass<*>): String = when (cls) {
|
|
||||||
SubsConfig::class -> "subs_config"
|
|
||||||
SubsItem::class -> "subs_item"
|
|
||||||
TriggerLog::class -> "trigger_log"
|
|
||||||
else -> throw Exception("""not found className : ${cls.qualifiedName}""")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getColumnName(cls: KClass<*>, propertyName: String): String = when (cls) {
|
|
||||||
SubsConfig::class -> when (propertyName) {
|
|
||||||
SubsConfig::id.name -> "id"
|
|
||||||
SubsConfig::ctime.name -> "ctime"
|
|
||||||
SubsConfig::mtime.name -> "mtime"
|
|
||||||
SubsConfig::type.name -> "type"
|
|
||||||
SubsConfig::enable.name -> "enable"
|
|
||||||
SubsConfig::subsItemId.name -> "subs_item_id"
|
|
||||||
SubsConfig::appId.name -> "app_id"
|
|
||||||
SubsConfig::groupKey.name -> "group_key"
|
|
||||||
SubsConfig::ruleKey.name -> "rule_key"
|
|
||||||
else -> error("""not found columnName : ${cls.qualifiedName}#$propertyName""")
|
|
||||||
}
|
|
||||||
|
|
||||||
SubsItem::class -> when (propertyName) {
|
|
||||||
SubsItem::id.name -> "id"
|
|
||||||
SubsItem::ctime.name -> "ctime"
|
|
||||||
SubsItem::mtime.name -> "mtime"
|
|
||||||
SubsItem::enable.name -> "enable"
|
|
||||||
SubsItem::name.name -> "name"
|
|
||||||
SubsItem::updateUrl.name -> "update_url"
|
|
||||||
SubsItem::filePath.name -> "file_path"
|
|
||||||
SubsItem::index.name -> "index"
|
|
||||||
else -> error("""not found columnName : ${cls.qualifiedName}#$propertyName""")
|
|
||||||
}
|
|
||||||
|
|
||||||
TriggerLog::class -> when (propertyName) {
|
|
||||||
TriggerLog::id.name -> "id"
|
|
||||||
TriggerLog::ctime.name -> "ctime"
|
|
||||||
TriggerLog::mtime.name -> "mtime"
|
|
||||||
TriggerLog::appId.name -> "app_id"
|
|
||||||
TriggerLog::activityId.name -> "activity_id"
|
|
||||||
TriggerLog::selector.name -> "selector"
|
|
||||||
else -> error("""not found columnName : ${cls.qualifiedName}#$propertyName""")
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> error("""not found className : ${cls.qualifiedName}""")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,84 +0,0 @@
|
||||||
package li.songe.gkd.db.util
|
|
||||||
|
|
||||||
import androidx.sqlite.db.SimpleSQLiteQuery
|
|
||||||
import li.songe.gkd.db.AppDatabase.Companion.db
|
|
||||||
import li.songe.gkd.db.BaseDao
|
|
||||||
import li.songe.gkd.db.LogDatabase.Companion.logDb
|
|
||||||
import li.songe.gkd.db.table.*
|
|
||||||
import kotlin.reflect.KClass
|
|
||||||
|
|
||||||
|
|
||||||
object RoomX {
|
|
||||||
// 把表类和具体数据库方法关联起来
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
fun <T : Any> getBaseDao(cls: KClass<T>) = when (cls) {
|
|
||||||
SubsItem::class -> db.subsItemRoomDao()
|
|
||||||
SubsConfig::class -> db.subsConfigRoomDao()
|
|
||||||
TriggerLog::class -> logDb.triggerLogRoomDao()
|
|
||||||
else -> error("not found class dao : ${cls::class.java.name}")
|
|
||||||
} as BaseDao<T>
|
|
||||||
|
|
||||||
|
|
||||||
suspend inline fun <reified T : Any> update(vararg objects: T): Int {
|
|
||||||
return getBaseDao(T::class).update(*objects)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 插入成功后, 自动改变入参对象的 id
|
|
||||||
*/
|
|
||||||
suspend inline fun <reified T : Any> insert(vararg objects: T): List<Long> {
|
|
||||||
return getBaseDao(T::class).insert(*objects)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend inline fun <reified T : Any> delete(vararg objects: T) =
|
|
||||||
getBaseDao(T::class).delete(*objects)
|
|
||||||
|
|
||||||
suspend inline fun <reified T : Any> select(
|
|
||||||
limit: Int? = null,
|
|
||||||
offset: Int? = null,
|
|
||||||
noinline block: (() -> Expression<*, *, T>)? = null
|
|
||||||
): List<T> {
|
|
||||||
val expression = block?.invoke()
|
|
||||||
val tableName = RoomAnnotation.getTableName(T::class)
|
|
||||||
val sqlString = "SELECT * FROM $tableName" + (if (expression != null) {
|
|
||||||
" WHERE ${expression.stringify()}"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
}) + (if (limit != null) {
|
|
||||||
" LIMIT $limit"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
}) + (if (offset != null) {
|
|
||||||
" OFFSET $offset"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
})
|
|
||||||
val baseDao = getBaseDao(T::class)
|
|
||||||
return baseDao.query(SimpleSQLiteQuery(sqlString))
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend inline fun <reified T : Any> delete(
|
|
||||||
limit: Int? = null,
|
|
||||||
offset: Int? = null,
|
|
||||||
noinline block: (() -> Expression<*, *, T>)? = null
|
|
||||||
): List<Int> {
|
|
||||||
val expression = block?.invoke()
|
|
||||||
val tableName = RoomAnnotation.getTableName(T::class)
|
|
||||||
val sqlString = "DELETE FROM $tableName" + (if (expression != null) {
|
|
||||||
" WHERE ${expression.stringify()}"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
}) + (if (limit != null) {
|
|
||||||
" LIMIT $limit"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
}) + (if (offset != null) {
|
|
||||||
" OFFSET $offset"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
})
|
|
||||||
val baseDao = getBaseDao(T::class)
|
|
||||||
return baseDao.delete(SimpleSQLiteQuery(sqlString))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -8,11 +8,14 @@ import com.blankj.utilcode.util.ServiceUtils
|
||||||
import com.torrydo.floatingbubbleview.FloatingBubble
|
import com.torrydo.floatingbubbleview.FloatingBubble
|
||||||
import li.songe.gkd.App
|
import li.songe.gkd.App
|
||||||
import li.songe.gkd.R
|
import li.songe.gkd.R
|
||||||
|
import li.songe.gkd.composition.CompositionExt.useLifeCycleLog
|
||||||
import li.songe.gkd.composition.CompositionFbService
|
import li.songe.gkd.composition.CompositionFbService
|
||||||
import li.songe.gkd.composition.CompositionExt.useMessage
|
import li.songe.gkd.composition.CompositionExt.useMessage
|
||||||
import li.songe.gkd.composition.InvokeMessage
|
import li.songe.gkd.composition.InvokeMessage
|
||||||
|
import li.songe.gkd.utils.SafeR
|
||||||
|
|
||||||
class FloatingService : CompositionFbService({
|
class FloatingService : CompositionFbService({
|
||||||
|
useLifeCycleLog()
|
||||||
val context = this
|
val context = this
|
||||||
val (onMessage, sendMessage) = useMessage(this::class.simpleName)
|
val (onMessage, sendMessage) = useMessage(this::class.simpleName)
|
||||||
|
|
||||||
|
@ -22,9 +25,8 @@ class FloatingService : CompositionFbService({
|
||||||
"removeBubbles" -> context.removeBubbles()
|
"removeBubbles" -> context.removeBubbles()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupBubble { _, resolve ->
|
setupBubble { _, resolve ->
|
||||||
val builder = FloatingBubble.Builder(this).bubble(R.drawable.capture, 40, 40)
|
val builder = FloatingBubble.Builder(this).bubble(SafeR.capture, 40, 40)
|
||||||
.enableCloseBubble(false)
|
.enableCloseBubble(false)
|
||||||
.addFloatingBubbleListener(object : FloatingBubble.Listener {
|
.addFloatingBubbleListener(object : FloatingBubble.Listener {
|
||||||
override fun onClick() {
|
override fun onClick() {
|
||||||
|
@ -38,16 +40,16 @@ class FloatingService : CompositionFbService({
|
||||||
override fun setupNotificationBuilder(channelId: String): Notification {
|
override fun setupNotificationBuilder(channelId: String): Notification {
|
||||||
return NotificationCompat.Builder(this, channelId)
|
return NotificationCompat.Builder(this, channelId)
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
.setSmallIcon(R.drawable.ic_app_2)
|
.setSmallIcon(SafeR.ic_launcher)
|
||||||
.setContentTitle("bubble is running")
|
.setContentTitle("搞快点")
|
||||||
.setContentText("click to do nothing")
|
.setContentText("正在显示悬浮窗按钮")
|
||||||
.setPriority(NotificationCompat.PRIORITY_MIN)
|
.setPriority(NotificationCompat.PRIORITY_MIN)
|
||||||
.setCategory(Notification.CATEGORY_SERVICE)
|
.setCategory(Notification.CATEGORY_SERVICE)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun channelId() = "your_channel_id"
|
override fun channelId() = "service-floating"
|
||||||
override fun channelName() = "your_channel_name"
|
override fun channelName() = "悬浮窗按钮服务"
|
||||||
override fun notificationId() = 69
|
override fun notificationId() = 69
|
||||||
|
|
||||||
companion object{
|
companion object{
|
||||||
|
|
|
@ -21,16 +21,24 @@ import io.ktor.server.routing.route
|
||||||
import io.ktor.server.routing.routing
|
import io.ktor.server.routing.routing
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.launch
|
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.CompositionExt.useMessage
|
||||||
import li.songe.gkd.composition.CompositionService
|
import li.songe.gkd.composition.CompositionService
|
||||||
import li.songe.gkd.composition.InvokeMessage
|
import li.songe.gkd.composition.InvokeMessage
|
||||||
|
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.debug.SnapshotExt.captureSnapshot
|
||||||
import li.songe.gkd.util.Ext.getIpAddressInLocalNetwork
|
import li.songe.gkd.utils.Ext.getIpAddressInLocalNetwork
|
||||||
import li.songe.gkd.util.Storage
|
import li.songe.gkd.utils.Storage
|
||||||
|
import li.songe.gkd.utils.launchTry
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class HttpService : CompositionService({
|
class HttpService : CompositionService({
|
||||||
|
@ -71,30 +79,16 @@ class HttpService : CompositionService({
|
||||||
|
|
||||||
routing {
|
routing {
|
||||||
route("/api") {
|
route("/api") {
|
||||||
get("/device") { call.respond(DeviceSnapshot.instance) }
|
get("/device") { call.respond(DeviceInfo.instance) }
|
||||||
get("/snapshotIds") {
|
|
||||||
call.respond(SnapshotExt.getSnapshotIds())
|
|
||||||
}
|
|
||||||
get("/snapshot") {
|
get("/snapshot") {
|
||||||
val id = call.request.queryParameters["id"]?.toLongOrNull()
|
val id = call.request.queryParameters["id"]?.toLongOrNull()
|
||||||
if (id != null) {
|
?: throw RpcError("miss id")
|
||||||
val fp = File(SnapshotExt.getSnapshotPath(id))
|
val fp = File(SnapshotExt.getSnapshotPath(id))
|
||||||
if (!fp.exists()) {
|
if (!fp.exists()) {
|
||||||
throw RpcError("对应快照不存在")
|
throw RpcError("对应快照不存在")
|
||||||
}
|
}
|
||||||
call.response.cacheControl(CacheControl.MaxAge(3600))
|
call.response.cacheControl(CacheControl.MaxAge(3600 * 24 * 7))
|
||||||
call.respondFile(fp)
|
call.respondFile(fp)
|
||||||
} else {
|
|
||||||
removeBubbles()
|
|
||||||
delay(200)
|
|
||||||
try {
|
|
||||||
call.respond(captureSnapshot())
|
|
||||||
} catch (e: Exception) {
|
|
||||||
throw e
|
|
||||||
} finally {
|
|
||||||
showBubbles()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
get("/screenshot") {
|
get("/screenshot") {
|
||||||
val id = call.request.queryParameters["id"]?.toLongOrNull()
|
val id = call.request.queryParameters["id"]?.toLongOrNull()
|
||||||
|
@ -103,22 +97,35 @@ class HttpService : CompositionService({
|
||||||
if (!fp.exists()) {
|
if (!fp.exists()) {
|
||||||
throw RpcError("对应截图不存在")
|
throw RpcError("对应截图不存在")
|
||||||
}
|
}
|
||||||
call.response.cacheControl(CacheControl.MaxAge(3600))
|
call.response.cacheControl(CacheControl.MaxAge(3600 * 24 * 7))
|
||||||
call.respondFile(fp)
|
call.respondFile(fp)
|
||||||
}
|
}
|
||||||
|
get("/captureSnapshot") {
|
||||||
|
removeBubbles()
|
||||||
|
delay(200)
|
||||||
|
val snapshot = try {
|
||||||
|
captureSnapshot()
|
||||||
|
} finally {
|
||||||
|
showBubbles()
|
||||||
|
}
|
||||||
|
call.respond(snapshot)
|
||||||
|
}
|
||||||
|
get("/snapshots") {
|
||||||
|
call.respond(DbSet.snapshotDao.query().first())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
scope.launch {
|
}
|
||||||
|
scope.launchTry(Dispatchers.IO) {
|
||||||
LogUtils.d(*getIpAddressInLocalNetwork().map { host -> "http://${host}:${Storage.settings.httpServerPort}" }
|
LogUtils.d(*getIpAddressInLocalNetwork().map { host -> "http://${host}:${Storage.settings.httpServerPort}" }
|
||||||
.toList().toTypedArray())
|
.toList().toTypedArray())
|
||||||
server.start(true)
|
server.start(true)
|
||||||
}
|
}
|
||||||
onDestroy {
|
onDestroy {
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launchTry(Dispatchers.IO) {
|
||||||
server.stop(1000, 2000)
|
server.stop()
|
||||||
scope.cancel()
|
|
||||||
LogUtils.d("http server is stopped")
|
LogUtils.d("http server is stopped")
|
||||||
|
scope.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import io.ktor.server.application.hooks.CallFailed
|
||||||
import io.ktor.server.request.uri
|
import io.ktor.server.request.uri
|
||||||
import io.ktor.server.response.header
|
import io.ktor.server.response.header
|
||||||
import io.ktor.server.response.respond
|
import io.ktor.server.response.respond
|
||||||
|
import li.songe.gkd.data.RpcError
|
||||||
|
|
||||||
val RpcErrorHeaderPlugin = createApplicationPlugin(name = "RpcErrorHeaderPlugin") {
|
val RpcErrorHeaderPlugin = createApplicationPlugin(name = "RpcErrorHeaderPlugin") {
|
||||||
onCall { call ->
|
onCall { call ->
|
||||||
|
@ -36,6 +37,7 @@ val RpcErrorHeaderPlugin = createApplicationPlugin(name = "RpcErrorHeaderPlugin"
|
||||||
}
|
}
|
||||||
onCallRespond { call, _ ->
|
onCallRespond { call, _ ->
|
||||||
call.response.header("Access-Control-Expose-Headers", "*")
|
call.response.header("Access-Control-Expose-Headers", "*")
|
||||||
|
call.response.header("Access-Control-Allow-Private-Network", "true")
|
||||||
val status = call.response.status() ?: HttpStatusCode.OK
|
val status = call.response.status() ?: HttpStatusCode.OK
|
||||||
if (status == HttpStatusCode.OK &&
|
if (status == HttpStatusCode.OK &&
|
||||||
!call.response.headers.contains(
|
!call.response.headers.contains(
|
|
@ -6,11 +6,13 @@ import android.content.Intent
|
||||||
import com.blankj.utilcode.util.LogUtils
|
import com.blankj.utilcode.util.LogUtils
|
||||||
import com.blankj.utilcode.util.ServiceUtils
|
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.composition.CompositionService
|
||||||
import li.songe.gkd.util.Ext
|
import li.songe.gkd.utils.Ext
|
||||||
import li.songe.gkd.util.ScreenshotUtil
|
import li.songe.gkd.utils.ScreenshotUtil
|
||||||
|
|
||||||
class ScreenshotService : CompositionService({
|
class ScreenshotService : CompositionService({
|
||||||
|
useLifeCycleLog()
|
||||||
Ext.createNotificationChannel(this, 110)
|
Ext.createNotificationChannel(this, 110)
|
||||||
|
|
||||||
onStartCommand { intent, _, _ ->
|
onStartCommand { intent, _, _ ->
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
package li.songe.gkd.debug
|
|
||||||
|
|
||||||
import com.blankj.utilcode.util.ScreenUtils
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import li.songe.gkd.accessibility.GkdAbService
|
|
||||||
import li.songe.gkd.util.Ext
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Snapshot(
|
|
||||||
val id: Long = System.currentTimeMillis(),
|
|
||||||
val device: DeviceSnapshot? = DeviceSnapshot.instance,
|
|
||||||
val screenHeight: Int = ScreenUtils.getScreenHeight(),
|
|
||||||
val screenWidth: Int = ScreenUtils.getScreenWidth(),
|
|
||||||
val appId: String? = null,
|
|
||||||
val appName: String? = null,
|
|
||||||
val activityId: String? = null,
|
|
||||||
val nodes: List<NodeSnapshot>? = null,
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
fun current(): Snapshot {
|
|
||||||
val shot = GkdAbService.currentNodeSnapshot()
|
|
||||||
return Snapshot(
|
|
||||||
appId = shot?.appId,
|
|
||||||
appName = if (shot?.appId != null) {
|
|
||||||
Ext.getAppName(shot.appId)
|
|
||||||
} else null,
|
|
||||||
activityId = shot?.activityId,
|
|
||||||
nodes = NodeSnapshot.info2nodeList(shot?.root),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,13 +2,20 @@ package li.songe.gkd.debug
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import com.blankj.utilcode.util.LogUtils
|
import com.blankj.utilcode.util.LogUtils
|
||||||
|
import com.blankj.utilcode.util.ScreenUtils
|
||||||
|
import com.blankj.utilcode.util.ZipUtils
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import li.songe.gkd.App
|
import li.songe.gkd.App
|
||||||
import li.songe.gkd.accessibility.GkdAbService
|
import li.songe.gkd.accessibility.GkdAbService
|
||||||
import li.songe.gkd.util.Singleton
|
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 java.io.File
|
import java.io.File
|
||||||
|
|
||||||
object SnapshotExt {
|
object SnapshotExt {
|
||||||
|
@ -16,7 +23,15 @@ object SnapshotExt {
|
||||||
App.context.getExternalFilesDir("snapshot")!!.apply { if (!exists()) mkdir() }
|
App.context.getExternalFilesDir("snapshot")!!.apply { if (!exists()) mkdir() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSnapshotParentPath(snapshotId: Long) =
|
private val emptyBitmap by lazy {
|
||||||
|
Bitmap.createBitmap(
|
||||||
|
ScreenUtils.getScreenWidth(),
|
||||||
|
ScreenUtils.getScreenHeight(),
|
||||||
|
Bitmap.Config.ARGB_8888
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSnapshotParentPath(snapshotId: Long) =
|
||||||
"${snapshotDir.absolutePath}/${snapshotId}"
|
"${snapshotDir.absolutePath}/${snapshotId}"
|
||||||
|
|
||||||
fun getSnapshotPath(snapshotId: Long) =
|
fun getSnapshotPath(snapshotId: Long) =
|
||||||
|
@ -30,21 +45,51 @@ object SnapshotExt {
|
||||||
?.mapNotNull { f -> f.name.toLongOrNull() } ?: emptyList()
|
?.mapNotNull { f -> f.name.toLongOrNull() } ?: emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getSnapshotZipFile(snapshotId: Long): File {
|
||||||
|
val file = File(getSnapshotParentPath(snapshotId) + "/${snapshotId}.zip")
|
||||||
|
if (!file.exists()) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
ZipUtils.zipFiles(
|
||||||
|
listOf(
|
||||||
|
getSnapshotPath(snapshotId),
|
||||||
|
getScreenshotPath(snapshotId)
|
||||||
|
), file.absolutePath
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
fun remove(id: Long) {
|
||||||
|
File(getSnapshotParentPath(id)).apply {
|
||||||
|
if (exists()) {
|
||||||
|
deleteRecursively()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun captureSnapshot(): Snapshot {
|
suspend fun captureSnapshot(): Snapshot {
|
||||||
if (!GkdAbService.isRunning()) {
|
if (!GkdAbService.isRunning()) {
|
||||||
throw RpcError("无障碍不可用")
|
throw RpcError("无障碍不可用")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val snapshotDef = coroutineScope { async(Dispatchers.IO) { Snapshot.current() } }
|
||||||
|
val bitmapDef = coroutineScope {
|
||||||
|
async(Dispatchers.IO) {
|
||||||
|
GkdAbService.currentScreenshot() ?: withTimeoutOrNull(3_000) {
|
||||||
if (!ScreenshotService.isRunning()) {
|
if (!ScreenshotService.isRunning()) {
|
||||||
|
return@withTimeoutOrNull null
|
||||||
|
}
|
||||||
|
ScreenshotService.screenshot()
|
||||||
|
} ?: emptyBitmap.apply {
|
||||||
LogUtils.d("截屏不可用,即将使用空白图片")
|
LogUtils.d("截屏不可用,即将使用空白图片")
|
||||||
}
|
}
|
||||||
val snapshot = Snapshot.current()
|
}
|
||||||
val bitmap = withTimeoutOrNull(3_000) {
|
}
|
||||||
ScreenshotService.screenshot()
|
|
||||||
} ?: Bitmap.createBitmap(
|
val bitmap = bitmapDef.await()
|
||||||
snapshot.screenWidth,
|
val snapshot = snapshotDef.await()
|
||||||
snapshot.screenHeight,
|
|
||||||
Bitmap.Config.ARGB_8888
|
|
||||||
)
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
File(getSnapshotParentPath(snapshot.id)).apply { if (!exists()) mkdirs() }
|
File(getSnapshotParentPath(snapshot.id)).apply { if (!exists()) mkdirs() }
|
||||||
val stream =
|
val stream =
|
||||||
|
@ -53,6 +98,7 @@ object SnapshotExt {
|
||||||
stream.close()
|
stream.close()
|
||||||
val text = Singleton.json.encodeToString(snapshot)
|
val text = Singleton.json.encodeToString(snapshot)
|
||||||
File(getSnapshotPath(snapshot.id)).writeText(text)
|
File(getSnapshotPath(snapshot.id)).writeText(text)
|
||||||
|
DbSet.snapshotDao.insert(snapshot)
|
||||||
}
|
}
|
||||||
return snapshot
|
return snapshot
|
||||||
}
|
}
|
||||||
|
|
24
app/src/main/java/li/songe/gkd/icon/AddIcon.kt
Normal file
24
app/src/main/java/li/songe/gkd/icon/AddIcon.kt
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
package li.songe.gkd.icon
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
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
|
||||||
|
|
||||||
|
// @DslMarker
|
||||||
|
// https://github.com/JetBrains/kotlin-wrappers/blob/master/kotlin-react/src/jsMain/kotlin/react/ChildrenBuilder.kt
|
||||||
|
val AddIcon = materialIcon(name = "add") {
|
||||||
|
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))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun PreviewIconAdd() {
|
||||||
|
Image(imageVector = AddIcon, contentDescription = null)
|
||||||
|
}
|
31
app/src/main/java/li/songe/gkd/icon/TestDsl.kt
Normal file
31
app/src/main/java/li/songe/gkd/icon/TestDsl.kt
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
package li.songe.gkd.icon
|
||||||
|
|
||||||
|
import android.graphics.drawable.BitmapDrawable
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun PreviewTestDsl() {
|
||||||
|
val vectorString = """
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8l8,8l1.41,-1.41L7.83,13H20v-2z" />
|
||||||
|
</vector>
|
||||||
|
""".trim()
|
||||||
|
val drawable = Drawable.createFromStream(vectorString.byteInputStream(), "ic_back")
|
||||||
|
if (drawable != null) {
|
||||||
|
Image(painter = rememberDrawablePainter(drawable = drawable), contentDescription = null)
|
||||||
|
} else {
|
||||||
|
Text(text = "null drawable")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,41 +0,0 @@
|
||||||
package li.songe.gkd.selector
|
|
||||||
|
|
||||||
import android.view.accessibility.AccessibilityNodeInfo
|
|
||||||
import li.songe.selector_core.NodeExt
|
|
||||||
|
|
||||||
@JvmInline
|
|
||||||
value class AbNode(val value: AccessibilityNodeInfo) : NodeExt {
|
|
||||||
override val parent: NodeExt?
|
|
||||||
get() = value.parent?.let { AbNode(it) }
|
|
||||||
override val children: Sequence<NodeExt?>
|
|
||||||
get() = sequence {
|
|
||||||
repeat(value.childCount) { i ->
|
|
||||||
val child = value.getChild(i)
|
|
||||||
if (child != null) {
|
|
||||||
yield(AbNode(child))
|
|
||||||
} else {
|
|
||||||
yield(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getChild(offset: Int) = value.getChild(offset)?.let { AbNode(it) }
|
|
||||||
|
|
||||||
override val name: CharSequence
|
|
||||||
get() = value.className
|
|
||||||
|
|
||||||
override fun attr(name: String): Any? = when (name) {
|
|
||||||
"id" -> value.viewIdResourceName
|
|
||||||
"name" -> value.className
|
|
||||||
"text" -> value.text
|
|
||||||
"textLen" -> value.text?.length
|
|
||||||
"desc" -> value.contentDescription
|
|
||||||
"descLen" -> value.contentDescription?.length
|
|
||||||
"isClickable" -> value.isClickable
|
|
||||||
"childCount" -> value.childCount
|
|
||||||
"index" -> value.getIndex()
|
|
||||||
"depth" -> value.getDepth()
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,77 +0,0 @@
|
||||||
package li.songe.gkd.selector
|
|
||||||
|
|
||||||
import android.accessibilityservice.AccessibilityService
|
|
||||||
import android.accessibilityservice.GestureDescription
|
|
||||||
import android.graphics.Path
|
|
||||||
import android.graphics.Rect
|
|
||||||
import android.view.accessibility.AccessibilityNodeInfo
|
|
||||||
import li.songe.selector_core.Selector
|
|
||||||
|
|
||||||
fun AccessibilityNodeInfo.getIndex(): Int {
|
|
||||||
parent?.forEachIndexed { index, accessibilityNodeInfo ->
|
|
||||||
if (accessibilityNodeInfo == this) {
|
|
||||||
return index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun AccessibilityNodeInfo.forEachIndexed(action: (index: Int, childNode: AccessibilityNodeInfo?) -> Unit) {
|
|
||||||
var index = 0
|
|
||||||
val childCount = this.childCount
|
|
||||||
while (index < childCount) {
|
|
||||||
val child: AccessibilityNodeInfo? = getChild(index)
|
|
||||||
action(index, child)
|
|
||||||
index += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun AccessibilityNodeInfo.querySelector(selector: Selector): AccessibilityNodeInfo? {
|
|
||||||
val ab = AbNode(this)
|
|
||||||
val result = (ab.querySelector(selector) as AbNode?) ?: return null
|
|
||||||
return result.value
|
|
||||||
}
|
|
||||||
|
|
||||||
fun AccessibilityNodeInfo.querySelectorAll(selector: Selector): Sequence<AbNode> {
|
|
||||||
val ab = AbNode(this)
|
|
||||||
return ab.querySelectorAll(selector) as Sequence<AbNode>
|
|
||||||
}
|
|
||||||
|
|
||||||
fun AccessibilityNodeInfo.click(service: AccessibilityService) = when {
|
|
||||||
this.isClickable -> {
|
|
||||||
this.performAction(AccessibilityNodeInfo.ACTION_CLICK)
|
|
||||||
"self"
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
val react = Rect()
|
|
||||||
this.getBoundsInScreen(react)
|
|
||||||
val x = react.left + 50f / 100f * (react.right - react.left)
|
|
||||||
val y = react.top + 50f / 100f * (react.bottom - react.top)
|
|
||||||
if (x >= 0 && y >= 0) {
|
|
||||||
val gestureDescription = GestureDescription.Builder()
|
|
||||||
val path = Path()
|
|
||||||
path.moveTo(x, y)
|
|
||||||
gestureDescription.addStroke(GestureDescription.StrokeDescription(path, 0, 300))
|
|
||||||
service.dispatchGesture(gestureDescription.build(), null, null)
|
|
||||||
"(50%, 50%)"
|
|
||||||
} else {
|
|
||||||
"($x, $y) no click"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun AccessibilityNodeInfo.getDepth(): Int {
|
|
||||||
var p: AccessibilityNodeInfo? = this
|
|
||||||
var depth = 0
|
|
||||||
while (true) {
|
|
||||||
val p2 = p?.parent
|
|
||||||
if (p2 != null) {
|
|
||||||
p = p2
|
|
||||||
depth++
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return depth
|
|
||||||
}
|
|
|
@ -11,27 +11,9 @@ class AutoStartReceiver : BroadcastReceiver() {
|
||||||
Shizuku.addBinderReceivedListenerSticky(oneShotBinderReceivedListener)
|
Shizuku.addBinderReceivedListenerSticky(oneShotBinderReceivedListener)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val oneShotBinderReceivedListener = object : Shizuku.OnBinderReceivedListener {
|
private val oneShotBinderReceivedListener = object : Shizuku.OnBinderReceivedListener {
|
||||||
override fun onBinderReceived() {
|
override fun onBinderReceived() {
|
||||||
// AutomatorViewModel.get().run {
|
|
||||||
// app.openFileOutput("on_boot", Context.MODE_PRIVATE).bufferedWriter().apply {
|
|
||||||
// write("binder received")
|
|
||||||
// newLine()
|
|
||||||
// write("permission granted: ${Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED}")
|
|
||||||
// newLine()
|
|
||||||
// write("is_binding: ${isBinding.value}")
|
|
||||||
// newLine()
|
|
||||||
// write("is_running: ${isRunning.value}")
|
|
||||||
// newLine()
|
|
||||||
// if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED &&
|
|
||||||
// isBinding.value != true && isRunning.value != true
|
|
||||||
// ) {
|
|
||||||
// write("starting service...")
|
|
||||||
// toggleService()
|
|
||||||
// isAutoStarted.value = true
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
Shizuku.removeBinderReceivedListener(this)
|
Shizuku.removeBinderReceivedListener(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,168 +0,0 @@
|
||||||
package li.songe.gkd.shizuku
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.Log
|
|
||||||
import java.io.*
|
|
||||||
import java.nio.charset.Charset
|
|
||||||
import java.nio.charset.StandardCharsets
|
|
||||||
import java.security.DigestInputStream
|
|
||||||
import java.security.MessageDigest
|
|
||||||
import java.util.zip.CRC32
|
|
||||||
|
|
||||||
|
|
||||||
object IOUtils {
|
|
||||||
private const val TAG = "IOUtils"
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun copyStream(from: InputStream, to: OutputStream) {
|
|
||||||
val buf = ByteArray(1024 * 1024)
|
|
||||||
var len: Int
|
|
||||||
while (from.read(buf).also { len = it } > 0) {
|
|
||||||
to.write(buf, 0, len)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun copyFile(original: File?, destination: File?) {
|
|
||||||
FileInputStream(original).use { inputStream ->
|
|
||||||
FileOutputStream(destination).use { outputStream ->
|
|
||||||
copyStream(
|
|
||||||
inputStream,
|
|
||||||
outputStream
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun copyFileFromAssets(context: Context, assetFileName: String?, destination: File?) {
|
|
||||||
context.assets.open(assetFileName!!).use { inputStream ->
|
|
||||||
FileOutputStream(destination).use { outputStream ->
|
|
||||||
copyStream(
|
|
||||||
inputStream,
|
|
||||||
outputStream
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteRecursively(f: File) {
|
|
||||||
if (f.isDirectory) {
|
|
||||||
val files = f.listFiles()
|
|
||||||
if (files != null) {
|
|
||||||
for (child in files) deleteRecursively(child)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
f.delete()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun calculateFileCrc32(file: File?): Long {
|
|
||||||
return calculateCrc32(FileInputStream(file))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun calculateBytesCrc32(bytes: ByteArray?): Long {
|
|
||||||
return calculateCrc32(ByteArrayInputStream(bytes))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun calculateCrc32(inputStream: InputStream): Long {
|
|
||||||
inputStream.use { `in` ->
|
|
||||||
val crc32 = CRC32()
|
|
||||||
val buffer = ByteArray(1024 * 1024)
|
|
||||||
var read: Int
|
|
||||||
while (`in`.read(buffer).also { read = it } > 0) crc32.update(buffer, 0, read)
|
|
||||||
return crc32.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun writeStreamToStringBuilder(builder: StringBuilder, inputStream: InputStream?): Thread {
|
|
||||||
val t = Thread {
|
|
||||||
try {
|
|
||||||
val buf = CharArray(1024)
|
|
||||||
var len: Int
|
|
||||||
val reader =
|
|
||||||
BufferedReader(InputStreamReader(inputStream))
|
|
||||||
while (reader.read(buf).also { len = it } > 0) builder.append(buf, 0, len)
|
|
||||||
reader.close()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.wtf(TAG, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
t.start()
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read contents of input stream to a byte array and close it
|
|
||||||
*
|
|
||||||
* @param inputStream
|
|
||||||
* @return contents of input stream
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun readStream(inputStream: InputStream): ByteArray {
|
|
||||||
inputStream.use { `in` -> return readStreamNoClose(`in`) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun readStream(inputStream: InputStream, charset: Charset?): String {
|
|
||||||
return String(readStream(inputStream), charset!!)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read contents of input stream to a byte array, but don't close the stream
|
|
||||||
*
|
|
||||||
* @param inputStream
|
|
||||||
* @return contents of input stream
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun readStreamNoClose(inputStream: InputStream): ByteArray {
|
|
||||||
val buffer = ByteArrayOutputStream()
|
|
||||||
copyStream(inputStream, buffer)
|
|
||||||
return buffer.toByteArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun closeSilently(closeable: Closeable?) {
|
|
||||||
if (closeable == null) return
|
|
||||||
try {
|
|
||||||
closeable.close()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w(TAG, String.format("Unable to close %s", closeable.javaClass.canonicalName), e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hashes stream content using passed [MessageDigest], closes the stream and returns digest bytes
|
|
||||||
*
|
|
||||||
* @param inputStream
|
|
||||||
* @param messageDigest
|
|
||||||
* @return
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun hashStream(inputStream: InputStream?, messageDigest: MessageDigest): ByteArray {
|
|
||||||
DigestInputStream(inputStream, messageDigest).use { digestInputStream ->
|
|
||||||
val buffer = ByteArray(1024 * 64)
|
|
||||||
var read: Int
|
|
||||||
while (digestInputStream.read(buffer).also { read = it } > 0) {
|
|
||||||
//Do nothing
|
|
||||||
}
|
|
||||||
return messageDigest.digest()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun hashString(s: String, messageDigest: MessageDigest): ByteArray {
|
|
||||||
return hashStream(
|
|
||||||
ByteArrayInputStream(s.toByteArray(StandardCharsets.UTF_8)),
|
|
||||||
messageDigest
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun readFile(file: File?): ByteArray {
|
|
||||||
FileInputStream(file).use { `in` -> return readStream(`in`) }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,71 +0,0 @@
|
||||||
package li.songe.gkd.shizuku
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
|
|
||||||
interface Shell {
|
|
||||||
val isAvailable: Boolean
|
|
||||||
|
|
||||||
fun exec(command: Command): Result
|
|
||||||
fun exec(command: Command, inputPipe: InputStream): Result
|
|
||||||
fun makeLiteral(arg: String): String
|
|
||||||
class Command(command: String, vararg args: String) {
|
|
||||||
private val mArgs = mutableListOf<String>()
|
|
||||||
fun toStringArray(): Array<String?> {
|
|
||||||
val array = arrayOfNulls<String>(mArgs.size)
|
|
||||||
for (i in mArgs.indices) array[i] = mArgs[i]
|
|
||||||
return array
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
val sb = StringBuilder()
|
|
||||||
for (i in mArgs.indices) {
|
|
||||||
val arg = mArgs[i]
|
|
||||||
sb.append(arg)
|
|
||||||
if (i < mArgs.size - 1) sb.append(" ")
|
|
||||||
}
|
|
||||||
return sb.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
class Builder(command: String, vararg args: String) {
|
|
||||||
private val mCommand: Command = Command(command, *args)
|
|
||||||
fun addArg(argument: String): Builder {
|
|
||||||
mCommand.mArgs.add(argument)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
fun build(): Command {
|
|
||||||
return mCommand
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
mArgs.add(command)
|
|
||||||
mArgs.addAll(args)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
open class Result(
|
|
||||||
var cmd: Command,
|
|
||||||
var exitCode: Int,
|
|
||||||
var out: String,
|
|
||||||
var err: String
|
|
||||||
) {
|
|
||||||
val isSuccessful: Boolean
|
|
||||||
get() = exitCode == 0
|
|
||||||
|
|
||||||
@SuppressLint("DefaultLocale")
|
|
||||||
override fun toString(): String {
|
|
||||||
return String.format(
|
|
||||||
"Command: %s\nExit code: %d\nOut:\n%s\n=============\nErr:\n%s",
|
|
||||||
cmd,
|
|
||||||
exitCode,
|
|
||||||
out,
|
|
||||||
err
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
12
app/src/main/java/li/songe/gkd/shizuku/ShizukuExt.kt
Normal file
12
app/src/main/java/li/songe/gkd/shizuku/ShizukuExt.kt
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
package li.songe.gkd.shizuku
|
||||||
|
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import rikka.shizuku.Shizuku
|
||||||
|
|
||||||
|
fun shizukuIsSafeOK(): Boolean {
|
||||||
|
return try {
|
||||||
|
Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED
|
||||||
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,89 +0,0 @@
|
||||||
package li.songe.gkd.shizuku
|
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
|
||||||
import rikka.shizuku.Shizuku
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* https://github.com/Aefyr/SAI/blob/master/app/src/main/java/com/aefyr/sai/shell/ShizukuShell.java
|
|
||||||
*/
|
|
||||||
class ShizukuShell private constructor() : Shell {
|
|
||||||
override val isAvailable: Boolean
|
|
||||||
get() = if (!Shizuku.pingBinder()) false else try {
|
|
||||||
exec(Shell.Command("echo", "test")).isSuccessful
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w(TAG, "Unable to access shizuku: ")
|
|
||||||
Log.w(TAG, e)
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun exec(command: Shell.Command): Shell.Result {
|
|
||||||
return execInternal(command, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun exec(command: Shell.Command, inputPipe: InputStream): Shell.Result {
|
|
||||||
return execInternal(command, inputPipe)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun makeLiteral(arg: String): String {
|
|
||||||
return "'" + arg.replace("'", "'\\''") + "'"
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun execInternal(command: Shell.Command, inputPipe: InputStream?): Shell.Result {
|
|
||||||
val stdOutSb = StringBuilder()
|
|
||||||
val stdErrSb = StringBuilder()
|
|
||||||
return try {
|
|
||||||
val shCommand = Shell.Command.Builder("sh", "-c", command.toString())
|
|
||||||
val process = Shizuku.newProcess(shCommand.build().toStringArray(), null, null)
|
|
||||||
val stdOutD: Thread = IOUtils.writeStreamToStringBuilder(stdOutSb, process.inputStream)
|
|
||||||
val stdErrD: Thread = IOUtils.writeStreamToStringBuilder(stdErrSb, process.errorStream)
|
|
||||||
if (inputPipe != null) {
|
|
||||||
try {
|
|
||||||
process.outputStream.use { outputStream ->
|
|
||||||
inputPipe.use { inputStream ->
|
|
||||||
IOUtils.copyStream(
|
|
||||||
inputStream,
|
|
||||||
outputStream
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
stdOutD.interrupt()
|
|
||||||
stdErrD.interrupt()
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
process.destroyForcibly()
|
|
||||||
} else {
|
|
||||||
process.destroy()
|
|
||||||
}
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
process.waitFor()
|
|
||||||
stdOutD.join()
|
|
||||||
stdErrD.join()
|
|
||||||
Shell.Result(
|
|
||||||
command,
|
|
||||||
process.exitValue(),
|
|
||||||
stdOutSb.toString().trim { it <= ' ' },
|
|
||||||
stdErrSb.toString().trim { it <= ' ' })
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w(TAG, "Unable execute command: ")
|
|
||||||
Log.w(TAG, e)
|
|
||||||
Shell.Result(
|
|
||||||
command, -1, stdOutSb.toString().trim { it <= ' ' },
|
|
||||||
"""$stdErrSb
|
|
||||||
|
|
||||||
<!> SAI ShizukuShell Java exception: ${Utils.throwableToString(e)}"""
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "ShizukuShell"
|
|
||||||
val instance by lazy { ShizukuShell() }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
package li.songe.gkd.shizuku
|
||||||
|
|
||||||
|
|
||||||
|
import android.app.IActivityTaskManager
|
||||||
|
import android.content.pm.IPackageManager
|
||||||
|
import rikka.shizuku.ShizukuBinderWrapper
|
||||||
|
import rikka.shizuku.SystemServiceHelper
|
||||||
|
|
||||||
|
val activityTaskManager: IActivityTaskManager by lazy {
|
||||||
|
SystemServiceHelper.getSystemService("activity_task")
|
||||||
|
.let(::ShizukuBinderWrapper)
|
||||||
|
.let(IActivityTaskManager.Stub::asInterface)
|
||||||
|
}
|
||||||
|
|
||||||
|
val iPackageManager: IPackageManager by lazy {
|
||||||
|
SystemServiceHelper.getSystemService("package")
|
||||||
|
.let(::ShizukuBinderWrapper)
|
||||||
|
.let(IPackageManager.Stub::asInterface)
|
||||||
|
}
|
|
@ -1,17 +0,0 @@
|
||||||
package li.songe.gkd.shizuku
|
|
||||||
|
|
||||||
import java.io.PrintWriter
|
|
||||||
import java.io.StringWriter
|
|
||||||
|
|
||||||
|
|
||||||
object Utils {
|
|
||||||
fun throwableToString(throwable: Throwable): String {
|
|
||||||
val sw = StringWriter(1024)
|
|
||||||
val pw = PrintWriter(sw)
|
|
||||||
|
|
||||||
throwable.printStackTrace(pw)
|
|
||||||
pw.close()
|
|
||||||
|
|
||||||
return sw.toString()
|
|
||||||
}
|
|
||||||
}
|
|
86
app/src/main/java/li/songe/gkd/ui/AboutPage.kt
Normal file
86
app/src/main/java/li/songe/gkd/ui/AboutPage.kt
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
@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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
|
@ -16,6 +16,7 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.shape.GenericShape
|
import androidx.compose.foundation.shape.GenericShape
|
||||||
import androidx.compose.material.Switch
|
import androidx.compose.material.Switch
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
@ -30,82 +31,69 @@ import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
import com.google.accompanist.placeholder.PlaceholderHighlight
|
import com.google.accompanist.placeholder.PlaceholderHighlight
|
||||||
import com.google.accompanist.placeholder.material.fade
|
import com.google.accompanist.placeholder.material.fade
|
||||||
import com.google.accompanist.placeholder.material.placeholder
|
import com.google.accompanist.placeholder.material.placeholder
|
||||||
import kotlinx.coroutines.delay
|
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.SubscriptionRaw
|
||||||
import li.songe.gkd.db.table.SubsConfig
|
import li.songe.gkd.data.getAppInfo
|
||||||
import li.songe.gkd.db.util.Operator.eq
|
import li.songe.gkd.db.DbSet
|
||||||
import li.songe.gkd.db.util.RoomX
|
import li.songe.gkd.utils.Singleton
|
||||||
import li.songe.gkd.ui.component.StatusBar
|
import li.songe.gkd.utils.launchAsFn
|
||||||
import li.songe.gkd.util.ThrottleState
|
|
||||||
import li.songe.router.LocalRoute
|
|
||||||
import li.songe.router.LocalRouter
|
|
||||||
import li.songe.router.Page
|
|
||||||
|
|
||||||
data class AppItemPageParams(
|
|
||||||
val subsApp: SubscriptionRaw.AppRaw,
|
|
||||||
val subsConfig: SubsConfig,
|
|
||||||
val appName: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
val AppItemPage = Page {
|
|
||||||
|
|
||||||
// https://developer.android.com/jetpack/compose/modifiers-list
|
|
||||||
|
|
||||||
val router = LocalRouter.current
|
|
||||||
|
|
||||||
val params = LocalRoute.current.data as AppItemPageParams
|
|
||||||
|
|
||||||
|
@RootNavGraph
|
||||||
|
@Destination
|
||||||
|
@Composable
|
||||||
|
fun AppItemPage(
|
||||||
|
subsApp: SubscriptionRaw.AppRaw,
|
||||||
|
subsConfig: SubsConfig,
|
||||||
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
// val context = LocalContext.current
|
|
||||||
|
|
||||||
var subsConfigList: List<SubsConfig?>? by remember { mutableStateOf(null) }
|
var subsConfigs: List<SubsConfig?>? by remember { mutableStateOf(null) }
|
||||||
|
|
||||||
val changeItemThrottle = ThrottleState.use(scope)
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
delay(400)
|
val mutableSet = DbSet.subsConfigDao.queryGroupTypeConfig(subsConfig.subsItemId, subsApp.id)
|
||||||
val config = params.subsConfig
|
|
||||||
val mutableSet =
|
|
||||||
RoomX.select { (SubsConfig::type eq SubsConfig.GroupType) and (SubsConfig::subsItemId eq config.subsItemId) and (SubsConfig::appId eq config.appId) }
|
|
||||||
.toMutableSet()
|
|
||||||
val list = mutableListOf<SubsConfig?>()
|
val list = mutableListOf<SubsConfig?>()
|
||||||
params.subsApp.groups.forEach { group ->
|
subsApp.groups.forEach { group ->
|
||||||
if (group.key == null) {
|
if (group.key == null) {
|
||||||
list.add(null)
|
list.add(null)
|
||||||
} else {
|
} else {
|
||||||
val item = mutableSet.find { s -> s.groupKey == group.key } ?: SubsConfig(
|
val item = mutableSet.find { s -> s.groupKey == group.key }
|
||||||
subsItemId = config.subsItemId,
|
?: SubsConfig(
|
||||||
appId = config.appId,
|
subsItemId = subsConfig.subsItemId,
|
||||||
|
appId = subsConfig.appId,
|
||||||
groupKey = group.key,
|
groupKey = group.key,
|
||||||
type = SubsConfig.GroupType
|
type = SubsConfig.GroupType
|
||||||
)
|
)
|
||||||
list.add(item)
|
list.add(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
subsConfigList = list
|
subsConfigs = list
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var showGroupItem: SubscriptionRaw.GroupRaw? by remember { mutableStateOf(null) }
|
||||||
|
|
||||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||||
item {
|
item {
|
||||||
Column {
|
|
||||||
StatusBar()
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(10.dp, 0.dp)
|
.padding(10.dp, 0.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = params.appName,
|
text = getAppInfo(subsApp.id).name ?: "-",
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
softWrap = false,
|
softWrap = false,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(10.dp))
|
Spacer(modifier = Modifier.width(10.dp))
|
||||||
Text(
|
Text(
|
||||||
text = params.subsApp.id,
|
text = subsApp.id,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
softWrap = false,
|
softWrap = false,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
@ -113,20 +101,13 @@ val AppItemPage = Page {
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(10.dp))
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
items(params.subsApp.groups.size) { i ->
|
items(subsApp.groups.size) { i ->
|
||||||
val group = params.subsApp.groups[i]
|
val group = subsApp.groups[i]
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable {
|
.clickable {
|
||||||
// router.navigate(
|
showGroupItem = group
|
||||||
// GroupItemPage, GroupItemPage.Params(
|
|
||||||
// group = group,
|
|
||||||
// subsConfig = subsConfigList?.get(i),
|
|
||||||
// appName = params.appName
|
|
||||||
// )
|
|
||||||
// )
|
|
||||||
}
|
}
|
||||||
.padding(10.dp, 6.dp)
|
.padding(10.dp, 6.dp)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
@ -149,7 +130,7 @@ val AppItemPage = Page {
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = group.activityIds?.joinToString() ?: "",
|
text = group.desc ?: "-",
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
softWrap = false,
|
softWrap = false,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
@ -163,10 +144,10 @@ val AppItemPage = Page {
|
||||||
if (group.key != null) {
|
if (group.key != null) {
|
||||||
val crPx = with(LocalDensity.current) { 4.dp.toPx() }
|
val crPx = with(LocalDensity.current) { 4.dp.toPx() }
|
||||||
Switch(
|
Switch(
|
||||||
checked = subsConfigList?.get(i)?.enable ?: true,
|
checked = subsConfigs?.get(i)?.enable != false,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.placeholder(
|
.placeholder(
|
||||||
subsConfigList == null,
|
subsConfigs == null,
|
||||||
highlight = PlaceholderHighlight.fade(),
|
highlight = PlaceholderHighlight.fade(),
|
||||||
shape = GenericShape { size, _ ->
|
shape = GenericShape { size, _ ->
|
||||||
val cr = CornerRadius(crPx, crPx)
|
val cr = CornerRadius(crPx, crPx)
|
||||||
|
@ -184,16 +165,12 @@ val AppItemPage = Page {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
// 当 onCheckedChange 是 null 时, size 是长方形, 反之是 正方形
|
onCheckedChange = scope.launchAsFn { enable ->
|
||||||
onCheckedChange = changeItemThrottle.invoke { enable ->
|
val subsConfigsVal = subsConfigs ?: return@launchAsFn
|
||||||
val list = subsConfigList ?: return@invoke
|
val newItem =
|
||||||
val newItem = list[i]?.copy(enable = enable) ?: return@invoke
|
subsConfigsVal[i]?.copy(enable = enable) ?: return@launchAsFn
|
||||||
if (newItem.id == 0L) {
|
DbSet.subsConfigDao.insert(newItem)
|
||||||
RoomX.insert(newItem)
|
subsConfigs = subsConfigsVal.toMutableList().apply {
|
||||||
} else {
|
|
||||||
RoomX.update(newItem)
|
|
||||||
}
|
|
||||||
subsConfigList = list.toMutableList().apply {
|
|
||||||
set(i, newItem)
|
set(i, newItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -210,5 +187,15 @@ val AppItemPage = Page {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
showGroupItem?.let { showGroupItemVal ->
|
||||||
|
Dialog(onDismissRequest = { showGroupItem = null }) {
|
||||||
|
Text(
|
||||||
|
text = Singleton.json.encodeToString(showGroupItemVal),
|
||||||
|
modifier = Modifier.width(400.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,151 +0,0 @@
|
||||||
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.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
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.ToastUtils
|
|
||||||
import com.dylanc.activityresult.launcher.launchForResult
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import li.songe.gkd.accessibility.GkdAbService
|
|
||||||
import li.songe.gkd.debug.FloatingService
|
|
||||||
import li.songe.gkd.debug.ScreenshotService
|
|
||||||
import li.songe.gkd.debug.HttpService
|
|
||||||
import li.songe.gkd.ui.component.StatusBar
|
|
||||||
import li.songe.gkd.ui.component.TextSwitch
|
|
||||||
import li.songe.gkd.util.Ext
|
|
||||||
import li.songe.gkd.util.Ext.LocalLauncher
|
|
||||||
import li.songe.gkd.util.Ext.usePollState
|
|
||||||
import li.songe.gkd.util.Storage
|
|
||||||
import li.songe.router.Page
|
|
||||||
|
|
||||||
|
|
||||||
val DebugPage = Page {
|
|
||||||
val context = LocalContext.current as ComponentActivity
|
|
||||||
val launcher = LocalLauncher.current
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
val httpServerRunning by usePollState { HttpService.isRunning() }
|
|
||||||
val screenshotRunning by usePollState { ScreenshotService.isRunning() }
|
|
||||||
val gkdAccessRunning by usePollState { GkdAbService.isRunning() }
|
|
||||||
val floatingRunning by usePollState {
|
|
||||||
FloatingService.isRunning() && Settings.canDrawOverlays(
|
|
||||||
context
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
val debugAvailable by remember {
|
|
||||||
derivedStateOf { httpServerRunning }
|
|
||||||
}
|
|
||||||
|
|
||||||
val serverUrl by remember {
|
|
||||||
derivedStateOf {
|
|
||||||
if (debugAvailable) {
|
|
||||||
Ext.getIpAddressInLocalNetwork()
|
|
||||||
.map { host -> "http://${host}:${Storage.settings.httpServerPort}" }
|
|
||||||
.joinToString("\n")
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.verticalScroll(
|
|
||||||
state = rememberScrollState()
|
|
||||||
)
|
|
||||||
.padding(20.dp)
|
|
||||||
) {
|
|
||||||
StatusBar()
|
|
||||||
|
|
||||||
Text("调试模式需要WIFI和另一台设备\n您可以一台设备开热点,另一台设备连入\n满足以上外部条件后, 本机需要开启以下服务")
|
|
||||||
|
|
||||||
TextSwitch("HTTP服务(需WIFI)", httpServerRunning) {
|
|
||||||
if (it) {
|
|
||||||
HttpService.start()
|
|
||||||
} else {
|
|
||||||
HttpService.stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TextSwitch("截屏服务", screenshotRunning) {
|
|
||||||
if (it) {
|
|
||||||
scope.launch {
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TextSwitch("无障碍服务", gkdAccessRunning) {
|
|
||||||
if (it) {
|
|
||||||
scope.launch {
|
|
||||||
ToastUtils.showShort("请先启动无障碍服务")
|
|
||||||
delay(500)
|
|
||||||
val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
|
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ToastUtils.showLong("无障碍服务不可在调试模式中关闭")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (debugAvailable && serverUrl != null) {
|
|
||||||
Text("调试模式可用, 请使用同一局域网的另一台设备打开链接")
|
|
||||||
SelectionContainer {
|
|
||||||
Text("长按可复制: " + serverUrl!!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
50
app/src/main/java/li/songe/gkd/ui/ImagePreviewPage.kt
Normal file
50
app/src/main/java/li/songe/gkd/ui/ImagePreviewPage.kt
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package li.songe.gkd.ui
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
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.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
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
|
||||||
|
|
||||||
|
@RootNavGraph
|
||||||
|
@Destination
|
||||||
|
@Composable
|
||||||
|
fun ImagePreviewPage(
|
||||||
|
filePath: String?
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current as ComponentActivity
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
var bitmap by remember {
|
||||||
|
mutableStateOf<Bitmap?>(null)
|
||||||
|
}
|
||||||
|
LaunchedEffectTry {
|
||||||
|
if (filePath != null) {
|
||||||
|
bitmap = withContext(IO) { BitmapFactory.decodeFile(filePath) }
|
||||||
|
} else {
|
||||||
|
ToastUtils.showShort("图片路径缺失")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bitmap?.let { bitmapVal ->
|
||||||
|
Image(
|
||||||
|
bitmap = bitmapVal.asImageBitmap(),
|
||||||
|
contentDescription = null,
|
||||||
|
Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
13
app/src/main/java/li/songe/gkd/ui/RecordPage.kt
Normal file
13
app/src/main/java/li/songe/gkd/ui/RecordPage.kt
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package li.songe.gkd.ui
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
|
import com.ramcosta.composedestinations.annotation.RootNavGraph
|
||||||
|
import li.songe.gkd.db.DbSet
|
||||||
|
|
||||||
|
@RootNavGraph
|
||||||
|
@Destination
|
||||||
|
@Composable
|
||||||
|
fun RecordPage() {
|
||||||
|
DbSet.triggerLogDao
|
||||||
|
}
|
163
app/src/main/java/li/songe/gkd/ui/SnapshotPage.kt
Normal file
163
app/src/main/java/li/songe/gkd/ui/SnapshotPage.kt
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
package li.songe.gkd.ui
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
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.Box
|
||||||
|
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.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
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.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
|
import com.ramcosta.composedestinations.annotation.RootNavGraph
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
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
|
||||||
|
|
||||||
|
@RootNavGraph
|
||||||
|
@Destination
|
||||||
|
@Composable
|
||||||
|
fun SnapshotPage() {
|
||||||
|
val context = LocalContext.current as ComponentActivity
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
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
|
||||||
|
.fillMaxWidth()
|
||||||
|
.border(BorderStroke(1.dp, Color.Black))
|
||||||
|
.clickable {
|
||||||
|
selectedSnapshot = snapshots[i]
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
Text(
|
||||||
|
text = Singleton.simpleDateFormat.format(snapshots[i].id),
|
||||||
|
fontFamily = FontFamily.Monospace
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(10.dp))
|
||||||
|
Text(text = snapshots[i].appName ?: "")
|
||||||
|
}
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selectedSnapshot?.let { snapshot ->
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = { selectedSnapshot = null }
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.width(200.dp)
|
||||||
|
.background(Color.White)
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
val modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp)
|
||||||
|
Text(
|
||||||
|
text = "查看", modifier = Modifier
|
||||||
|
.clickable(onClick = scope.launchAsFn {
|
||||||
|
// router.navigate(
|
||||||
|
// ImagePreviewPage,
|
||||||
|
// SnapshotExt.getScreenshotPath(snapshot.id)
|
||||||
|
// )
|
||||||
|
selectedSnapshot = null
|
||||||
|
})
|
||||||
|
.then(modifier)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "分享", modifier = Modifier
|
||||||
|
.clickable(onClick = scope.launchAsFn {
|
||||||
|
val zipFile = SnapshotExt.getSnapshotZipFile(snapshot.id)
|
||||||
|
val uri = FileProvider.getUriForFile(
|
||||||
|
context,
|
||||||
|
"${context.packageName}.provider",
|
||||||
|
zipFile
|
||||||
|
)
|
||||||
|
val intent = Intent().apply {
|
||||||
|
action = Intent.ACTION_SEND
|
||||||
|
putExtra(Intent.EXTRA_STREAM, uri)
|
||||||
|
type = "application/zip"
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
}
|
||||||
|
context.startActivity(Intent.createChooser(intent, "分享zip文件"))
|
||||||
|
})
|
||||||
|
.then(modifier)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "删除", modifier = Modifier
|
||||||
|
.clickable(onClick = scope.launchAsFn {
|
||||||
|
DbSet.snapshotDao.delete(snapshot)
|
||||||
|
withContext(IO) {
|
||||||
|
SnapshotExt.remove(snapshot.id)
|
||||||
|
}
|
||||||
|
selectedSnapshot = null
|
||||||
|
})
|
||||||
|
.then(modifier)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,162 +1,96 @@
|
||||||
package li.songe.gkd.ui
|
package li.songe.gkd.ui
|
||||||
|
|
||||||
import android.content.pm.ApplicationInfo
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.wrapContentSize
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
|
||||||
import androidx.core.content.res.ResourcesCompat
|
|
||||||
import com.google.accompanist.placeholder.PlaceholderHighlight
|
import com.google.accompanist.placeholder.PlaceholderHighlight
|
||||||
import com.google.accompanist.placeholder.material.fade
|
import com.google.accompanist.placeholder.material.fade
|
||||||
import com.google.accompanist.placeholder.material.placeholder
|
import com.google.accompanist.placeholder.material.placeholder
|
||||||
|
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.Dispatchers.IO
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.flow.cancellable
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import li.songe.gkd.R
|
import li.songe.gkd.data.SubsConfig
|
||||||
|
import li.songe.gkd.data.SubsItem
|
||||||
import li.songe.gkd.data.SubscriptionRaw
|
import li.songe.gkd.data.SubscriptionRaw
|
||||||
import li.songe.gkd.db.table.SubsConfig
|
import li.songe.gkd.data.getAppInfo
|
||||||
import li.songe.gkd.db.table.SubsItem
|
import li.songe.gkd.db.DbSet
|
||||||
import li.songe.gkd.db.util.Operator.eq
|
|
||||||
import li.songe.gkd.db.util.RoomX
|
|
||||||
import li.songe.gkd.ui.component.StatusBar
|
|
||||||
import li.songe.gkd.ui.component.SubsAppCard
|
import li.songe.gkd.ui.component.SubsAppCard
|
||||||
import li.songe.gkd.ui.component.SubsAppCardData
|
import li.songe.gkd.ui.component.SubsAppCardData
|
||||||
import li.songe.gkd.util.Ext.getApplicationInfoExt
|
import li.songe.gkd.ui.destinations.AppItemPageDestination
|
||||||
import li.songe.gkd.util.Status
|
import li.songe.gkd.utils.LaunchedEffectTry
|
||||||
import li.songe.gkd.util.ThrottleState
|
import li.songe.gkd.utils.LocalNavController
|
||||||
import li.songe.router.LocalRoute
|
import li.songe.gkd.utils.launchAsFn
|
||||||
import li.songe.router.LocalRouter
|
import li.songe.gkd.utils.rememberCache
|
||||||
import li.songe.router.Page
|
import li.songe.gkd.utils.useTask
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
|
|
||||||
val SubsPage = Page {
|
|
||||||
val router = LocalRouter.current
|
|
||||||
val subsItem = LocalRoute.current.data as SubsItem
|
|
||||||
|
|
||||||
|
@RootNavGraph
|
||||||
|
@Destination
|
||||||
|
@Composable
|
||||||
|
fun SubsPage(
|
||||||
|
subsItem: SubsItem
|
||||||
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val context = LocalContext.current
|
val navController = LocalNavController.current
|
||||||
|
|
||||||
var sub: SubscriptionRaw? by remember { mutableStateOf(null) }
|
var sub: SubscriptionRaw? by rememberCache { mutableStateOf(null) }
|
||||||
var subStatus: Status<SubscriptionRaw> by remember { mutableStateOf(Status.Progress()) }
|
var subsAppCards: List<SubsAppCardData>? by rememberCache { mutableStateOf(null) }
|
||||||
var subsAppCardDataList: List<SubsAppCardData>? by remember { mutableStateOf(null) }
|
|
||||||
val placeholderList: List<SubsAppCardData> = remember {
|
|
||||||
mutableListOf<SubsAppCardData>().apply {
|
|
||||||
repeat(5) {
|
|
||||||
add(
|
|
||||||
SubsAppCardData(
|
|
||||||
appName = "" + it,
|
|
||||||
icon = ResourcesCompat.getDrawable(
|
|
||||||
context.resources,
|
|
||||||
R.drawable.ic_app_2,
|
|
||||||
context.theme
|
|
||||||
)!!,
|
|
||||||
subsConfig = SubsConfig(
|
|
||||||
subsItemId = it.toLong(),
|
|
||||||
appId = "" + it
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var subsAppCardDataListStatus: Status<List<SubsAppCardData>> by remember {
|
|
||||||
mutableStateOf(Status.Progress())
|
|
||||||
}
|
|
||||||
|
|
||||||
val changeItemThrottle = ThrottleState.use(scope)
|
LaunchedEffectTry(Unit) {
|
||||||
|
scope.launchAsFn { }
|
||||||
LaunchedEffect(Unit) {
|
val newSub = if (sub === null) {
|
||||||
val st = System.currentTimeMillis()
|
SubscriptionRaw.parse5(subsItem.subsFile.readText()).apply {
|
||||||
val file = File(subsItem.filePath)
|
withContext(IO) {
|
||||||
if (!(file.exists() && file.isFile)) {
|
apps.forEach {
|
||||||
subStatus = Status.Error("在本地存储没有找到订阅文件")
|
getAppInfo(it.id)
|
||||||
return@LaunchedEffect
|
|
||||||
}
|
}
|
||||||
val rawText = try {
|
|
||||||
withContext(IO) { file.readText() }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
subStatus = Status.Error("读取文件失败:$e")
|
|
||||||
return@LaunchedEffect
|
|
||||||
}
|
}
|
||||||
val newSub = try {
|
|
||||||
SubscriptionRaw.parse5(rawText)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
subStatus = Status.Error("序列化失败:$e")
|
|
||||||
return@LaunchedEffect
|
|
||||||
}
|
}
|
||||||
subStatus = Status.Success(newSub)
|
} else {
|
||||||
|
sub!!
|
||||||
val mutableSet =
|
}
|
||||||
RoomX.select { (SubsConfig::type eq SubsConfig.AppType) and (SubsConfig::subsItemId eq subsItem.id) }
|
sub = newSub
|
||||||
.toMutableSet()
|
DbSet.subsConfigDao.queryAppTypeConfig(subsItem.id).flowOn(IO).cancellable().collect {
|
||||||
|
val mutableSet = it.toMutableSet()
|
||||||
val packageManager = context.packageManager
|
val newSubsAppCards = newSub.apps.map { appRaw ->
|
||||||
val defaultIcon = ResourcesCompat.getDrawable(
|
|
||||||
context.resources,
|
|
||||||
R.drawable.ic_app_2,
|
|
||||||
context.theme
|
|
||||||
)!!
|
|
||||||
val defaultName = "-"
|
|
||||||
val newSubsAppCardDataList = (subStatus as Status.Success).value.apps.map { appRaw ->
|
|
||||||
mutableSet.firstOrNull { v ->
|
mutableSet.firstOrNull { v ->
|
||||||
v.appId == appRaw.id
|
v.appId == appRaw.id
|
||||||
}.apply {
|
}.apply {
|
||||||
if (this != null) {
|
|
||||||
mutableSet.remove(this)
|
mutableSet.remove(this)
|
||||||
}
|
|
||||||
} ?: SubsConfig(
|
} ?: SubsConfig(
|
||||||
subsItemId = subsItem.id,
|
subsItemId = subsItem.id,
|
||||||
appId = appRaw.id,
|
appId = appRaw.id,
|
||||||
type = SubsConfig.AppType
|
type = SubsConfig.AppType
|
||||||
)
|
)
|
||||||
}.map { subsConfig ->
|
}.mapIndexed { index, subsConfig ->
|
||||||
async(IO) {
|
SubsAppCardData(
|
||||||
val info: ApplicationInfo = try {
|
subsConfig,
|
||||||
packageManager.getApplicationInfoExt(
|
newSub.apps[index]
|
||||||
subsConfig.appId,
|
|
||||||
PackageManager.GET_META_DATA
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
return@async SubsAppCardData(
|
|
||||||
defaultName,
|
|
||||||
defaultIcon,
|
|
||||||
subsConfig
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return@async SubsAppCardData(
|
subsAppCards = newSubsAppCards
|
||||||
packageManager.getApplicationLabel(info).toString(),
|
|
||||||
packageManager.getApplicationIcon(info),
|
|
||||||
subsConfig
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}.awaitAll()
|
}
|
||||||
subsAppCardDataListStatus = Status.Success(newSubsAppCardDataList)
|
|
||||||
delay(400 - (System.currentTimeMillis() - st))
|
val openAppPage = scope.useTask().launchAsFn<SubsAppCardData> {
|
||||||
sub = newSub
|
navController.navigate(AppItemPageDestination(it.appRaw, it.subsConfig))
|
||||||
subsAppCardDataList = newSubsAppCardDataList
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
|
@ -165,8 +99,6 @@ val SubsPage = Page {
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
Column {
|
|
||||||
StatusBar()
|
|
||||||
val textModifier = Modifier
|
val textModifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.placeholder(visible = sub == null, highlight = PlaceholderHighlight.fade())
|
.placeholder(visible = sub == null, highlight = PlaceholderHighlight.fade())
|
||||||
|
@ -188,90 +120,37 @@ val SubsPage = Page {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
subsAppCards?.let { subsAppCardsVal ->
|
||||||
subsAppCardDataList?.let { cardDataList ->
|
items(subsAppCardsVal.size) { i ->
|
||||||
items(cardDataList.size) { i ->
|
|
||||||
AnimatedVisibility(visible = true) {
|
|
||||||
Box(modifier = Modifier
|
|
||||||
.wrapContentSize()
|
|
||||||
.clickable {
|
|
||||||
router.navigate(
|
|
||||||
AppItemPage,
|
|
||||||
AppItemPageParams(
|
|
||||||
sub?.apps?.get(i)!!,
|
|
||||||
cardDataList[i].subsConfig,
|
|
||||||
cardDataList[i].appName
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}) {
|
|
||||||
SubsAppCard(
|
SubsAppCard(
|
||||||
sub = cardDataList[i],
|
sub = subsAppCardsVal[i],
|
||||||
onValueChange = changeItemThrottle.invoke { enable ->
|
onClick = {
|
||||||
val newItem = cardDataList[i].subsConfig.copy(
|
openAppPage(subsAppCardsVal[i])
|
||||||
|
},
|
||||||
|
onValueChange = scope.launchAsFn { enable ->
|
||||||
|
val newItem = subsAppCardsVal[i].subsConfig.copy(
|
||||||
enable = enable
|
enable = enable
|
||||||
)
|
)
|
||||||
if (newItem.id == 0L) {
|
DbSet.subsConfigDao.insert(newItem)
|
||||||
RoomX.insert(newItem)
|
|
||||||
} else {
|
|
||||||
RoomX.update(newItem)
|
|
||||||
}
|
|
||||||
subsAppCardDataList = cardDataList.toMutableList().apply {
|
|
||||||
set(i, cardDataList[i].copy(subsConfig = newItem))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
// if (subsAppCards == null) {
|
||||||
}
|
// items(placeholderList.size) { i ->
|
||||||
if (subsAppCardDataList == null) {
|
// Box(
|
||||||
items(placeholderList.size) { i ->
|
// modifier = Modifier
|
||||||
Box(
|
// .wrapContentSize()
|
||||||
modifier = Modifier
|
// ) {
|
||||||
.wrapContentSize()
|
// SubsAppCard(loading = true, sub = placeholderList[i])
|
||||||
) {
|
// Text(text = "")
|
||||||
SubsAppCard(loading = true, sub = placeholderList[i])
|
// }
|
||||||
Text(text = "")
|
// }
|
||||||
}
|
// }
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
item(true) {
|
item(true) {
|
||||||
Spacer(modifier = Modifier.height(10.dp))
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (subStatus !is Status.Success || subsAppCardDataListStatus !is Status.Success) {
|
|
||||||
when (val s = subStatus) {
|
|
||||||
is Status.Success -> {
|
|
||||||
when (val s2 = subsAppCardDataListStatus) {
|
|
||||||
is Status.Error -> {
|
|
||||||
Dialog(onDismissRequest = { router.back() }) {
|
|
||||||
Text(text = s2.value.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is Status.Progress -> {
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is Status.Error -> {
|
|
||||||
Dialog(onDismissRequest = { router.back() }) {
|
|
||||||
Text(text = s.value.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is Status.Progress -> {
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
32
app/src/main/java/li/songe/gkd/ui/component/SnapshotCard.kt
Normal file
32
app/src/main/java/li/songe/gkd/ui/component/SnapshotCard.kt
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
package li.songe.gkd.ui.component
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SnapshotCard() {
|
||||||
|
Row {
|
||||||
|
Text(text = "06-02 20:47:48")
|
||||||
|
Spacer(modifier = Modifier.size(10.dp))
|
||||||
|
Text(text = "酷安")
|
||||||
|
Spacer(modifier = Modifier.size(10.dp))
|
||||||
|
Text(text = "查看")
|
||||||
|
Spacer(modifier = Modifier.size(10.dp))
|
||||||
|
Text(text = "分享")
|
||||||
|
Spacer(modifier = Modifier.size(10.dp))
|
||||||
|
Text(text = "删除")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun PreviewSnapshotCard() {
|
||||||
|
SnapshotCard()
|
||||||
|
}
|
|
@ -12,7 +12,7 @@ import com.blankj.utilcode.util.BarUtils
|
||||||
import com.blankj.utilcode.util.ConvertUtils
|
import com.blankj.utilcode.util.ConvertUtils
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun StatusBar(color: Color = Color.White) {
|
fun StatusBar(color: Color = Color.Transparent) {
|
||||||
Spacer(
|
Spacer(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.height(statusBarHeight)
|
.height(statusBarHeight)
|
||||||
|
|
|
@ -2,6 +2,7 @@ package li.songe.gkd.ui.component
|
||||||
|
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
@ -18,31 +19,43 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
||||||
import com.google.accompanist.placeholder.PlaceholderHighlight
|
import com.google.accompanist.placeholder.PlaceholderHighlight
|
||||||
import com.google.accompanist.placeholder.material.fade
|
import com.google.accompanist.placeholder.material.fade
|
||||||
import com.google.accompanist.placeholder.material.placeholder
|
import com.google.accompanist.placeholder.material.placeholder
|
||||||
import li.songe.gkd.db.table.SubsConfig
|
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
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SubsAppCard(
|
fun SubsAppCard(
|
||||||
loading: Boolean = false,
|
loading: Boolean = false,
|
||||||
sub: SubsAppCardData,
|
sub: SubsAppCardData,
|
||||||
|
onClick: (() -> Unit)? = null,
|
||||||
onValueChange: ((Boolean) -> Unit)? = null
|
onValueChange: ((Boolean) -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
|
val info = getAppInfo(sub.appRaw.id)
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.height(60.dp)
|
.height(60.dp)
|
||||||
|
.clickable {
|
||||||
|
onClick?.invoke()
|
||||||
|
}
|
||||||
.padding(4.dp),
|
.padding(4.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
Image(
|
Image(
|
||||||
painter = rememberDrawablePainter(sub.icon),
|
painter = if (info.icon != null) rememberDrawablePainter(info.icon) else painterResource(
|
||||||
|
SafeR.ic_app_2
|
||||||
|
),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
|
@ -60,7 +73,7 @@ fun SubsAppCard(
|
||||||
verticalArrangement = Arrangement.SpaceBetween
|
verticalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = sub.appName, maxLines = 1,
|
text = info.name ?: "-", maxLines = 1,
|
||||||
softWrap = false,
|
softWrap = false,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
@ -68,7 +81,7 @@ fun SubsAppCard(
|
||||||
.placeholder(loading, highlight = PlaceholderHighlight.fade())
|
.placeholder(loading, highlight = PlaceholderHighlight.fade())
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = sub.subsConfig.appId, maxLines = 1,
|
text = sub.appRaw.groups.size.toString() + "组规则", maxLines = 1,
|
||||||
softWrap = false,
|
softWrap = false,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
@ -88,8 +101,7 @@ fun SubsAppCard(
|
||||||
}
|
}
|
||||||
|
|
||||||
data class SubsAppCardData(
|
data class SubsAppCardData(
|
||||||
val appName: String,
|
|
||||||
val icon: Drawable,
|
|
||||||
val subsConfig: SubsConfig,
|
val subsConfig: SubsConfig,
|
||||||
|
val appRaw: SubscriptionRaw.AppRaw
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -21,15 +21,14 @@ import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import li.songe.gkd.R
|
import li.songe.gkd.data.SubsItem
|
||||||
import li.songe.gkd.db.table.SubsItem
|
import li.songe.gkd.utils.SafeR
|
||||||
import li.songe.gkd.util.Singleton
|
import li.songe.gkd.utils.Singleton
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SubsItemCard(
|
fun SubsItemCard(
|
||||||
subsItem: SubsItem,
|
subsItem: SubsItem,
|
||||||
onShareClick: (() -> Unit)? = null,
|
onShareClick: (() -> Unit)? = null,
|
||||||
onEditClick: (() -> Unit)? = null,
|
|
||||||
onDelClick: (() -> Unit)? = null,
|
onDelClick: (() -> Unit)? = null,
|
||||||
onRefreshClick: (() -> Unit)? = null,
|
onRefreshClick: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
|
@ -49,16 +48,25 @@ fun SubsItemCard(
|
||||||
softWrap = false,
|
softWrap = false,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
|
Row {
|
||||||
Text(
|
Text(
|
||||||
text = dateStr,
|
text = dateStr,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
softWrap = false,
|
softWrap = false,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.width(10.dp))
|
||||||
|
Text(
|
||||||
|
text = "版本:" + subsItem.version,
|
||||||
|
maxLines = 1,
|
||||||
|
softWrap = false,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.width(5.dp))
|
Spacer(modifier = Modifier.width(5.dp))
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.drawable.ic_refresh),
|
painter = painterResource(SafeR.ic_refresh),
|
||||||
contentDescription = "refresh",
|
contentDescription = "refresh",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable {
|
.clickable {
|
||||||
|
@ -69,7 +77,7 @@ fun SubsItemCard(
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(5.dp))
|
Spacer(modifier = Modifier.width(5.dp))
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.drawable.ic_share),
|
painter = painterResource(SafeR.ic_share),
|
||||||
contentDescription = "share",
|
contentDescription = "share",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable {
|
.clickable {
|
||||||
|
@ -80,18 +88,7 @@ fun SubsItemCard(
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(5.dp))
|
Spacer(modifier = Modifier.width(5.dp))
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.drawable.ic_create_round),
|
painter = painterResource(SafeR.ic_del),
|
||||||
contentDescription = "edit",
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable {
|
|
||||||
onEditClick?.invoke()
|
|
||||||
}
|
|
||||||
.padding(4.dp)
|
|
||||||
.size(20.dp)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(5.dp))
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.ic_del),
|
|
||||||
contentDescription = "edit",
|
contentDescription = "edit",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable {
|
.clickable {
|
||||||
|
@ -109,7 +106,6 @@ fun PreviewSubscriptionItemCard() {
|
||||||
Surface(modifier = Modifier.width(300.dp)) {
|
Surface(modifier = Modifier.width(300.dp)) {
|
||||||
SubsItemCard(
|
SubsItemCard(
|
||||||
SubsItem(
|
SubsItem(
|
||||||
filePath = "filepath",
|
|
||||||
updateUrl = "https://raw.githubusercontents.com/lisonge/gkd-subscription/main/src/ad-startup.gkd.json",
|
updateUrl = "https://raw.githubusercontents.com/lisonge/gkd-subscription/main/src/ad-startup.gkd.json",
|
||||||
name = "APP工具箱"
|
name = "APP工具箱"
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,33 +1,41 @@
|
||||||
package li.songe.gkd.ui.component
|
package li.songe.gkd.ui.component
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
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.width
|
||||||
import androidx.compose.material.Surface
|
import androidx.compose.material.Surface
|
||||||
import androidx.compose.material.Switch
|
import androidx.compose.material.Switch
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TextSwitch(
|
fun TextSwitch(
|
||||||
text: String,
|
name: String = "",
|
||||||
checked: Boolean,
|
desc: String = "",
|
||||||
|
checked: Boolean = true,
|
||||||
onCheckedChange: ((Boolean) -> Unit)? = null,
|
onCheckedChange: ((Boolean) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
val animatedColor = (
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Color(
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
(0xFF * (if (checked) 1f else .3f)).toInt()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Text(
|
Text(
|
||||||
text,
|
name,
|
||||||
color = animatedColor
|
fontSize = 18.sp
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
|
Text(
|
||||||
|
desc,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(10.dp))
|
||||||
Switch(
|
Switch(
|
||||||
checked,
|
checked,
|
||||||
onCheckedChange,
|
onCheckedChange,
|
||||||
|
@ -38,7 +46,7 @@ fun TextSwitch(
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun PreviewTextSwitch() {
|
fun PreviewTextSwitch() {
|
||||||
Surface {
|
Surface(modifier = Modifier.width(300.dp)) {
|
||||||
TextSwitch("text", true)
|
TextSwitch("隐藏后台", "在最近任务列表中隐藏", true)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,6 +2,7 @@ package li.songe.gkd.ui.home
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import li.songe.gkd.R
|
import li.songe.gkd.R
|
||||||
|
import li.songe.gkd.utils.SafeR
|
||||||
|
|
||||||
data class BottomNavItem(
|
data class BottomNavItem(
|
||||||
val label: String,
|
val label: String,
|
||||||
|
@ -13,12 +14,12 @@ data class BottomNavItem(
|
||||||
val BottomNavItems = listOf(
|
val BottomNavItems = listOf(
|
||||||
BottomNavItem(
|
BottomNavItem(
|
||||||
label = "订阅",
|
label = "订阅",
|
||||||
icon = R.drawable.ic_link,
|
icon = SafeR.ic_link,
|
||||||
route = "subscription"
|
route = "subscription"
|
||||||
),
|
),
|
||||||
BottomNavItem(
|
BottomNavItem(
|
||||||
label = "设置",
|
label = "设置",
|
||||||
icon = R.drawable.ic_cog,
|
icon = SafeR.ic_cog,
|
||||||
route = "settings"
|
route = "settings"
|
||||||
),
|
),
|
||||||
)
|
)
|
|
@ -1,47 +1,33 @@
|
||||||
package li.songe.gkd.ui.home
|
package li.songe.gkd.ui.home
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.Scaffold
|
import androidx.compose.material.Scaffold
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
import androidx.compose.ui.zIndex
|
import com.ramcosta.composedestinations.annotation.RootNavGraph
|
||||||
import li.songe.gkd.ui.component.StatusBar
|
import li.songe.gkd.utils.LocalStateCache
|
||||||
import li.songe.gkd.util.ModifierExt.noRippleClickable
|
import li.songe.gkd.utils.StateCache
|
||||||
import li.songe.router.Page
|
import li.songe.gkd.utils.rememberCache
|
||||||
|
|
||||||
val HomePage = Page {
|
@RootNavGraph(start = true)
|
||||||
var tabInt by remember { mutableStateOf(0) }
|
@Destination
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
@Composable
|
||||||
StatusBar()
|
fun HomePage() {
|
||||||
Scaffold(
|
var tabIndex by rememberCache { mutableStateOf(0) }
|
||||||
bottomBar = { BottomNavigationBar(tabInt) { tabInt = it } },
|
val subsStateCache = rememberCache { StateCache() }
|
||||||
content = { padding ->
|
val settingStateCache = rememberCache { StateCache() }
|
||||||
|
Scaffold(bottomBar = { BottomNavigationBar(tabIndex) { tabIndex = it } }, content = { padding ->
|
||||||
Box(modifier = Modifier.padding(padding)) {
|
Box(modifier = Modifier.padding(padding)) {
|
||||||
Box(
|
when (tabIndex) {
|
||||||
modifier = Modifier
|
0 -> CompositionLocalProvider(LocalStateCache provides subsStateCache) { SubscriptionManagePage() }
|
||||||
.fillMaxSize()
|
1 -> CompositionLocalProvider(LocalStateCache provides settingStateCache) { SettingsPage() }
|
||||||
.alpha(if (tabInt == 0) 1f else 0f)
|
|
||||||
.zIndex(if (tabInt == 0) 1f else 0f)
|
|
||||||
.noRippleClickable { }) {
|
|
||||||
SubscriptionManagePage()
|
|
||||||
}
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.alpha(if (tabInt == 1) 1f else 0f)
|
|
||||||
.zIndex(if (tabInt == 1) 1f else 0f)
|
|
||||||
.noRippleClickable { }) {
|
|
||||||
SettingsPage()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -17,6 +17,7 @@ import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import li.songe.gkd.R
|
import li.songe.gkd.R
|
||||||
|
import li.songe.gkd.utils.SafeR
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NativePage() {
|
fun NativePage() {
|
||||||
|
@ -28,7 +29,7 @@ fun NativePage() {
|
||||||
modifier = Modifier.height(40.dp)
|
modifier = Modifier.height(40.dp)
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.drawable.ic_app_2),
|
painter = painterResource(SafeR.ic_app_2),
|
||||||
contentDescription = "",
|
contentDescription = "",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package li.songe.gkd.ui.home
|
package li.songe.gkd.ui.home
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.BottomNavigation
|
import androidx.compose.material.BottomNavigation
|
||||||
import androidx.compose.material.BottomNavigationItem
|
import androidx.compose.material.BottomNavigationItem
|
||||||
|
@ -43,11 +44,13 @@ import androidx.compose.ui.unit.dp
|
||||||
@Composable
|
@Composable
|
||||||
fun BottomNavigationBar(tabInt: Int, onTabChange: ((Int) -> Unit)? = null) {
|
fun BottomNavigationBar(tabInt: Int, onTabChange: ((Int) -> Unit)? = null) {
|
||||||
BottomNavigation(
|
BottomNavigation(
|
||||||
backgroundColor = Color.White,
|
backgroundColor = Color.Transparent,
|
||||||
|
elevation = 0.dp
|
||||||
) {
|
) {
|
||||||
BottomNavItems.forEachIndexed { i, navItem ->
|
BottomNavItems.forEachIndexed { i, navItem ->
|
||||||
BottomNavigationItem(
|
BottomNavigationItem(
|
||||||
selected = i == tabInt,
|
selected = i == tabInt,
|
||||||
|
modifier = Modifier.background(Color.Transparent),
|
||||||
onClick = {
|
onClick = {
|
||||||
onTabChange?.invoke(i)
|
onTabChange?.invoke(i)
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,16 @@
|
||||||
package li.songe.gkd.ui.home
|
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.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
@ -10,21 +20,43 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import com.blankj.utilcode.util.LogUtils
|
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.MainActivity
|
||||||
import li.songe.gkd.R
|
import li.songe.gkd.accessibility.GkdAbService
|
||||||
import li.songe.gkd.ui.DebugPage
|
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.ui.component.TextSwitch
|
||||||
import li.songe.gkd.util.LocaleString.Companion.localeString
|
import li.songe.gkd.utils.Ext
|
||||||
import li.songe.gkd.util.Storage
|
import li.songe.gkd.utils.LocalLauncher
|
||||||
import li.songe.router.LocalRouter
|
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
|
@Composable
|
||||||
fun SettingsPage() {
|
fun SettingsPage() {
|
||||||
|
val context = LocalContext.current as MainActivity
|
||||||
|
val launcher = LocalLauncher.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.verticalScroll(
|
.verticalScroll(
|
||||||
|
@ -32,37 +64,154 @@ fun SettingsPage() {
|
||||||
)
|
)
|
||||||
.padding(20.dp, 0.dp)
|
.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 context = LocalContext.current as MainActivity
|
|
||||||
// val composableScope = rememberCoroutineScope()
|
val shizukuIsOk by usePollState { shizukuIsSafeOK() }
|
||||||
val router = LocalRouter.current
|
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) }
|
var enableService by remember { mutableStateOf(Storage.settings.enableService) }
|
||||||
TextSwitch(
|
|
||||||
text = "保持服务${(if (enableService) "开启" else "关闭")}",
|
Spacer(modifier = Modifier.height(5.dp))
|
||||||
|
TextSwitch(name = "服务开启",
|
||||||
|
desc = "保持服务开启,根据订阅规则匹配屏幕目标节点",
|
||||||
checked = enableService,
|
checked = enableService,
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
enableService = it
|
enableService = it
|
||||||
Storage.settings.commit {
|
Storage.settings.commit {
|
||||||
this.enableService = it
|
this.enableService = it
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
Spacer(modifier = Modifier.height(5.dp))
|
||||||
|
|
||||||
var excludeFromRecents by remember { mutableStateOf(Storage.settings.excludeFromRecents) }
|
var excludeFromRecents by remember { mutableStateOf(Storage.settings.excludeFromRecents) }
|
||||||
TextSwitch(
|
TextSwitch(name = "隐藏后台",
|
||||||
text = "在[最近任务]界面中隐藏本应用",
|
desc = "在[最近任务]界面中隐藏本应用",
|
||||||
checked = excludeFromRecents,
|
checked = excludeFromRecents,
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
excludeFromRecents = it
|
excludeFromRecents = it
|
||||||
Storage.settings.commit {
|
Storage.settings.commit {
|
||||||
this.excludeFromRecents = it
|
this.excludeFromRecents = it
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
Spacer(modifier = Modifier.height(5.dp))
|
||||||
|
|
||||||
var enableConsoleLogOut by remember { mutableStateOf(Storage.settings.enableConsoleLogOut) }
|
var enableConsoleLogOut by remember { mutableStateOf(Storage.settings.enableConsoleLogOut) }
|
||||||
TextSwitch(
|
TextSwitch(name = "日志输出",
|
||||||
text = "保持日志输出到控制台",
|
desc = "保持日志输出到控制台",
|
||||||
checked = enableConsoleLogOut,
|
checked = enableConsoleLogOut,
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
enableConsoleLogOut = it
|
enableConsoleLogOut = it
|
||||||
|
@ -70,24 +219,49 @@ fun SettingsPage() {
|
||||||
Storage.settings.commit {
|
Storage.settings.commit {
|
||||||
this.enableConsoleLogOut = it
|
this.enableConsoleLogOut = it
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
Spacer(modifier = Modifier.height(5.dp))
|
||||||
|
|
||||||
var notificationVisible by remember { mutableStateOf(Storage.settings.notificationVisible) }
|
var notificationVisible by remember { mutableStateOf(Storage.settings.notificationVisible) }
|
||||||
TextSwitch(text = "通知栏显示", checked = notificationVisible,
|
TextSwitch(name = "通知栏显示",
|
||||||
|
desc = "通知栏显示可以降低系统杀后台的概率",
|
||||||
|
checked = notificationVisible,
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
notificationVisible = it
|
notificationVisible = it
|
||||||
Storage.settings.commit {
|
Storage.settings.commit {
|
||||||
this.notificationVisible = it
|
this.notificationVisible = it
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
Spacer(modifier = Modifier.height(5.dp))
|
||||||
|
|
||||||
Button(onClick = {
|
var enableScreenshot by remember {
|
||||||
router.navigate(DebugPage)
|
mutableStateOf(Storage.settings.enableCaptureSystemScreenshot)
|
||||||
}) {
|
}
|
||||||
Text(text = "调试模式")
|
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 = "查看关于")
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(text = "多语言自动切换:" + localeString(R.string.app_name))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
package li.songe.gkd.ui.home
|
package li.songe.gkd.ui.home
|
||||||
|
|
||||||
|
import android.webkit.URLUtil
|
||||||
import androidx.compose.foundation.*
|
import androidx.compose.foundation.*
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
@ -19,59 +20,127 @@ import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import com.blankj.utilcode.util.ClipboardUtils
|
import com.blankj.utilcode.util.ClipboardUtils
|
||||||
import com.blankj.utilcode.util.PathUtils
|
|
||||||
import com.blankj.utilcode.util.ToastUtils
|
import com.blankj.utilcode.util.ToastUtils
|
||||||
import com.google.zxing.BarcodeFormat
|
import com.google.zxing.BarcodeFormat
|
||||||
|
import com.ramcosta.composedestinations.navigation.navigate
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
import io.ktor.client.statement.*
|
import io.ktor.client.statement.*
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import li.songe.gkd.R
|
import li.songe.gkd.data.SubsItem
|
||||||
import li.songe.gkd.data.SubscriptionRaw
|
import li.songe.gkd.data.SubscriptionRaw
|
||||||
import li.songe.gkd.db.table.SubsConfig
|
import li.songe.gkd.db.DbSet
|
||||||
import li.songe.gkd.db.table.SubsItem
|
|
||||||
import li.songe.gkd.db.util.Operator.eq
|
|
||||||
import li.songe.gkd.db.util.RoomX
|
|
||||||
import li.songe.gkd.hooks.useNavigateForQrcodeResult
|
|
||||||
import li.songe.gkd.ui.SubsPage
|
|
||||||
import li.songe.gkd.ui.component.SubsItemCard
|
import li.songe.gkd.ui.component.SubsItemCard
|
||||||
import li.songe.gkd.util.Ext.launchTry
|
import li.songe.gkd.ui.destinations.SubsPageDestination
|
||||||
import li.songe.gkd.util.Singleton
|
import li.songe.gkd.utils.LaunchedEffectTry
|
||||||
import li.songe.gkd.util.ThrottleState
|
import li.songe.gkd.utils.LocalNavController
|
||||||
import li.songe.router.LocalRouter
|
import li.songe.gkd.utils.SafeR
|
||||||
import java.io.File
|
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
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SubscriptionManagePage() {
|
fun SubscriptionManagePage() {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val router = LocalRouter.current
|
val navController = LocalNavController.current
|
||||||
|
|
||||||
var subItemList by remember { mutableStateOf(listOf<SubsItem>()) }
|
var subItems by rememberCache { mutableStateOf(listOf<SubsItem>()) }
|
||||||
var shareSubItem: SubsItem? by remember { mutableStateOf(null) }
|
var shareSubItem: SubsItem? by rememberCache { mutableStateOf(null) }
|
||||||
var shareQrcode: ImageBitmap? by remember { mutableStateOf(null) }
|
var shareQrcode: ImageBitmap? by rememberCache { mutableStateOf(null) }
|
||||||
var deleteSubItem: SubsItem? by remember { 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("") }
|
||||||
|
|
||||||
var showAddDialog by remember { mutableStateOf(false) }
|
|
||||||
var showLinkInputDialog by remember { mutableStateOf(false) }
|
|
||||||
val viewSubItemThrottle = ThrottleState.use(scope)
|
|
||||||
val editSubItemThrottle = ThrottleState.use(scope)
|
|
||||||
val refreshSubItemThrottle = ThrottleState.use(scope, 250)
|
|
||||||
val navigateForQrcodeResult = useNavigateForQrcodeResult()
|
val navigateForQrcodeResult = useNavigateForQrcodeResult()
|
||||||
|
|
||||||
var linkText by remember {
|
LaunchedEffectTry(Unit) {
|
||||||
mutableStateOf("")
|
DbSet.subsItemDao.query().flowOn(IO).collect {
|
||||||
|
subItems = it
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
val addSubs = scope.useTask(dialog = true).launchAsFn<List<String>> { urls ->
|
||||||
subItemList = RoomX.select<SubsItem>().sortedBy { it.index }
|
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("添加失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val updateSubs = scope.useTask(dialog = true).launchAsFn<List<SubsItem>> { oldItems ->
|
||||||
|
if (oldItems.isEmpty()) return@launchAsFn
|
||||||
|
onChangeLoading(true)
|
||||||
|
val newItems = oldItems.mapNotNull { oldItem ->
|
||||||
|
try {
|
||||||
|
val subscriptionRaw = SubscriptionRaw.parse5(
|
||||||
|
Singleton.client.get(oldItem.updateUrl).bodyAsText()
|
||||||
|
)
|
||||||
|
if (subscriptionRaw.version <= oldItem.version) {
|
||||||
|
return@mapNotNull null
|
||||||
|
}
|
||||||
|
val newItem = oldItem.copy(
|
||||||
|
updateUrl = subscriptionRaw.updateUrl ?: oldItem.updateUrl,
|
||||||
|
name = subscriptionRaw.name,
|
||||||
|
mtime = System.currentTimeMillis(),
|
||||||
|
version = subscriptionRaw.version
|
||||||
|
)
|
||||||
|
withContext(IO) {
|
||||||
|
newItem.subsFile.writeText(
|
||||||
|
SubscriptionRaw.stringify(
|
||||||
|
subscriptionRaw
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
newItem
|
||||||
|
} catch (e: Exception) {
|
||||||
|
ToastUtils.showShort(e.message)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (newItems.isEmpty()) {
|
||||||
|
ToastUtils.showShort("暂无更新")
|
||||||
|
} else {
|
||||||
|
DbSet.subsItemDao.update(*newItems.toTypedArray())
|
||||||
|
ToastUtils.showShort("更新 ${newItems.size} 条订阅")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier.fillMaxHeight()
|
verticalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier.fillMaxHeight()
|
||||||
) {
|
) {
|
||||||
item(subItemList) {
|
item(subItems) {
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
@ -79,11 +148,17 @@ fun SubscriptionManagePage() {
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(10.dp, 0.dp)
|
.padding(10.dp, 0.dp)
|
||||||
) {
|
) {
|
||||||
|
if (subItems.isEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
text = "共有${subItemList.size}条订阅,激活:${subItemList.count { it.enable }},禁用:${subItemList.count { !it.enable }}",
|
text = "暂无订阅",
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = "共有${subItems.size}条订阅,激活:${subItems.count { it.enable }},禁用:${subItems.count { !it.enable }}",
|
||||||
|
)
|
||||||
|
}
|
||||||
Row {
|
Row {
|
||||||
Image(painter = painterResource(R.drawable.ic_add),
|
Image(painter = painterResource(SafeR.ic_add),
|
||||||
contentDescription = "",
|
contentDescription = "",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable {
|
.clickable {
|
||||||
|
@ -91,98 +166,51 @@ fun SubscriptionManagePage() {
|
||||||
}
|
}
|
||||||
.padding(4.dp)
|
.padding(4.dp)
|
||||||
.size(25.dp))
|
.size(25.dp))
|
||||||
Image(painter = painterResource(R.drawable.ic_refresh),
|
Image(
|
||||||
|
painter = painterResource(SafeR.ic_refresh),
|
||||||
contentDescription = "",
|
contentDescription = "",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable {
|
.clickable(onClick = {
|
||||||
scope.launchTry {
|
updateSubs(subItems)
|
||||||
subItemList.mapIndexed { i, oldItem ->
|
})
|
||||||
val subscriptionRaw = SubscriptionRaw.parse5(
|
|
||||||
Singleton.client
|
|
||||||
.get(oldItem.updateUrl)
|
|
||||||
.bodyAsText()
|
|
||||||
)
|
|
||||||
if (subscriptionRaw.version <= oldItem.version) {
|
|
||||||
ToastUtils.showShort("暂无更新:${oldItem.name}")
|
|
||||||
return@mapIndexed
|
|
||||||
}
|
|
||||||
val newItem = oldItem.copy(
|
|
||||||
updateUrl = subscriptionRaw.updateUrl
|
|
||||||
?: oldItem.updateUrl,
|
|
||||||
name = subscriptionRaw.name,
|
|
||||||
mtime = System.currentTimeMillis(),
|
|
||||||
version = subscriptionRaw.version
|
|
||||||
)
|
|
||||||
RoomX.update(newItem)
|
|
||||||
File(newItem.filePath).writeText(
|
|
||||||
SubscriptionRaw.stringify(
|
|
||||||
subscriptionRaw
|
|
||||||
)
|
|
||||||
)
|
|
||||||
ToastUtils.showShort("更新成功:${newItem.name}")
|
|
||||||
subItemList = subItemList
|
|
||||||
.toMutableList()
|
|
||||||
.also {
|
|
||||||
it[i] = newItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(4.dp)
|
.padding(4.dp)
|
||||||
.size(25.dp))
|
.size(25.dp)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items(subItemList.size) { i ->
|
items(subItems.size) { i ->
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.animateItemPlacement()
|
.animateItemPlacement()
|
||||||
.padding(vertical = 3.dp, horizontal = 8.dp)
|
.padding(vertical = 3.dp, horizontal = 8.dp)
|
||||||
.clickable(onClick = { router.navigate(SubsPage, subItemList[i]) }),
|
.combinedClickable(
|
||||||
|
onClick = scope
|
||||||
|
.useTask()
|
||||||
|
.launchAsFn {
|
||||||
|
navController.navigate(SubsPageDestination(subItems[i]))
|
||||||
|
}, onLongClick = {
|
||||||
|
if (subItems.size > 1) {
|
||||||
|
moveSubItem = subItems[i]
|
||||||
|
}
|
||||||
|
}),
|
||||||
elevation = 0.dp,
|
elevation = 0.dp,
|
||||||
border = BorderStroke(1.dp, Color(0xfff6f6f6)),
|
border = BorderStroke(1.dp, Color(0xfff6f6f6)),
|
||||||
shape = RoundedCornerShape(8.dp),
|
shape = RoundedCornerShape(8.dp),
|
||||||
) {
|
) {
|
||||||
SubsItemCard(subItemList[i], onShareClick = {
|
SubsItemCard(subItems[i], onShareClick = {
|
||||||
shareSubItem = subItemList[i]
|
shareSubItem = subItems[i]
|
||||||
}, onEditClick = editSubItemThrottle.invoke {
|
|
||||||
}, onDelClick = {
|
}, onDelClick = {
|
||||||
deleteSubItem = subItemList[i]
|
deleteSubItem = subItems[i]
|
||||||
}, onRefreshClick = refreshSubItemThrottle.invoke {
|
}, onRefreshClick = {
|
||||||
val oldItem = subItemList[i]
|
updateSubs(listOf(subItems[i]))
|
||||||
val subscriptionRaw = SubscriptionRaw.parse5(
|
|
||||||
Singleton.client.get(oldItem.updateUrl).bodyAsText()
|
|
||||||
)
|
|
||||||
if (subscriptionRaw.version <= oldItem.version) {
|
|
||||||
ToastUtils.showShort("暂无更新:${oldItem.name}")
|
|
||||||
return@invoke
|
|
||||||
}
|
|
||||||
val newItem = oldItem.copy(
|
|
||||||
updateUrl = subscriptionRaw.updateUrl
|
|
||||||
?: oldItem.updateUrl,
|
|
||||||
name = subscriptionRaw.name,
|
|
||||||
mtime = System.currentTimeMillis(),
|
|
||||||
version = subscriptionRaw.version
|
|
||||||
)
|
|
||||||
RoomX.update(newItem)
|
|
||||||
withContext(IO) {
|
|
||||||
File(newItem.filePath).writeText(SubscriptionRaw.stringify(subscriptionRaw))
|
|
||||||
}
|
|
||||||
subItemList = subItemList.toMutableList().also {
|
|
||||||
it[i] = newItem
|
|
||||||
}
|
|
||||||
ToastUtils.showShort("更新成功:${newItem.name}")
|
|
||||||
}.catch {
|
|
||||||
if (!it.message.isNullOrEmpty()) {
|
|
||||||
ToastUtils.showShort(it.message)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
shareSubItem?.let { _shareSubItem ->
|
shareSubItem?.let { shareSubItemVal ->
|
||||||
Dialog(onDismissRequest = { shareSubItem = null }) {
|
Dialog(onDismissRequest = { shareSubItem = null }) {
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
|
@ -191,26 +219,22 @@ fun SubscriptionManagePage() {
|
||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
) {
|
) {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
Text(text = "二维码",
|
Text(text = "二维码", modifier = Modifier
|
||||||
modifier = Modifier
|
|
||||||
.clickable {
|
.clickable {
|
||||||
shareQrcode = Singleton.barcodeEncoder
|
shareQrcode = Singleton.barcodeEncoder
|
||||||
.encodeBitmap(
|
.encodeBitmap(
|
||||||
_shareSubItem.updateUrl,
|
shareSubItemVal.updateUrl, BarcodeFormat.QR_CODE, 500, 500
|
||||||
BarcodeFormat.QR_CODE,
|
|
||||||
500,
|
|
||||||
500
|
|
||||||
)
|
)
|
||||||
.asImageBitmap()
|
.asImageBitmap()
|
||||||
shareSubItem = null
|
shareSubItem = null
|
||||||
}
|
}
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(8.dp))
|
.padding(8.dp))
|
||||||
Text(text = "导出至剪切板",
|
Text(text = "导出至剪切板", modifier = Modifier
|
||||||
modifier = Modifier
|
|
||||||
.clickable {
|
.clickable {
|
||||||
ClipboardUtils.copyText(_shareSubItem.updateUrl)
|
ClipboardUtils.copyText(shareSubItemVal.updateUrl)
|
||||||
shareSubItem = null
|
shareSubItem = null
|
||||||
|
ToastUtils.showShort("复制成功")
|
||||||
}
|
}
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(8.dp))
|
.padding(8.dp))
|
||||||
|
@ -219,36 +243,85 @@ fun SubscriptionManagePage() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
shareQrcode?.let { _shareQrcode ->
|
shareQrcode?.let { shareQrcodeVal ->
|
||||||
Dialog(onDismissRequest = { shareQrcode = null }) {
|
Dialog(onDismissRequest = { shareQrcode = null }) {
|
||||||
Image(
|
Image(
|
||||||
bitmap = _shareQrcode,
|
bitmap = shareQrcodeVal,
|
||||||
contentDescription = "qrcode",
|
contentDescription = "qrcode",
|
||||||
modifier = Modifier.size(400.dp)
|
modifier = Modifier.size(400.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
moveSubItem?.let { moveSubItemVal ->
|
||||||
|
Dialog(onDismissRequest = { moveSubItem = null }) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.width(200.dp)
|
||||||
|
.wrapContentHeight()
|
||||||
|
.background(Color.White)
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
if (subItems.firstOrNull() != moveSubItemVal) {
|
||||||
|
Text(
|
||||||
|
text = "上移",
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(
|
||||||
|
onClick = scope
|
||||||
|
.useTask()
|
||||||
|
.launchAsFn {
|
||||||
|
val lastItem =
|
||||||
|
subItems[subItems.indexOf(moveSubItemVal) - 1]
|
||||||
|
DbSet.subsItemDao.update(
|
||||||
|
lastItem.copy(
|
||||||
|
order = moveSubItemVal.order
|
||||||
|
),
|
||||||
|
moveSubItemVal.copy(
|
||||||
|
order = lastItem.order
|
||||||
|
)
|
||||||
|
)
|
||||||
|
moveSubItem = null
|
||||||
|
})
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (subItems.lastOrNull() != moveSubItemVal) {
|
||||||
|
Text(
|
||||||
|
text = "下移",
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(
|
||||||
|
onClick = scope
|
||||||
|
.useTask()
|
||||||
|
.launchAsFn {
|
||||||
|
val nextItem =
|
||||||
|
subItems[subItems.indexOf(moveSubItemVal) + 1]
|
||||||
|
DbSet.subsItemDao.update(
|
||||||
|
nextItem.copy(
|
||||||
|
order = moveSubItemVal.order
|
||||||
|
),
|
||||||
|
moveSubItemVal.copy(
|
||||||
|
order = nextItem.order
|
||||||
|
)
|
||||||
|
)
|
||||||
|
moveSubItem = null
|
||||||
|
})
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val delSubItemThrottle = ThrottleState.use(scope)
|
|
||||||
if (deleteSubItem != null) {
|
deleteSubItem?.let { deleteSubItemVal ->
|
||||||
AlertDialog(onDismissRequest = { deleteSubItem = null },
|
AlertDialog(onDismissRequest = { deleteSubItem = null },
|
||||||
title = { Text(text = "是否删除该项") },
|
title = { Text(text = "是否删除该项") },
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
Button(onClick = delSubItemThrottle.invoke {
|
Button(onClick = scope.launchAsFn {
|
||||||
if (deleteSubItem == null) return@invoke
|
deleteSubItemVal.removeAssets()
|
||||||
deleteSubItem?.let {
|
|
||||||
RoomX.delete(it)
|
|
||||||
RoomX.delete { SubsConfig::subsItemId eq it.id }
|
|
||||||
}
|
|
||||||
withContext(IO) {
|
|
||||||
try {
|
|
||||||
File(deleteSubItem!!.filePath).delete()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
subItemList = subItemList.toMutableList().also { it.remove(deleteSubItem) }
|
|
||||||
deleteSubItem = null
|
deleteSubItem = null
|
||||||
}) {
|
}) {
|
||||||
Text("是")
|
Text("是")
|
||||||
|
@ -262,25 +335,40 @@ fun SubscriptionManagePage() {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showAddDialog) {
|
if (showAddDialog) {
|
||||||
val clickQrcodeThrottle = ThrottleState.use(scope)
|
|
||||||
Dialog(onDismissRequest = { showAddDialog = false }) {
|
Dialog(onDismissRequest = { showAddDialog = false }) {
|
||||||
Box(
|
Column(
|
||||||
Modifier
|
modifier = Modifier
|
||||||
.width(250.dp)
|
.width(250.dp)
|
||||||
.background(Color.White)
|
.background(Color.White)
|
||||||
.padding(8.dp)
|
.padding(8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
Column(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(
|
||||||
text = "二维码", modifier = Modifier
|
text = "二维码", modifier = Modifier
|
||||||
.clickable(onClick = clickQrcodeThrottle.invoke {
|
.clickable(onClick = scope.launchAsFn {
|
||||||
showAddDialog = false
|
showAddDialog = false
|
||||||
val qrCode = navigateForQrcodeResult()
|
val qrCode = navigateForQrcodeResult()
|
||||||
val contents = qrCode.contents
|
val contents = qrCode.contents
|
||||||
if (contents != null) {
|
if (contents != null) {
|
||||||
showLinkInputDialog = true
|
showLinkDialog = true
|
||||||
linkText = contents
|
link = contents
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
@ -288,7 +376,7 @@ fun SubscriptionManagePage() {
|
||||||
)
|
)
|
||||||
Text(text = "链接", modifier = Modifier
|
Text(text = "链接", modifier = Modifier
|
||||||
.clickable {
|
.clickable {
|
||||||
showLinkInputDialog = true
|
showLinkDialog = true
|
||||||
showAddDialog = false
|
showAddDialog = false
|
||||||
}
|
}
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
@ -296,64 +384,30 @@ fun SubscriptionManagePage() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
LaunchedEffect(showLinkDialog) {
|
||||||
|
if (!showLinkDialog) {
|
||||||
|
link = ""
|
||||||
}
|
}
|
||||||
if (showLinkInputDialog) {
|
}
|
||||||
Dialog(onDismissRequest = { showLinkInputDialog = false;linkText = "" }) {
|
if (showLinkDialog) {
|
||||||
|
Dialog(onDismissRequest = { showLinkDialog = false }) {
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.width(250.dp)
|
.width(300.dp)
|
||||||
.background(Color.White)
|
.background(Color.White)
|
||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
) {
|
) {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
Text(text = "请输入订阅链接")
|
Text(text = "请输入订阅链接")
|
||||||
TextField(
|
TextField(
|
||||||
value = linkText,
|
value = link, onValueChange = { link = it.trim() }, singleLine = true
|
||||||
onValueChange = { linkText = it },
|
|
||||||
singleLine = true
|
|
||||||
)
|
)
|
||||||
Button(onClick = {
|
Button(onClick = {
|
||||||
showLinkInputDialog = false
|
addSubs(listOf(link))
|
||||||
if (subItemList.any { it.updateUrl == linkText }) {
|
showLinkDialog = false
|
||||||
ToastUtils.showShort("该链接已经添加过")
|
|
||||||
return@Button
|
|
||||||
}
|
|
||||||
scope.launch {
|
|
||||||
try {
|
|
||||||
val text = Singleton.client.get(linkText).bodyAsText()
|
|
||||||
val subscriptionRaw = SubscriptionRaw.parse5(text)
|
|
||||||
File(
|
|
||||||
PathUtils.getExternalAppFilesPath()
|
|
||||||
.plus("/subscription/")
|
|
||||||
).apply {
|
|
||||||
if (!exists()) {
|
|
||||||
mkdir()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val file = File(
|
|
||||||
PathUtils.getExternalAppFilesPath()
|
|
||||||
.plus("/subscription/")
|
|
||||||
.plus(System.currentTimeMillis())
|
|
||||||
.plus(".json")
|
|
||||||
)
|
|
||||||
withContext(IO) {
|
|
||||||
file.writeText(text)
|
|
||||||
}
|
|
||||||
val tempItem = SubsItem(
|
|
||||||
updateUrl = subscriptionRaw.updateUrl ?: linkText,
|
|
||||||
filePath = file.absolutePath,
|
|
||||||
name = subscriptionRaw.name,
|
|
||||||
version = subscriptionRaw.version
|
|
||||||
)
|
|
||||||
val newItem = tempItem.copy(
|
|
||||||
id = RoomX.insert(tempItem)[0]
|
|
||||||
)
|
|
||||||
subItemList = subItemList.toMutableList().apply { add(newItem) }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
ToastUtils.showShort(e.message ?: "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) {
|
}) {
|
||||||
Text(text = "添加")
|
Text(text = "添加")
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ package li.songe.gkd.ui.theme
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
val Purple200 = Color(0xFFBB86FC)
|
val Purple200 = Color(0xFFf8f9f9)
|
||||||
val Purple500 = Color(0xFF6200EE)
|
val Purple500 = Color(0xFFf2f3f4)
|
||||||
val Purple700 = Color(0xFF3700B3)
|
val Purple700 = Color(0xFFe5e7e9)
|
||||||
val Teal200 = Color(0xFF03DAC5)
|
val Teal200 = Color(0xFF03DAC5)
|
|
@ -6,29 +6,12 @@ import androidx.compose.material.darkColors
|
||||||
import androidx.compose.material.lightColors
|
import androidx.compose.material.lightColors
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
||||||
private val DarkColorPalette = darkColors(
|
private val DarkColorPalette = darkColors()
|
||||||
primary = Purple200,
|
|
||||||
primaryVariant = Purple700,
|
|
||||||
secondary = Teal200
|
|
||||||
)
|
|
||||||
|
|
||||||
private val LightColorPalette = lightColors(
|
private val LightColorPalette = lightColors()
|
||||||
primary = Purple500,
|
|
||||||
primaryVariant = Purple700,
|
|
||||||
secondary = Teal200
|
|
||||||
|
|
||||||
/* Other default colors to override
|
|
||||||
background = Color.White,
|
|
||||||
surface = Color.White,
|
|
||||||
onPrimary = Color.White,
|
|
||||||
onSecondary = Color.Black,
|
|
||||||
onBackground = Color.Black,
|
|
||||||
onSurface = Color.Black,
|
|
||||||
*/
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MainTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) {
|
fun AppTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) {
|
||||||
val colors = if (darkTheme) {
|
val colors = if (darkTheme) {
|
||||||
DarkColorPalette
|
DarkColorPalette
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
package li.songe.gkd.util
|
|
||||||
|
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.os.LocaleList
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import li.songe.gkd.App
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class LocaleString(private vararg val localeList: Locale) : (Int) -> String {
|
|
||||||
private val languageContext by lazy {
|
|
||||||
App.context.createConfigurationContext(Configuration(App.context.resources.configuration).apply {
|
|
||||||
if (this@LocaleString.localeList.isNotEmpty()) {
|
|
||||||
setLocales(LocaleList(*this@LocaleString.localeList))
|
|
||||||
} else {
|
|
||||||
setLocales(App.context.resources.configuration.locales)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun invoke(@StringRes resId: Int) = languageContext.getString(resId)
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
other as LocaleString
|
|
||||||
if (!localeList.contentEquals(other.localeList)) return false
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
|
|
||||||
return localeList.contentHashCode()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
var localeString by mutableStateOf(LocaleString())
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
package li.songe.gkd.util
|
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.composed
|
|
||||||
|
|
||||||
object ModifierExt {
|
|
||||||
inline fun Modifier.noRippleClickable(crossinline onClick: () -> Unit): Modifier = composed {
|
|
||||||
clickable(indication = null,
|
|
||||||
interactionSource = remember { MutableInteractionSource() }) {
|
|
||||||
onClick()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
package li.songe.gkd.util
|
|
||||||
|
|
||||||
sealed class Status<out T> {
|
|
||||||
object Empty : Status<Nothing>()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param value 0f to 1f
|
|
||||||
*/
|
|
||||||
class Progress(val value: Float = 0f) : Status<Nothing>()
|
|
||||||
class Success<T>(val value: T) : Status<T>()
|
|
||||||
class Error(val value: Any?) : Status<Nothing>() {
|
|
||||||
// override fun toString(): String {
|
|
||||||
// val nullMsg = "未知错误"
|
|
||||||
// return when (value) {
|
|
||||||
// null -> nullMsg
|
|
||||||
// is String -> value
|
|
||||||
// is Exception -> value.message ?: nullMsg
|
|
||||||
// else -> value.toString()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,109 +0,0 @@
|
||||||
package li.songe.gkd.util
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class ThrottleState(
|
|
||||||
private val scope: CoroutineScope,
|
|
||||||
private val miniAwaitTime: Long = 200L,
|
|
||||||
val loading: Boolean = false,
|
|
||||||
private val onChangeLoading: (value: Boolean) -> Unit = {},
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
private lateinit var defaultFalseInstance: ThrottleState
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun use(scope: CoroutineScope, miniAwaitTime: Long = 0): ThrottleState {
|
|
||||||
var loading by remember { mutableStateOf(false) }
|
|
||||||
if (loading) {
|
|
||||||
if (!::defaultFalseInstance.isInitialized) {
|
|
||||||
defaultFalseInstance = ThrottleState(scope, miniAwaitTime, loading = true)
|
|
||||||
}
|
|
||||||
return defaultFalseInstance
|
|
||||||
}
|
|
||||||
return ThrottleState(scope, miniAwaitTime, loading = false) {
|
|
||||||
loading = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class CatchInvoke(
|
|
||||||
private val onChangeCatch: (catchFn: ((e: Exception) -> Unit)) -> Unit,
|
|
||||||
private val fn: () -> Unit,
|
|
||||||
) : () -> Unit {
|
|
||||||
override fun invoke() {
|
|
||||||
fn()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun catch(catchFn: ((e: Exception) -> Unit)): CatchInvoke {
|
|
||||||
onChangeCatch(catchFn)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class CatchInvoke1<T>(
|
|
||||||
private val onChangeCatch: (catchFn: ((e: Exception) -> Unit)) -> Unit,
|
|
||||||
private val fn: (T) -> Unit,
|
|
||||||
) : (T) -> Unit {
|
|
||||||
override fun invoke(t: T) {
|
|
||||||
fn(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun catch(catchFn: ((e: Exception) -> Unit)): CatchInvoke1<T> {
|
|
||||||
onChangeCatch(catchFn)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun invoke(
|
|
||||||
miniAwaitTime: Long = this.miniAwaitTime,
|
|
||||||
fn: suspend () -> Unit,
|
|
||||||
): CatchInvoke {
|
|
||||||
var catchFn = { e: Exception -> e.printStackTrace() }
|
|
||||||
return CatchInvoke({ catchFn = it }) fnWrapper@{
|
|
||||||
if (loading) return@fnWrapper
|
|
||||||
onChangeLoading(true)
|
|
||||||
scope.launch {
|
|
||||||
try {
|
|
||||||
fn()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
catchFn(e)
|
|
||||||
} finally {
|
|
||||||
delay(miniAwaitTime)
|
|
||||||
onChangeLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> invoke(
|
|
||||||
miniAwaitTime: Long = this.miniAwaitTime,
|
|
||||||
fn: suspend (T) -> Unit,
|
|
||||||
): CatchInvoke1<T> {
|
|
||||||
var catchFn = { e: Exception -> e.printStackTrace() }
|
|
||||||
return CatchInvoke1({ catchFn = it }) fnWrapper@{ t ->
|
|
||||||
if (loading) return@fnWrapper
|
|
||||||
onChangeLoading(true)
|
|
||||||
scope.launch {
|
|
||||||
try {
|
|
||||||
fn(t)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
catchFn(e)
|
|
||||||
} finally {
|
|
||||||
delay(miniAwaitTime)
|
|
||||||
onChangeLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
package li.songe.gkd.util
|
|
||||||
|
|
||||||
import android.content.res.Configuration
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import com.blankj.utilcode.util.ScreenUtils
|
|
||||||
|
|
||||||
object UseHook {
|
|
||||||
var screenWidth by mutableStateOf(ScreenUtils.getAppScreenWidth())
|
|
||||||
var screenHeight by mutableStateOf(ScreenUtils.getAppScreenHeight())
|
|
||||||
var screenOrientationIsLandscape by mutableStateOf(ScreenUtils.isLandscape())
|
|
||||||
// var locale by mutableStateOf(LanguageUtils.getSystemLanguage())
|
|
||||||
|
|
||||||
fun update(newConfig: Configuration) {
|
|
||||||
screenHeight = ScreenUtils.getAppScreenHeight()
|
|
||||||
screenWidth = ScreenUtils.getAppScreenWidth()
|
|
||||||
screenOrientationIsLandscape = ScreenUtils.isLandscape()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,8 @@
|
||||||
package li.songe.gkd.util
|
package li.songe.gkd.utils
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -8,24 +10,24 @@ import kotlinx.parcelize.Parcelize
|
||||||
*/
|
*/
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class AppSettings(
|
data class AppSettings(
|
||||||
var ctime: Long = System.currentTimeMillis(),
|
|
||||||
var mtime: Long = System.currentTimeMillis(),
|
|
||||||
var enableService: Boolean = true,
|
var enableService: Boolean = true,
|
||||||
var excludeFromRecents: Boolean = true,
|
var excludeFromRecents: Boolean = true,
|
||||||
var notificationVisible: Boolean = true,
|
var notificationVisible: Boolean = true,
|
||||||
var enableDebugServer: Boolean = false,
|
|
||||||
var httpServerPort: Int = 8888,
|
|
||||||
var enableConsoleLogOut: Boolean = true,
|
var enableConsoleLogOut: Boolean = true,
|
||||||
|
var enableCaptureSystemScreenshot: Boolean = true,
|
||||||
|
var httpServerPort: Int = 8888,
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
fun commit(block: AppSettings.() -> Unit) {
|
fun commit(block: AppSettings.() -> Unit) {
|
||||||
val backup = copy()
|
val backup = copy()
|
||||||
block.invoke(this)
|
block.invoke(this)
|
||||||
if (this != backup) {
|
if (this != backup) {
|
||||||
mtime = System.currentTimeMillis()
|
|
||||||
Storage.kv.encode(saveKey, this)
|
Storage.kv.encode(saveKey, this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val saveKey = "settings-v1"
|
const val saveKey = "settings-v2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val appSettingsFlow by lazy { MutableStateFlow(AppSettings()) }
|
50
app/src/main/java/li/songe/gkd/utils/ComposeExt.kt
Normal file
50
app/src/main/java/li/songe/gkd/utils/ComposeExt.kt
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package li.songe.gkd.utils
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import com.blankj.utilcode.util.ToastUtils
|
||||||
|
import com.dylanc.activityresult.launcher.StartActivityLauncher
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
|
||||||
|
|
||||||
|
val LocalLauncher =
|
||||||
|
compositionLocalOf<StartActivityLauncher> { error("not found StartActivityLauncher") }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun <T> usePollState(interval: Long = 1000L, getter: () -> T): MutableState<T> {
|
||||||
|
val mutableState = remember { mutableStateOf(getter()) }
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
while (isActive) {
|
||||||
|
delay(interval)
|
||||||
|
mutableState.value = getter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mutableState
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LaunchedEffectTry(
|
||||||
|
key1: Any? = null,
|
||||||
|
block: suspend CoroutineScope.() -> Unit
|
||||||
|
) {
|
||||||
|
LaunchedEffect(key1) {
|
||||||
|
try {
|
||||||
|
block()
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
ToastUtils.showShort(e.message ?: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
100
app/src/main/java/li/songe/gkd/utils/CoroutineExt.kt
Normal file
100
app/src/main/java/li/songe/gkd/utils/CoroutineExt.kt
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
package li.songe.gkd.utils
|
||||||
|
|
||||||
|
import com.blankj.utilcode.util.LogUtils
|
||||||
|
import com.blankj.utilcode.util.ToastUtils
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.CoroutineStart
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
import kotlin.coroutines.coroutineContext
|
||||||
|
|
||||||
|
|
||||||
|
fun CoroutineScope.launchWhile(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
block: suspend CoroutineScope.() -> Unit,
|
||||||
|
) = launch(context, start) {
|
||||||
|
while (isActive) {
|
||||||
|
block()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun CoroutineScope.launchWhileTry(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
interval: Long = 0,
|
||||||
|
block: suspend CoroutineScope.() -> Unit,
|
||||||
|
) = launch(context, start) {
|
||||||
|
while (isActive) {
|
||||||
|
try {
|
||||||
|
block()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
delay(interval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun CoroutineScope.launchTry(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
block: suspend CoroutineScope.() -> Unit,
|
||||||
|
) = launch(context, start) {
|
||||||
|
try {
|
||||||
|
block()
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
ToastUtils.showShort(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun CoroutineScope.launchAsFn(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
block: suspend CoroutineScope.() -> Unit,
|
||||||
|
): () -> Unit {
|
||||||
|
return {
|
||||||
|
launch(context, start) {
|
||||||
|
try {
|
||||||
|
block()
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
ToastUtils.showShort(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> CoroutineScope.launchAsFn(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
block: suspend CoroutineScope.(T) -> Unit,
|
||||||
|
): (T) -> Unit {
|
||||||
|
return {
|
||||||
|
launch(context, start) {
|
||||||
|
try {
|
||||||
|
block(it)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
ToastUtils.showShort(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun stopJob(): Nothing {
|
||||||
|
coroutineContext[Job]?.cancel()
|
||||||
|
delay(1)
|
||||||
|
error("stop failed")
|
||||||
|
}
|
||||||
|
|
79
app/src/main/java/li/songe/gkd/utils/DrawableDsl.kt
Normal file
79
app/src/main/java/li/songe/gkd/utils/DrawableDsl.kt
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
package li.songe.gkd.utils
|
||||||
|
|
||||||
|
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
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
fun createDrawable(block: () -> Unit) {
|
||||||
|
val s = materialIcon(name = "xx") {
|
||||||
|
addPath(addPathNodes(""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createVectorDrawable(block: () -> Unit) {
|
||||||
|
val path = Path().apply {
|
||||||
|
addPathNodes("").forEach {
|
||||||
|
it.isQuad
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val shapeDrawable = ShapeDrawable(RectShape())
|
||||||
|
shapeDrawable.apply {
|
||||||
|
|
||||||
|
addPathNodes("")[0].isCurve
|
||||||
|
}
|
||||||
|
// val r = Resources()
|
||||||
|
// Drawable.createFromXml()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val x = createDrawable {
|
||||||
|
// val p =
|
||||||
|
vector {
|
||||||
|
width = 24.dp
|
||||||
|
height = 24.dp
|
||||||
|
viewportWidth = 24F
|
||||||
|
viewportHeight = 24F
|
||||||
|
path {
|
||||||
|
width
|
||||||
|
fillColor = Color(0xFF000000)
|
||||||
|
pathData = "M20,11H7.83l5.59,-5.59L12,4l-8,8l8,8l1.41,-1.41L7.83,13H20v-2z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VectorType {
|
||||||
|
var width: Dp
|
||||||
|
var height: Dp
|
||||||
|
var viewportWidth: Float
|
||||||
|
var viewportHeight: Float
|
||||||
|
}
|
||||||
|
|
||||||
|
fun vector(block: VectorType.() -> Unit) {}
|
||||||
|
|
||||||
|
interface PathType {
|
||||||
|
var fillColor: Color
|
||||||
|
var pathData: String
|
||||||
|
}
|
||||||
|
|
||||||
|
fun path(block: PathType.() -> Unit) {}
|
||||||
|
|
||||||
|
fun testDrawable() {
|
||||||
|
|
||||||
|
val s2 = vector {
|
||||||
|
width = 24.dp
|
||||||
|
height = 24.dp
|
||||||
|
viewportWidth = 24F
|
||||||
|
viewportHeight = 24F
|
||||||
|
path {
|
||||||
|
width
|
||||||
|
fillColor = Color(0xFF000000)
|
||||||
|
pathData = "M20,11H7.83l5.59,-5.59L12,4l-8,8l8,8l1.41,-1.41L7.83,13H20v-2z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package li.songe.gkd.util
|
package li.songe.gkd.utils
|
||||||
|
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
|
@ -16,32 +16,18 @@ import android.os.Looper
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import com.blankj.utilcode.util.ToastUtils
|
import androidx.core.graphics.drawable.IconCompat
|
||||||
import com.dylanc.activityresult.launcher.StartActivityLauncher
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.CoroutineStart
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.isActive
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
|
||||||
import li.songe.gkd.App
|
import li.songe.gkd.App
|
||||||
import li.songe.gkd.MainActivity
|
import li.songe.gkd.MainActivity
|
||||||
import li.songe.gkd.R
|
import li.songe.gkd.R
|
||||||
import li.songe.gkd.data.RuleManager
|
import li.songe.gkd.db.DbSet
|
||||||
import li.songe.gkd.data.SubscriptionRaw
|
import li.songe.gkd.icon.AddIcon
|
||||||
import li.songe.gkd.db.table.SubsItem
|
|
||||||
import li.songe.gkd.db.util.RoomX
|
|
||||||
import li.songe.gkd.shizuku.Shell
|
|
||||||
import li.songe.gkd.shizuku.ShizukuShell
|
|
||||||
import java.io.File
|
|
||||||
import java.net.NetworkInterface
|
import java.net.NetworkInterface
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
|
|
||||||
object Ext {
|
object Ext {
|
||||||
fun PackageManager.getApplicationInfoExt(
|
fun PackageManager.getApplicationInfoExt(
|
||||||
packageName: String,
|
packageName: String,
|
||||||
|
@ -120,7 +106,7 @@ object Ext {
|
||||||
)
|
)
|
||||||
|
|
||||||
val builder = NotificationCompat.Builder(context, channelId)
|
val builder = NotificationCompat.Builder(context, channelId)
|
||||||
.setSmallIcon(R.drawable.ic_app_2)
|
.setSmallIcon(SafeR.ic_launcher)
|
||||||
.setContentTitle("调试模式")
|
.setContentTitle("调试模式")
|
||||||
.setContentText("正在录制您的屏幕内容")
|
.setContentText("正在录制您的屏幕内容")
|
||||||
.setContentIntent(pendingIntent)
|
.setContentIntent(pendingIntent)
|
||||||
|
@ -160,90 +146,14 @@ object Ext {
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getSubsFileLastModified(): Long {
|
suspend fun getSubsFileLastModified(): Long {
|
||||||
return RoomX.select<SubsItem>().map { File(it.filePath) }
|
return DbSet.subsItemDao.query().first().map { it.subsFile }
|
||||||
.filter { it.isFile && it.exists() }
|
.filter { it.isFile && it.exists() }
|
||||||
.maxOfOrNull { it.lastModified() } ?: -1L
|
.maxOfOrNull { it.lastModified() } ?: -1L
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun buildRuleManager(): RuleManager {
|
@SuppressWarnings("fallthrough")
|
||||||
return RuleManager(*RoomX.select<SubsItem>().sortedBy { it.index }.map { subsItem ->
|
|
||||||
if (!subsItem.enable) return@map null
|
|
||||||
try {
|
|
||||||
val file = File(subsItem.filePath)
|
|
||||||
if (file.isFile && file.exists()) {
|
|
||||||
return@map SubscriptionRaw.parse5(file.readText())
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
return@map null
|
|
||||||
}.filterNotNull().toTypedArray())
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getActivityIdByShizuku(): String? {
|
|
||||||
if (!ShizukuShell.instance.isAvailable) return null
|
|
||||||
val result = withTimeoutOrNull(250) {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
ShizukuShell.instance.exec(Shell.Command("dumpsys activity activities | grep mResumedActivity"))
|
|
||||||
}
|
|
||||||
} ?: return null
|
|
||||||
val strList = result.out.split("\u0020")
|
|
||||||
if (!result.isSuccessful || strList.size < 4 || !strList[3].contains('/')) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
var (appId, activityId) = strList[3].split('/')
|
|
||||||
if (activityId.startsWith('.')) {
|
|
||||||
activityId = appId + activityId
|
|
||||||
}
|
|
||||||
return activityId
|
|
||||||
}
|
|
||||||
|
|
||||||
fun CoroutineScope.launchWhile(
|
|
||||||
context: CoroutineContext = EmptyCoroutineContext,
|
|
||||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
|
||||||
block: suspend CoroutineScope.() -> Unit,
|
|
||||||
) = launch(context, start) {
|
|
||||||
while (isActive) {
|
|
||||||
block()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun CoroutineScope.launchTry(
|
|
||||||
context: CoroutineContext = EmptyCoroutineContext,
|
|
||||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
|
||||||
block: suspend CoroutineScope.() -> Unit,
|
|
||||||
) = launch(context, start) {
|
|
||||||
try {
|
|
||||||
block()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
ToastUtils.showShort(e.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun createNotificationChannel(context: Service) {
|
fun createNotificationChannel(context: Service) {
|
||||||
val channelId = "channel_service_ab"
|
val channelId = "无障碍后台服务"
|
||||||
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, channelId)
|
|
||||||
.setSmallIcon(R.drawable.ic_app_2)
|
|
||||||
.setContentTitle("调试模式2")
|
|
||||||
.setContentText("测试后台任务")
|
|
||||||
.setContentIntent(pendingIntent)
|
|
||||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
|
||||||
.setOngoing(true)
|
|
||||||
.setAutoCancel(false)
|
|
||||||
|
|
||||||
val name = "无障碍服务"
|
val name = "无障碍服务"
|
||||||
val descriptionText = "无障碍服务保持活跃"
|
val descriptionText = "无障碍服务保持活跃"
|
||||||
val importance = NotificationManager.IMPORTANCE_DEFAULT
|
val importance = NotificationManager.IMPORTANCE_DEFAULT
|
||||||
|
@ -252,31 +162,40 @@ object Ext {
|
||||||
}
|
}
|
||||||
val notificationManager = NotificationManagerCompat.from(context)
|
val notificationManager = NotificationManagerCompat.from(context)
|
||||||
notificationManager.createNotificationChannel(channel)
|
notificationManager.createNotificationChannel(channel)
|
||||||
|
|
||||||
|
val serviceId = 100
|
||||||
|
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, channelId)
|
||||||
|
.setSmallIcon(SafeR.ic_add)
|
||||||
|
.setContentTitle("搞快点")
|
||||||
|
.setContentText("无障碍正在运行")
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
|
.setOngoing(true)
|
||||||
|
.setAutoCancel(false)
|
||||||
|
|
||||||
val notification = builder.build()
|
val notification = builder.build()
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
context.startForeground(
|
context.startForeground(
|
||||||
110,
|
serviceId,
|
||||||
notification,
|
notification,
|
||||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST
|
ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
context.startForeground(110, notification)
|
context.startForeground(serviceId, notification)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val LocalLauncher =
|
|
||||||
compositionLocalOf<StartActivityLauncher> { error("not found StartActivityLauncher") }
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun <T> usePollState(interval: Long = 400L, getter: () -> T): MutableState<T> {
|
|
||||||
val mutableState = remember { mutableStateOf(getter()) }
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
while (isActive) {
|
|
||||||
delay(interval)
|
|
||||||
mutableState.value = getter()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return mutableState
|
|
||||||
}
|
|
||||||
}
|
}
|
18
app/src/main/java/li/songe/gkd/utils/FolderExt.kt
Normal file
18
app/src/main/java/li/songe/gkd/utils/FolderExt.kt
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
package li.songe.gkd.utils
|
||||||
|
|
||||||
|
import com.blankj.utilcode.util.PathUtils
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
object FolderExt {
|
||||||
|
private fun createFolder(name: String): File {
|
||||||
|
return File(PathUtils.getExternalAppFilesPath().plus("/$name")).apply {
|
||||||
|
if (!exists()) {
|
||||||
|
mkdirs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val dbFolder by lazy { createFolder("db") }
|
||||||
|
val subsFolder by lazy { createFolder("subscription") }
|
||||||
|
val snapshotFolder by lazy { createFolder("snapshot") }
|
||||||
|
}
|
|
@ -1,23 +1,12 @@
|
||||||
package li.songe.gkd.hooks
|
package li.songe.gkd.utils
|
||||||
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import com.blankj.utilcode.util.LogUtils
|
|
||||||
import com.journeyapps.barcodescanner.ScanContract
|
import com.journeyapps.barcodescanner.ScanContract
|
||||||
import com.journeyapps.barcodescanner.ScanIntentResult
|
import com.journeyapps.barcodescanner.ScanIntentResult
|
||||||
import com.journeyapps.barcodescanner.ScanOptions
|
import com.journeyapps.barcodescanner.ScanOptions
|
||||||
import io.ktor.client.call.body
|
|
||||||
import io.ktor.client.request.get
|
|
||||||
import io.ktor.client.statement.bodyAsText
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import li.songe.gkd.data.SubscriptionRaw
|
|
||||||
import li.songe.gkd.data.Value
|
import li.songe.gkd.data.Value
|
||||||
import li.songe.gkd.util.Singleton
|
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
|
@ -43,14 +32,3 @@ fun useNavigateForQrcodeResult(): suspend () -> ScanIntentResult {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun useFetchSubs(): suspend (String) -> String {
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
var loading by remember { mutableStateOf(false) }
|
|
||||||
return remember {
|
|
||||||
{ url ->
|
|
||||||
loading
|
|
||||||
Singleton.client.get(url).bodyAsText()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
14
app/src/main/java/li/songe/gkd/utils/ModifierExt.kt
Normal file
14
app/src/main/java/li/songe/gkd/utils/ModifierExt.kt
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
package li.songe.gkd.utils
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.composed
|
||||||
|
|
||||||
|
inline fun Modifier.noRippleClickable(crossinline onClick: () -> Unit): Modifier = composed {
|
||||||
|
clickable(indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }) {
|
||||||
|
onClick()
|
||||||
|
}
|
||||||
|
}
|
8
app/src/main/java/li/songe/gkd/utils/NavExt.kt
Normal file
8
app/src/main/java/li/songe/gkd/utils/NavExt.kt
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
package li.songe.gkd.utils
|
||||||
|
|
||||||
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
|
||||||
|
|
||||||
|
val LocalNavController =
|
||||||
|
compositionLocalOf<NavHostController> { error("not found DestinationsNavigator") }
|
28
app/src/main/java/li/songe/gkd/utils/SafeR.kt
Normal file
28
app/src/main/java/li/songe/gkd/utils/SafeR.kt
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
package li.songe.gkd.utils
|
||||||
|
|
||||||
|
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_add: Int = R.drawable.ic_add
|
||||||
|
val ic_app_2: Int = R.drawable.ic_app_2
|
||||||
|
val ic_apps: Int = R.drawable.ic_apps
|
||||||
|
val ic_back: Int = R.drawable.ic_back
|
||||||
|
val ic_chart_bar: Int = R.drawable.ic_chart_bar
|
||||||
|
val ic_cog: Int = R.drawable.ic_cog
|
||||||
|
val ic_create_round: Int = R.drawable.ic_create_round
|
||||||
|
val ic_database_set: Int = R.drawable.ic_database_set
|
||||||
|
val ic_del: Int = R.drawable.ic_del
|
||||||
|
val ic_launcher: Int = R.drawable.ic_launcher
|
||||||
|
val ic_launcher_background: Int = R.drawable.ic_launcher_background
|
||||||
|
val ic_launcher_round: Int = R.drawable.ic_launcher_round
|
||||||
|
val ic_link: Int = R.drawable.ic_link
|
||||||
|
val ic_menu: Int = R.drawable.ic_menu
|
||||||
|
val ic_refresh: Int = R.drawable.ic_refresh
|
||||||
|
val ic_share: Int = R.drawable.ic_share
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package li.songe.gkd.util
|
package li.songe.gkd.utils
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
@ -16,7 +16,7 @@ import android.media.projection.MediaProjectionManager
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import com.blankj.utilcode.util.ScreenUtils
|
import com.blankj.utilcode.util.ScreenUtils
|
||||||
import li.songe.gkd.util.Ext.isEmptyBitmap
|
import li.songe.gkd.utils.Ext.isEmptyBitmap
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.resumeWithException
|
import kotlin.coroutines.resumeWithException
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user