mirror of
https://github.com/gkd-kit/gkd.git
synced 2024-11-16 03:32:38 +08:00
feat: selector_core
This commit is contained in:
parent
dcb86b3e94
commit
0038d5eb98
|
@ -9,6 +9,7 @@ plugins {
|
||||||
|
|
||||||
@Suppress("UnstableApiUsage")
|
@Suppress("UnstableApiUsage")
|
||||||
android {
|
android {
|
||||||
|
namespace = "li.songe.gkd"
|
||||||
compileSdk = libs.versions.android.compileSdk.get().toInt()
|
compileSdk = libs.versions.android.compileSdk.get().toInt()
|
||||||
buildToolsVersion = libs.versions.android.buildToolsVersion.get()
|
buildToolsVersion = libs.versions.android.buildToolsVersion.get()
|
||||||
|
|
||||||
|
@ -35,7 +36,6 @@ android {
|
||||||
|
|
||||||
lint {
|
lint {
|
||||||
disable.add("ModifierFactoryUnreferencedReceiver")
|
disable.add("ModifierFactoryUnreferencedReceiver")
|
||||||
// baseline = file("lint-baseline.xml")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
|
@ -85,6 +85,7 @@ android {
|
||||||
freeCompilerArgs = freeCompilerArgs + "-opt-in=kotlin.RequiresOptIn"
|
freeCompilerArgs = freeCompilerArgs + "-opt-in=kotlin.RequiresOptIn"
|
||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
|
buildConfig = true
|
||||||
compose = true
|
compose = true
|
||||||
}
|
}
|
||||||
composeOptions {
|
composeOptions {
|
||||||
|
@ -108,12 +109,14 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(mapOf("path" to ":selector")))
|
implementation(project(mapOf("path" to ":selector_core")))
|
||||||
|
implementation(project(mapOf("path" to ":selector_android")))
|
||||||
implementation(project(mapOf("path" to ":router")))
|
implementation(project(mapOf("path" to ":router")))
|
||||||
|
|
||||||
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)
|
||||||
|
@ -146,7 +149,6 @@ dependencies {
|
||||||
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.material)
|
|
||||||
implementation(libs.google.accompanist.drawablepainter)
|
implementation(libs.google.accompanist.drawablepainter)
|
||||||
implementation(libs.google.accompanist.placeholder.material)
|
implementation(libs.google.accompanist.placeholder.material)
|
||||||
|
|
||||||
|
|
|
@ -2,11 +2,11 @@
|
||||||
"formatVersion": 1,
|
"formatVersion": 1,
|
||||||
"database": {
|
"database": {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"identityHash": "5e3c352578a63c3fccbb5e3fba31c89d",
|
"identityHash": "2083d8585fffd897fde3733958e356f8",
|
||||||
"entities": [
|
"entities": [
|
||||||
{
|
{
|
||||||
"tableName": "subs_item",
|
"tableName": "subs_item",
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `comment` TEXT NOT NULL, `update_url` TEXT NOT NULL, `file_path` TEXT NOT NULL, `index` INTEGER NOT NULL)",
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `name` TEXT NOT NULL, `update_url` TEXT NOT NULL, `version` INTEGER NOT NULL, `file_path` TEXT NOT NULL, `index` INTEGER NOT NULL)",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldPath": "id",
|
"fieldPath": "id",
|
||||||
|
@ -33,8 +33,8 @@
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldPath": "comment",
|
"fieldPath": "name",
|
||||||
"columnName": "comment",
|
"columnName": "name",
|
||||||
"affinity": "TEXT",
|
"affinity": "TEXT",
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
|
@ -44,6 +44,12 @@
|
||||||
"affinity": "TEXT",
|
"affinity": "TEXT",
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "version",
|
||||||
|
"columnName": "version",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldPath": "filePath",
|
"fieldPath": "filePath",
|
||||||
"columnName": "file_path",
|
"columnName": "file_path",
|
||||||
|
@ -58,10 +64,10 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"primaryKey": {
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
"columnNames": [
|
"columnNames": [
|
||||||
"id"
|
"id"
|
||||||
],
|
]
|
||||||
"autoGenerate": true
|
|
||||||
},
|
},
|
||||||
"indices": [
|
"indices": [
|
||||||
{
|
{
|
||||||
|
@ -136,10 +142,10 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"primaryKey": {
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
"columnNames": [
|
"columnNames": [
|
||||||
"id"
|
"id"
|
||||||
],
|
]
|
||||||
"autoGenerate": true
|
|
||||||
},
|
},
|
||||||
"indices": [],
|
"indices": [],
|
||||||
"foreignKeys": []
|
"foreignKeys": []
|
||||||
|
@ -148,7 +154,7 @@
|
||||||
"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, '5e3c352578a63c3fccbb5e3fba31c89d')"
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2083d8585fffd897fde3733958e356f8')"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,154 +0,0 @@
|
||||||
{
|
|
||||||
"formatVersion": 1,
|
|
||||||
"database": {
|
|
||||||
"version": 2,
|
|
||||||
"identityHash": "c87d5110fcf059e6e690b1fb7938c8a8",
|
|
||||||
"entities": [
|
|
||||||
{
|
|
||||||
"tableName": "subs_item",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `name` TEXT NOT NULL, `update_url` TEXT NOT NULL, `file_path` TEXT NOT NULL, `index` INTEGER NOT NULL)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "ctime",
|
|
||||||
"columnName": "ctime",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "mtime",
|
|
||||||
"columnName": "mtime",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "enable",
|
|
||||||
"columnName": "enable",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "updateUrl",
|
|
||||||
"columnName": "update_url",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "filePath",
|
|
||||||
"columnName": "file_path",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "index",
|
|
||||||
"columnName": "index",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_subs_item_update_url",
|
|
||||||
"unique": true,
|
|
||||||
"columnNames": [
|
|
||||||
"update_url"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_subs_item_update_url` ON `${TABLE_NAME}` (`update_url`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "subs_config",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` 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, `rule_key` INTEGER NOT NULL)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "ctime",
|
|
||||||
"columnName": "ctime",
|
|
||||||
"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
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "ruleKey",
|
|
||||||
"columnName": "rule_key",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"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, 'c87d5110fcf059e6e690b1fb7938c8a8')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,154 +0,0 @@
|
||||||
{
|
|
||||||
"formatVersion": 1,
|
|
||||||
"database": {
|
|
||||||
"version": 3,
|
|
||||||
"identityHash": "c87d5110fcf059e6e690b1fb7938c8a8",
|
|
||||||
"entities": [
|
|
||||||
{
|
|
||||||
"tableName": "subs_item",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `name` TEXT NOT NULL, `update_url` TEXT NOT NULL, `file_path` TEXT NOT NULL, `index` INTEGER NOT NULL)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "ctime",
|
|
||||||
"columnName": "ctime",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "mtime",
|
|
||||||
"columnName": "mtime",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "enable",
|
|
||||||
"columnName": "enable",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "updateUrl",
|
|
||||||
"columnName": "update_url",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "filePath",
|
|
||||||
"columnName": "file_path",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "index",
|
|
||||||
"columnName": "index",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_subs_item_update_url",
|
|
||||||
"unique": true,
|
|
||||||
"columnNames": [
|
|
||||||
"update_url"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_subs_item_update_url` ON `${TABLE_NAME}` (`update_url`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "subs_config",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` 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, `rule_key` INTEGER NOT NULL)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "ctime",
|
|
||||||
"columnName": "ctime",
|
|
||||||
"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
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "ruleKey",
|
|
||||||
"columnName": "rule_key",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"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, 'c87d5110fcf059e6e690b1fb7938c8a8')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
64
app/schemas/li.songe.gkd.db.LogDatabase/1.json
Normal file
64
app/schemas/li.songe.gkd.db.LogDatabase/1.json
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 1,
|
||||||
|
"identityHash": "81719a0dbd7e0ef535884794b5eec49e",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "trigger_log",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `selector` TEXT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "ctime",
|
||||||
|
"columnName": "ctime",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mtime",
|
||||||
|
"columnName": "mtime",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "appId",
|
||||||
|
"columnName": "app_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "activityId",
|
||||||
|
"columnName": "activity_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "selector",
|
||||||
|
"columnName": "selector",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"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, '81719a0dbd7e0ef535884794b5eec49e')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
package="li.songe.gkd">
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
|
|
||||||
|
@ -19,6 +18,7 @@
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name="li.songe.gkd.App"
|
android:name="li.songe.gkd.App"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
@ -61,7 +61,7 @@
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="li.songe.gkd.accessibility.GkdAbService"
|
android:name=".accessibility.GkdAbService"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:label="@string/accessibility_service_label"
|
android:label="@string/accessibility_service_label"
|
||||||
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
|
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
|
||||||
|
|
|
@ -1,602 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://raw.githubusercontent.com/lisonge/gkd-subscription/main/lib/gkd.schema.json",
|
|
||||||
"version": 1,
|
|
||||||
"author": "https://github.com/lisonge",
|
|
||||||
"description": "APP内部广告集合",
|
|
||||||
"appList": [
|
|
||||||
{
|
|
||||||
"packageName": "com.zhihu.android",
|
|
||||||
"groupList": [
|
|
||||||
{
|
|
||||||
"key": 0,
|
|
||||||
"className": "com.zhihu.android.mix.activity.ContentMixProfileActivity",
|
|
||||||
"description": "知乎回答页面-文章底部-卡片广告",
|
|
||||||
"ruleList": [
|
|
||||||
{
|
|
||||||
"selector": "View[text$=`广告`] - View[text=`×`]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "TextView[text$=`广告`] - Image"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "TextView[text$=`广告`] +2 Image"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "TextView[text.length>0] + TextView[text*=`热度`] + View > Image"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "View[text$=`的广告`] + View[text=`×`]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "TextView[text$=`的广告`] + TextView[text=`×`]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "View[text$=`的广告`] +2 View[text=`×`]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "TextView[text=`了解更多`] > TextView[text=`×`]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "TextView[text$=`的广告`] +2 TextView[text=`×`]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "TextView[text=`限时解锁`] +2 Image",
|
|
||||||
"description": "盐选会员推荐"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": 1,
|
|
||||||
"className": "com.zhihu.android.mix.activity.ContentMixProfileActivity",
|
|
||||||
"description": "知乎回答页面-文章底部-文章推荐",
|
|
||||||
"ruleList": [
|
|
||||||
{
|
|
||||||
"selector": "View[text$=`关注`][text*=`回答`] + View[text=`×`]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "View[text*=`的回答`][text*=`点赞`][text$=`评论`] + TextView + Image"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "TextView[text*=`赞同`][text$=`评论`][text*=`·`] + TextView[text$=`专题精选`] + View[text=`×`]"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": 2,
|
|
||||||
"className": "com.zhihu.android.ContentActivity",
|
|
||||||
"description": "题目概览页面-回答列表-广告",
|
|
||||||
"ruleList": [
|
|
||||||
{
|
|
||||||
"selector": "TextView[text=`确认`][id=`com.zhihu.android:id/confirm_uninterest`]",
|
|
||||||
"description": "确认关闭广告按钮"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "TextView[text=`撤销`] <2 LinearLayoutCompat + FrameLayout > TextView[id=`com.zhihu.android:id/uninterest_reason`]",
|
|
||||||
"description": "不感兴趣理由按钮"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "ViewGroup > TextView[text*=`广告`] +4 ImageView",
|
|
||||||
"description": "广告关闭按钮-1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "ViewGroup > TextView[text*=`广告`] +2 ImageView",
|
|
||||||
"description": "广告关闭按钮-2"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"className": "com.zhihu.android.app.ui.activity.MainActivity",
|
|
||||||
"ruleList": [
|
|
||||||
{
|
|
||||||
"selector": "TextView[id=`com.zhihu.android:id/confirm_uninterest`]",
|
|
||||||
"description": "确认关闭广告按钮"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "TextView[id=`com.zhihu.android:id/uninterest_reason`]",
|
|
||||||
"description": "不感兴趣理由按钮"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "ViewGroup > TextView[text*=`的广告`] +2 ImageView"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"className": "com.zhihu.android.app.ui.activity.LauncherActivity",
|
|
||||||
"description": "开屏广告",
|
|
||||||
"ruleList": [
|
|
||||||
{
|
|
||||||
"selector": "TextView[id=`com.zhihu.android:id/btn_skip`]",
|
|
||||||
"className": "com.zhihu.android.app.ui.activity.LauncherActivity"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "TextView[id=`com.zhihu.android:id/btn_skip`]",
|
|
||||||
"className": "com.zhihu.android.app.ui.activity.LaunchAdActivity"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"packageName": "com.baidu.tieba",
|
|
||||||
"groupList": [
|
|
||||||
{
|
|
||||||
"className": "com.baidu.tieba.tblauncher.MainTabActivity",
|
|
||||||
"description": "开屏广告",
|
|
||||||
"ruleList": [
|
|
||||||
{
|
|
||||||
"selector": "View[id=`com.byted.pangle:id/tt_splash_skip_btn`]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "TextView[text*=`广告`] - TextView[text^=`跳过`]"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"className": "com.baidu.tieba.tblauncher.MainTabActivity",
|
|
||||||
"description": "主页推荐-广告",
|
|
||||||
"ruleList": [
|
|
||||||
{
|
|
||||||
"selector": "TextView[text*=`广告`] < LinearLayout -2 RelativeLayout > TextView[text=`选择不喜欢理由`] + View"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "TextView[text$=`广告`] < RelativeLayout <3 LinearLayout - LinearLayout >4 ImageView"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"className": "com.baidu.tieba.pb.pb.main.PbActivity",
|
|
||||||
"ruleList": [
|
|
||||||
{
|
|
||||||
"selector": "TextView[text*=`广告`] < LinearLayout -2 RelativeLayout > TextView[text*=`选择不喜欢理由`] + View[index=1]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "RelativeLayout[id=`com.baidu.tieba:id/obfuscated`] > LinearLayout > FrameLayout[id=`com.baidu.tieba:id/obfuscated`] > ImageView"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"packageName": "com.tencent.mm",
|
|
||||||
"groupList": [
|
|
||||||
{
|
|
||||||
"className": "com.tencent.mm.plugin.sns.ui.SnsTimeLineUI",
|
|
||||||
"description": "朋友圈-广告卡片",
|
|
||||||
"ruleList": [
|
|
||||||
{
|
|
||||||
"selector": "TextView[text=`选择后将减少该类推荐`] + TextView[text=`确认`]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "TextView[text=`选择后将减少该类推荐`] + FrameLayout > ViewGroup > TextView[text.length>0]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "TextView[text*=`广告`] + TextView[text*=`广告`] + FrameLayout > LinearLayout > LinearLayout + LinearLayout"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "ImageView[contentDescription$=`的头像`] + LinearLayout > LinearLayout[childCount=2] > TextView[id!=null][text.length>0] + LinearLayout[id!=null][childCount=0]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "ImageView[contentDescription$=`的头像`] + LinearLayout > LinearLayout[childCount=2] > TextView[id!=null][text.length>0] + LinearLayout > TextView[text*=`广告`]"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"className": "com.tencent.mm.plugin.webwx.ui.ExtDeviceWXLoginUI",
|
|
||||||
"description": "其他设备登录界面-自动点击登录",
|
|
||||||
"ruleList": [
|
|
||||||
{
|
|
||||||
"selector": "TextView[text=`取消登录`] - Button[text=`登录`]"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"className": "com.tencent.mm.ui.LauncherUI",
|
|
||||||
"description": "微信聊天界面-红包",
|
|
||||||
"ruleList": [
|
|
||||||
{
|
|
||||||
"selector": "TextView[text$=`的红包`] <2 LinearLayout < LinearLayout +2 ImageButton[contentDescription=`开`] + Button",
|
|
||||||
"className": "com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyNotHookReceiveUI"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "TextView[text$=`的红包`] >2 LinearLayout - ImageView < LinearLayout +2 RelativeLayout > TextView[text=`微信红包`]",
|
|
||||||
"description": "红包描述下面有一行小字-**的红包"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "RelativeLayout + LinearLayout >5 LinearLayout[childCount=1] - ImageView < LinearLayout +2 RelativeLayout > TextView[text=`微信红包`]",
|
|
||||||
"description": "红包下面没有小字, 但是得和自己的红包区分开来"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"className": "com.tencent.mm.ui.LauncherUI",
|
|
||||||
"description": "微信聊天界面-转账",
|
|
||||||
"ruleList": [
|
|
||||||
{
|
|
||||||
"className": "com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyDetailUI",
|
|
||||||
"selector": "ImageView[contentDescription=`返回按钮`]",
|
|
||||||
"description": "红包领取结果页面-返回按钮"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "TextView - Button[text=`收款`]",
|
|
||||||
"className": "com.tencent.mm.plugin.remittance.ui.RemittanceDetailUI"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "RelativeLayout + LinearLayout >4 TextView[text!=`已被接收`][text!=`已收款`][text!=`已收款待入账`] <2 LinearLayout < RelativeLayout <2 LinearLayout + RelativeLayout > TextView[text=`微信转账`]",
|
|
||||||
"description": "注意:如果红包备注是[已被接收,已收款,已收款待入账],则无法区分"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"className": "com.tencent.mm.plugin.webview.ui.tools.SDKOAuthUI",
|
|
||||||
"description": "第三方网页登录",
|
|
||||||
"ruleList": [
|
|
||||||
{
|
|
||||||
"selector": "Button[text=`拒绝`] - Button[text=`允许`]"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"packageName": "tv.danmaku.bili",
|
|
||||||
"groupList": [
|
|
||||||
{
|
|
||||||
"className": "tv.danmaku.bili.ui.video.VideoDetailsActivity",
|
|
||||||
"description": "视频播放页-评论区顶部-公告或推荐",
|
|
||||||
"ruleList": [
|
|
||||||
{
|
|
||||||
"selector": "ImageView + TextView[id=`tv.danmaku.bili:id/content`] + ImageView[id=`tv.danmaku.bili:id/close`]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "ImageView + TextView[id=`tv.danmaku.bili:id/content`] + ImageView[id=`tv.danmaku.bili:id/close`]",
|
|
||||||
"className": "com.bilibili.video.videodetail.VideoDetailsActivity",
|
|
||||||
"description": "视频评论区-公告"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "ImageView + TextView[id=`tv.danmaku.bili:id/content`] + ImageView[id=`tv.danmaku.bili:id/close`]",
|
|
||||||
"className": "com.bilibili.lib.ui.GeneralActivity",
|
|
||||||
"description": "用户动态评论区-公告"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "ImageView + TextView[id=`tv.danmaku.bili:id/content`] + ImageView[id=`tv.danmaku.bili:id/close`]",
|
|
||||||
"className": "com.bilibili.bangumi.ui.page.detail.BangumiDetailActivityV3",
|
|
||||||
"description": "番剧评论区-公告"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"className": "tv.danmaku.bili.MainActivityV2",
|
|
||||||
"description": "动态-综合-广告卡片",
|
|
||||||
"ruleList": [
|
|
||||||
{
|
|
||||||
"selector": "TextView[id^=`tv.danmaku.bili:id/reason`]",
|
|
||||||
"description": "点击广告关闭后的弹窗的列表项的第一个",
|
|
||||||
"className": "com.bilibili.lib.ui.menu.a"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "TextView[id=`tv.danmaku.bili:id/ad_tag_text`]",
|
|
||||||
"description": "广告卡片-右上角文本-广告^"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"className": "tv.danmaku.bili.MainActivityV2",
|
|
||||||
"description": "首屏推荐",
|
|
||||||
"ruleList": [
|
|
||||||
{
|
|
||||||
"selector": "TextView[text^=`跳过`][id=`tv.danmaku.bili:id/count_down`]"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"className": "com.bilibili.teenagersmode.ui.TeenagersModeDialogActivity",
|
|
||||||
"description": "青少年模式弹窗",
|
|
||||||
"ruleList": [
|
|
||||||
{
|
|
||||||
"selector": "TextView[text=`我知道了`]"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"packageName": "com.duokan.phone.remotecontroller",
|
|
||||||
"groupList": [
|
|
||||||
{
|
|
||||||
"className": "com.xiaomi.mitv.phone.remotecontroller.HoriWidgetMainActivityV2",
|
|
||||||
"description": "首页-底部推荐卡片",
|
|
||||||
"ruleList": [
|
|
||||||
{
|
|
||||||
"selector": "ImageView[id=`com.duokan.phone.remotecontroller:id/image_close_banner`]"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"packageName": "com.coolapk.market",
|
|
||||||
"groupList": [
|
|
||||||
{
|
|
||||||
"className": "com.coolapk.market.view.main.MainActivity",
|
|
||||||
"ruleList": [
|
|
||||||
{
|
|
||||||
"selector": "TextView[text=`举报广告`] < LinearLayout - LinearLayout",
|
|
||||||
"className": "com.bytedance.sdk.openadsdk.core.dislike.ui.c",
|
|
||||||
"description": "关闭广告后的弹窗, [举报广告] 上面有多个按钮, 且按钮文字顺序随机, 所以我们只需要点击最近一个按钮即可"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "TextView[text=`举报广告`] < LinearLayout - LinearLayout",
|
|
||||||
"description": "关闭广告后的弹窗, [举报广告] 上面有多个按钮, 且按钮文字顺序随机, 所以我们只需要点击最近一个按钮即可"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "Button[text*=`广告`] - Button[text=`不感兴趣`]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "TextView[id=`com.coolapk.market:id/ad_text_view`] + ImageView[id=`com.coolapk.market:id/close_view`]"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"packageName": "com.sina.weibo",
|
|
||||||
"groupList": [
|
|
||||||
{
|
|
||||||
"className": "com.sina.weibo.feed.DetailWeiboActivity",
|
|
||||||
"ruleList": [
|
|
||||||
{
|
|
||||||
"selector": "RelativeLayout > TextView[id=`com.sina.weibo:id/tvTrendsTitle`] + ImageView[id=`com.sina.weibo:id/iv_ad_x`]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "TextView[text=`为何会看到此广告`] -n TextView[text=`不感兴趣`]",
|
|
||||||
"className": "com.sina.weibo.utils.WeiboDialog$CustomDialog"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "TextView[id=`com.sina.weibo:id/tv_tips`][text=`广告`] + ImageView[id=`com.sina.weibo:id/iv_close_icon`]"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"className": "com.sina.weibo.MainTabActivity",
|
|
||||||
"ruleList": [
|
|
||||||
{
|
|
||||||
"selector": "LinearLayout[id=`com.sina.weibo:id/comment_guide_view`] -3 ViewGroup[id=`com.sina.weibo:id/mblogHeadtitle`][index=0](95%, 43%)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "RelativeLayout[id=`com.sina.weibo:id/complete_layout`] + ImageView[id=`com.sina.weibo:id/close`]",
|
|
||||||
"description": "立即领取红包右上角x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "TextView[text=`为何会看到此广告`] < LinearLayout <2 LinearLayout -3 LinearLayout >2 TextView[text=`不感兴趣`]"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"className": "com.sina.weibo.SplashActivity",
|
|
||||||
"ruleList": [
|
|
||||||
{
|
|
||||||
"selector": "TextView[text=`跳过`](50%, 50%)",
|
|
||||||
"className": "com.sina.weibo.mobileads.view.a"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "TextView[text=`跳过`](50%, 50%)"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"packageName": "com.tencent.mobileqq",
|
|
||||||
"groupList": [
|
|
||||||
{
|
|
||||||
"className": "com.tencent.mobileqq.activity.SplashActivity",
|
|
||||||
"description": "qq空间广告",
|
|
||||||
"ruleList": [
|
|
||||||
{
|
|
||||||
"selector": "TextView[text=`关闭此条广告`]",
|
|
||||||
"className": "com.tencent.qqlive.module.videoreport.inject.dialog.ReportDialog"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "ImageView[contentDescription=`关闭`] < FrameLayout[contentDescription*=`广告`]",
|
|
||||||
"description": "极简模式-动态"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "ImageView[contentDescription=`关闭`] < FrameLayout[contentDescription*=`广告`]",
|
|
||||||
"className": "cooperation.qzone.QzoneFeedsPluginProxyActivity",
|
|
||||||
"description": "动态-好友动态"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"className": "com.tencent.mobileqq.activity.SplashActivity",
|
|
||||||
"description": "qq空间-注销后-重新开通提示",
|
|
||||||
"ruleList": [
|
|
||||||
{
|
|
||||||
"selector": "View[contentDescription=`你已注销你的空间`] - ImageView",
|
|
||||||
"description": "极简模式-动态"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "View[contentDescription=`你已注销你的空间`] - ImageView",
|
|
||||||
"className": "cooperation.qzone.QzoneFeedsPluginProxyActivity",
|
|
||||||
"description": "动态-好友动态"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"packageName": "com.iqiyi.hotchat",
|
|
||||||
"groupList": [
|
|
||||||
{
|
|
||||||
"className": "com.iqiyi.hotchat.ui.activity.WelcomeActivity",
|
|
||||||
"description": "开屏广告/其他可跳过提示",
|
|
||||||
"ruleList": [
|
|
||||||
{
|
|
||||||
"selector": "TextView[id=`com.iqiyi.hotchat:id/tv_advertisement_lunch_skip`]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "TextView[id=`com.iqiyi.hotchat:id/tv_advertisement_lunch_skip`]",
|
|
||||||
"className": "com.iqiyi.hotchat.ui.activity.AdvertisementActivity"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"packageName": "com.sdu.didi.psnger",
|
|
||||||
"groupList": [
|
|
||||||
{
|
|
||||||
"className": "com.didi.sdk.app.launch.splash.SplashActivity",
|
|
||||||
"description": "开屏推荐",
|
|
||||||
"ruleList": [
|
|
||||||
{
|
|
||||||
"selector": "TextView[id=`com.sdu.didi.psnger:id/skip_ad_tv`]"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"packageName": "com.baidu.BaiduMap",
|
|
||||||
"groupList": [
|
|
||||||
{
|
|
||||||
"className": "com.baidu.baidumaps.WelcomeScreen",
|
|
||||||
"description": "开屏推荐",
|
|
||||||
"ruleList": [
|
|
||||||
{
|
|
||||||
"selector": "TextView[text=`com.baidu.BaiduMap:id/skip_text`]"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"packageName": "com.tencent.qqlive",
|
|
||||||
"groupList": [
|
|
||||||
{
|
|
||||||
"className": "com.tencent.qqlive.ona.activity.SplashHomeActivity",
|
|
||||||
"description": "开屏广告/推荐",
|
|
||||||
"ruleList": [
|
|
||||||
{
|
|
||||||
"selector": "TextView < LinearLayout + TextView[text=`跳过`](50%, 50%)"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"packageName": "com.tencent.qqmusic",
|
|
||||||
"groupList": [
|
|
||||||
{
|
|
||||||
"className": "com.tencent.qqmusic.activity.AppStarterActivity",
|
|
||||||
"ruleList": [
|
|
||||||
{
|
|
||||||
"selector": "ViewGroup + TextView[text=`跳过`]"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"packageName": "com.MobileTicket",
|
|
||||||
"groupList": [
|
|
||||||
{
|
|
||||||
"className": "com.MobileTicket.ui.dialog.SplashAdDialog",
|
|
||||||
"ruleList": [
|
|
||||||
{
|
|
||||||
"selector": "TextView[id=`com.MobileTicket:id/tv_skip`]"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"packageName": "com.hpbr.bosszhipin",
|
|
||||||
"groupList": [
|
|
||||||
{
|
|
||||||
"className": "com.hpbr.bosszhipin.module.launcher.WelcomeActivity",
|
|
||||||
"ruleList": [
|
|
||||||
{
|
|
||||||
"selector": "TextView[text*=`跳过`]"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"packageName": "com.sankuai.meituan.takeoutnew",
|
|
||||||
"groupList": [
|
|
||||||
{
|
|
||||||
"className": "com.sankuai.meituan.takeoutnew.ui.page.boot.SplashAdActivity",
|
|
||||||
"ruleList": [
|
|
||||||
{
|
|
||||||
"selector": "TextView[text*=`跳过`]"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"packageName": "com.sankuai.meituan",
|
|
||||||
"groupList": [
|
|
||||||
{
|
|
||||||
"className": "com.meituan.android.pt.homepage.activity.MainActivit",
|
|
||||||
"ruleList": [
|
|
||||||
{
|
|
||||||
"selector": "TextView[id=`com.sankuai.meituan:id/close_btn`][text*=`跳过`]"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"packageName": "com.mihoyo.hyperion",
|
|
||||||
"groupList": [
|
|
||||||
{
|
|
||||||
"className": "com.mihoyo.hyperion.ui.SplashActivity",
|
|
||||||
"description": "开屏推荐",
|
|
||||||
"ruleList": [
|
|
||||||
{
|
|
||||||
"selector": "Button[id=`com.mihoyo.hyperion:id/mSplashBtJump`]"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"className": "com.mihoyo.hyperion.teenage.ui.TeenageTipsDialogActivity",
|
|
||||||
"description": "青少年模式弹窗-我知道了",
|
|
||||||
"ruleList": [
|
|
||||||
{
|
|
||||||
"selector": "TextView[id=`com.mihoyo.hyperion:id/tv_i_know`]"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"packageName": "cn.wps.moffice.documentmanager",
|
|
||||||
"groupList": [
|
|
||||||
{
|
|
||||||
"className": "cn.wps.moffice.documentmanager.PreStartActivity",
|
|
||||||
"ruleList": [
|
|
||||||
{
|
|
||||||
"selector": "TextView[id=`cn.wps.moffice_eng:id/splash_skip`]"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"packageName": "com.duowan.kiwi",
|
|
||||||
"groupList": [
|
|
||||||
{
|
|
||||||
"className": "com.duowan.kiwi.adsplash.view.AdSplashActivity",
|
|
||||||
"ruleList": [
|
|
||||||
{
|
|
||||||
"selector": "TextView[text*=`跳过`]"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -16,6 +16,9 @@ class App : Application() {
|
||||||
context = this
|
context = this
|
||||||
MMKV.initialize(this)
|
MMKV.initialize(this)
|
||||||
LogUtils.d(Storage.settings)
|
LogUtils.d(Storage.settings)
|
||||||
|
if (!Storage.settings.enableConsoleLogOut){
|
||||||
|
LogUtils.d("关闭日志控制台输出")
|
||||||
|
}
|
||||||
LogUtils.getConfig().apply {
|
LogUtils.getConfig().apply {
|
||||||
isLog2FileSwitch = true
|
isLog2FileSwitch = true
|
||||||
saveDays = 30
|
saveDays = 30
|
||||||
|
|
|
@ -6,7 +6,7 @@ import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import com.blankj.utilcode.util.LogUtils
|
import com.blankj.utilcode.util.LogUtils
|
||||||
import com.dylanc.activityresult.launcher.StartActivityLauncher
|
import com.dylanc.activityresult.launcher.StartActivityLauncher
|
||||||
import li.songe.gkd.composition.CompositionActivity
|
import li.songe.gkd.composition.CompositionActivity
|
||||||
import li.songe.gkd.composition.Hook.useLifeCycleLog
|
import li.songe.gkd.composition.CompositionExt.useLifeCycleLog
|
||||||
import li.songe.gkd.ui.home.HomePage
|
import li.songe.gkd.ui.home.HomePage
|
||||||
import li.songe.gkd.ui.theme.MainTheme
|
import li.songe.gkd.ui.theme.MainTheme
|
||||||
import li.songe.gkd.util.Ext.LocalLauncher
|
import li.songe.gkd.util.Ext.LocalLauncher
|
||||||
|
|
|
@ -2,20 +2,28 @@ package li.songe.gkd.accessibility
|
||||||
|
|
||||||
import android.view.accessibility.AccessibilityEvent
|
import android.view.accessibility.AccessibilityEvent
|
||||||
import com.blankj.utilcode.util.LogUtils
|
import com.blankj.utilcode.util.LogUtils
|
||||||
|
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 io.ktor.client.request.get
|
||||||
|
import io.ktor.client.statement.bodyAsText
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import li.songe.gkd.composition.CompositionAbService
|
import li.songe.gkd.composition.CompositionAbService
|
||||||
import li.songe.gkd.composition.Hook.useLifeCycleLog
|
import li.songe.gkd.composition.CompositionExt.useLifeCycleLog
|
||||||
import li.songe.gkd.composition.Hook.useScope
|
import li.songe.gkd.composition.CompositionExt.useScope
|
||||||
import li.songe.gkd.data.RuleManager
|
import li.songe.gkd.data.RuleManager
|
||||||
|
import li.songe.gkd.data.SubscriptionRaw
|
||||||
|
import li.songe.gkd.db.table.SubsItem
|
||||||
|
import li.songe.gkd.db.util.RoomX
|
||||||
import li.songe.gkd.debug.server.api.Node
|
import li.songe.gkd.debug.server.api.Node
|
||||||
import li.songe.gkd.util.Ext.buildRuleManager
|
import li.songe.gkd.util.Ext.buildRuleManager
|
||||||
import li.songe.gkd.util.Ext.getActivityIdByShizuku
|
import li.songe.gkd.util.Ext.getActivityIdByShizuku
|
||||||
import li.songe.gkd.util.Ext.getSubsFileLastModified
|
import li.songe.gkd.util.Ext.getSubsFileLastModified
|
||||||
import li.songe.gkd.util.Ext.launchWhile
|
import li.songe.gkd.util.Ext.launchWhile
|
||||||
|
import li.songe.gkd.util.Singleton
|
||||||
import li.songe.gkd.util.Storage
|
import li.songe.gkd.util.Storage
|
||||||
import li.songe.selector.GkdSelector
|
import li.songe.selector_android.GkdSelector
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
class GkdAbService : CompositionAbService({
|
class GkdAbService : CompositionAbService({
|
||||||
useLifeCycleLog()
|
useLifeCycleLog()
|
||||||
|
@ -28,12 +36,14 @@ class GkdAbService : CompositionAbService({
|
||||||
onDestroy { service = null }
|
onDestroy { service = null }
|
||||||
|
|
||||||
KeepAliveService.start(context)
|
KeepAliveService.start(context)
|
||||||
|
onDestroy {
|
||||||
|
KeepAliveService.stop(context)
|
||||||
|
}
|
||||||
|
|
||||||
var serviceConnected = false
|
var serviceConnected = false
|
||||||
onServiceConnected { serviceConnected = true }
|
onServiceConnected { serviceConnected = true }
|
||||||
onInterrupt { serviceConnected = false }
|
onInterrupt { serviceConnected = false }
|
||||||
|
|
||||||
|
|
||||||
onAccessibilityEvent { event ->
|
onAccessibilityEvent { event ->
|
||||||
val activityId = event?.className?.toString() ?: return@onAccessibilityEvent
|
val activityId = event?.className?.toString() ?: return@onAccessibilityEvent
|
||||||
val rootAppId = rootInActiveWindow?.packageName?.toString() ?: return@onAccessibilityEvent
|
val rootAppId = rootInActiveWindow?.packageName?.toString() ?: return@onAccessibilityEvent
|
||||||
|
@ -105,6 +115,36 @@ class GkdAbService : CompositionAbService({
|
||||||
delay(200)
|
delay(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scope.launchWhile {
|
||||||
|
delay(5000)
|
||||||
|
RoomX.select<SubsItem>().map { subsItem ->
|
||||||
|
if (!NetworkUtils.isAvailable()) return@map
|
||||||
|
try {
|
||||||
|
val text = Singleton.client.get(subsItem.updateUrl).bodyAsText()
|
||||||
|
val subscriptionRaw = SubscriptionRaw.parse5(text)
|
||||||
|
if (subscriptionRaw.version <= subsItem.version) {
|
||||||
|
return@map
|
||||||
|
}
|
||||||
|
val newItem = subsItem.copy(
|
||||||
|
updateUrl = subscriptionRaw.updateUrl
|
||||||
|
?: subsItem.updateUrl,
|
||||||
|
name = subscriptionRaw.name,
|
||||||
|
mtime = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
RoomX.update(newItem)
|
||||||
|
File(newItem.filePath).writeText(
|
||||||
|
SubscriptionRaw.stringify(
|
||||||
|
subscriptionRaw
|
||||||
|
)
|
||||||
|
)
|
||||||
|
LogUtils.d("更新订阅文件:${subsItem.name}")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delay(30 * 60_000)
|
||||||
|
}
|
||||||
|
|
||||||
}) {
|
}) {
|
||||||
private var nodeSnapshot = NodeSnapshot()
|
private var nodeSnapshot = NodeSnapshot()
|
||||||
set(value) {
|
set(value) {
|
||||||
|
|
|
@ -5,7 +5,7 @@ import android.content.Intent
|
||||||
import kotlinx.coroutines.delay
|
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.Hook.useScope
|
import li.songe.gkd.composition.CompositionExt.useScope
|
||||||
import li.songe.gkd.util.Ext.createNotificationChannel
|
import li.songe.gkd.util.Ext.createNotificationChannel
|
||||||
import li.songe.gkd.util.Ext.launchWhile
|
import li.songe.gkd.util.Ext.launchWhile
|
||||||
|
|
||||||
|
@ -21,5 +21,9 @@ class KeepAliveService : CompositionService({
|
||||||
fun start(context: Context = App.context) {
|
fun start(context: Context = App.context) {
|
||||||
context.startForegroundService(Intent(context, KeepAliveService::class.java))
|
context.startForegroundService(Intent(context, KeepAliveService::class.java))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun stop(context: Context = App.context) {
|
||||||
|
context.stopService(Intent(context, KeepAliveService::class.java))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -16,7 +16,7 @@ import kotlinx.serialization.encodeToString
|
||||||
import li.songe.gkd.util.Singleton
|
import li.songe.gkd.util.Singleton
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
object Hook {
|
object CompositionExt {
|
||||||
fun CanOnDestroy.useScope(context: CoroutineContext = Dispatchers.Default): CoroutineScope {
|
fun CanOnDestroy.useScope(context: CoroutineContext = Dispatchers.Default): CoroutineScope {
|
||||||
val scope = CoroutineScope(context)
|
val scope = CoroutineScope(context)
|
||||||
onDestroy { scope.cancel() }
|
onDestroy { scope.cancel() }
|
||||||
|
@ -39,6 +39,7 @@ object Hook {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val filter = IntentFilter(packageName)
|
val filter = IntentFilter(packageName)
|
||||||
|
|
||||||
val broadcastManager = LocalBroadcastManager.getInstance(this)
|
val broadcastManager = LocalBroadcastManager.getInstance(this)
|
||||||
broadcastManager.registerReceiver(receiver, filter)
|
broadcastManager.registerReceiver(receiver, filter)
|
||||||
val sendMessage: (InvokeMessage) -> Unit = { message ->
|
val sendMessage: (InvokeMessage) -> Unit = { message ->
|
|
@ -1,14 +1,15 @@
|
||||||
package li.songe.gkd.data
|
package li.songe.gkd.data
|
||||||
|
|
||||||
import android.view.accessibility.AccessibilityNodeInfo
|
import android.view.accessibility.AccessibilityNodeInfo
|
||||||
import li.songe.selector.GkdSelector
|
import li.songe.gkd.selector.querySelector
|
||||||
|
import li.songe.selector_core.Selector
|
||||||
|
|
||||||
data class Rule(
|
data class Rule(
|
||||||
/**
|
/**
|
||||||
* length>0
|
* length>0
|
||||||
*/
|
*/
|
||||||
val matches: List<GkdSelector> = emptyList(),
|
val matches: List<Selector> = emptyList(),
|
||||||
val excludeMatches: List<GkdSelector> = emptyList(),
|
val excludeMatches: List<Selector> = emptyList(),
|
||||||
/**
|
/**
|
||||||
* 任意一个元素是上次触发过的
|
* 任意一个元素是上次触发过的
|
||||||
*/
|
*/
|
||||||
|
@ -34,11 +35,11 @@ data class Rule(
|
||||||
fun query(nodeInfo: AccessibilityNodeInfo?): AccessibilityNodeInfo? {
|
fun query(nodeInfo: AccessibilityNodeInfo?): AccessibilityNodeInfo? {
|
||||||
if (nodeInfo == null) return null
|
if (nodeInfo == null) return null
|
||||||
var target: AccessibilityNodeInfo? = null
|
var target: AccessibilityNodeInfo? = null
|
||||||
for (gkd in matches) {
|
for (selector in matches) {
|
||||||
target = gkd.collect(nodeInfo) ?: return null
|
target = nodeInfo.querySelector(selector) ?: return null
|
||||||
}
|
}
|
||||||
for (gkd in excludeMatches) {
|
for (selector in excludeMatches) {
|
||||||
if (gkd.collect(nodeInfo) != null) return null
|
if (nodeInfo.querySelector(selector) != null) return null
|
||||||
}
|
}
|
||||||
return target
|
return target
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package li.songe.gkd.data
|
package li.songe.gkd.data
|
||||||
|
|
||||||
import li.songe.selector.GkdSelector
|
import li.songe.selector_core.Selector
|
||||||
|
|
||||||
class RuleManager(vararg subscriptionRawArray: SubscriptionRaw) {
|
class RuleManager(vararg subscriptionRawArray: SubscriptionRaw) {
|
||||||
|
|
||||||
|
@ -42,13 +42,14 @@ class RuleManager(vararg subscriptionRawArray: SubscriptionRaw) {
|
||||||
?: appRaw.excludeActivityIds
|
?: appRaw.excludeActivityIds
|
||||||
?: emptyList()).toSet()
|
?: emptyList()).toSet()
|
||||||
|
|
||||||
|
|
||||||
ruleGroupList.add(
|
ruleGroupList.add(
|
||||||
Rule(
|
Rule(
|
||||||
cd = cd,
|
cd = cd,
|
||||||
index = count,
|
index = count,
|
||||||
matches = ruleRaw.matches.map { GkdSelector.gkdSelectorParser(it) },
|
matches = ruleRaw.matches.map { Selector.parse(it) },
|
||||||
excludeMatches = ruleRaw.excludeMatches.map {
|
excludeMatches = ruleRaw.excludeMatches.map {
|
||||||
GkdSelector.gkdSelectorParser(
|
Selector.parse(
|
||||||
it
|
it
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,7 +7,7 @@ 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.util.Singleton
|
||||||
import li.songe.selector.GkdSelector
|
import li.songe.selector_android.GkdSelector
|
||||||
|
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
@ -200,7 +200,7 @@ data class SubscriptionRaw(
|
||||||
|
|
||||||
fun stringify(source: SubscriptionRaw) = Singleton.json.encodeToString(source)
|
fun stringify(source: SubscriptionRaw) = Singleton.json.encodeToString(source)
|
||||||
|
|
||||||
private fun parse(source: String): SubscriptionRaw {
|
fun parse(source: String): SubscriptionRaw {
|
||||||
return jsonToSubscriptionRaw(Singleton.json.parseToJsonElement(source).jsonObject)
|
return jsonToSubscriptionRaw(Singleton.json.parseToJsonElement(source).jsonObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
3
app/src/main/java/li/songe/gkd/data/Value.kt
Normal file
3
app/src/main/java/li/songe/gkd/data/Value.kt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
package li.songe.gkd.data
|
||||||
|
|
||||||
|
data class Value<T>(var value: T)
|
|
@ -10,7 +10,7 @@ import li.songe.gkd.db.table.SubsItem
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
version = 3,
|
version = 1,
|
||||||
entities = [SubsItem::class, SubsConfig::class],
|
entities = [SubsItem::class, SubsConfig::class],
|
||||||
autoMigrations = [
|
autoMigrations = [
|
||||||
// AutoMigration(from = 1, to = 2),
|
// AutoMigration(from = 1, to = 2),
|
||||||
|
|
|
@ -5,7 +5,6 @@ import androidx.room.Insert
|
||||||
import androidx.room.RawQuery
|
import androidx.room.RawQuery
|
||||||
import androidx.room.Update
|
import androidx.room.Update
|
||||||
import androidx.sqlite.db.SupportSQLiteQuery
|
import androidx.sqlite.db.SupportSQLiteQuery
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
interface BaseDao<T : Any> {
|
interface BaseDao<T : Any> {
|
||||||
@Insert
|
@Insert
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package li.songe.gkd.db
|
package li.songe.gkd.db
|
||||||
|
|
||||||
interface BaseTable {
|
interface BaseTable {
|
||||||
var id: Long
|
val id: Long
|
||||||
var ctime: Long
|
val ctime: Long
|
||||||
var mtime: Long
|
val mtime: Long
|
||||||
}
|
}
|
35
app/src/main/java/li/songe/gkd/db/LogDatabase.kt
Normal file
35
app/src/main/java/li/songe/gkd/db/LogDatabase.kt
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,26 +11,25 @@ import li.songe.gkd.db.BaseTable
|
||||||
|
|
||||||
@Entity(
|
@Entity(
|
||||||
tableName = "subs_config",
|
tableName = "subs_config",
|
||||||
// indices = [Index(value = ["url"], unique = true)]
|
|
||||||
)
|
)
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class SubsConfig(
|
data class SubsConfig(
|
||||||
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") override var id: Long = 0,
|
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") override val id: Long = 0,
|
||||||
@ColumnInfo(name = "ctime") override var ctime: Long = System.currentTimeMillis(),
|
@ColumnInfo(name = "ctime") override val ctime: Long = System.currentTimeMillis(),
|
||||||
@ColumnInfo(name = "mtime") override var mtime: Long = System.currentTimeMillis(),
|
@ColumnInfo(name = "mtime") override val mtime: Long = System.currentTimeMillis(),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 0 - app
|
* 0 - app
|
||||||
* 1 - group
|
* 1 - group
|
||||||
* 2 - rule
|
* 2 - rule
|
||||||
*/
|
*/
|
||||||
@ColumnInfo(name = "type") var type: Int = 0,
|
@ColumnInfo(name = "type") val type: Int = 0,
|
||||||
@ColumnInfo(name = "enable") var enable: Boolean = true,
|
@ColumnInfo(name = "enable") val enable: Boolean = true,
|
||||||
|
|
||||||
@ColumnInfo(name = "subs_item_id") var subsItemId: Long = -1,
|
@ColumnInfo(name = "subs_item_id") val subsItemId: Long = -1,
|
||||||
@ColumnInfo(name = "app_id") var appId: String = "",
|
@ColumnInfo(name = "app_id") val appId: String = "",
|
||||||
@ColumnInfo(name = "group_key") var groupKey: Int = -1,
|
@ColumnInfo(name = "group_key") val groupKey: Int = -1,
|
||||||
@ColumnInfo(name = "rule_key") var ruleKey: Int = -1,
|
@ColumnInfo(name = "rule_key") val ruleKey: Int = -1,
|
||||||
) : BaseTable, Parcelable {
|
) : BaseTable, Parcelable {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -19,30 +19,36 @@ data class SubsItem(
|
||||||
/**
|
/**
|
||||||
* 当主键是0时,autoGenerate将覆盖此字段,插入数据库后 需要用返回值手动更新此字段
|
* 当主键是0时,autoGenerate将覆盖此字段,插入数据库后 需要用返回值手动更新此字段
|
||||||
*/
|
*/
|
||||||
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") override var id: Long = 0,
|
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") override val id: Long = 0,
|
||||||
@ColumnInfo(name = "ctime") override var ctime: Long = System.currentTimeMillis(),
|
@ColumnInfo(name = "ctime") override val ctime: Long = System.currentTimeMillis(),
|
||||||
@ColumnInfo(name = "mtime") override var mtime: Long = System.currentTimeMillis(),
|
@ColumnInfo(name = "mtime") override val mtime: Long = System.currentTimeMillis(),
|
||||||
|
|
||||||
@ColumnInfo(name = "enable") var enable: Boolean = true,
|
@ColumnInfo(name = "enable") val enable: Boolean = true,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户自定义备注
|
* 订阅文件 name 属性
|
||||||
*/
|
*/
|
||||||
@ColumnInfo(name = "name") var name: String = "",
|
@ColumnInfo(name = "name") val name: String = "",
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 订阅文件下载地址,也是更新链接
|
* 订阅文件下载地址,也是更新链接
|
||||||
*/
|
*/
|
||||||
@ColumnInfo(name = "update_url") var updateUrl: String,
|
@ColumnInfo(name = "update_url") val updateUrl: String = "",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅文件下载地址,也是更新链接
|
||||||
|
*/
|
||||||
|
@ColumnInfo(name = "version") val version: Int = 0,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 订阅文件下载后存放的路径
|
* 订阅文件下载后存放的路径
|
||||||
*/
|
*/
|
||||||
@ColumnInfo(name = "file_path") var filePath: String,
|
@ColumnInfo(name = "file_path") val filePath: String = "",
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 顺序
|
* 顺序
|
||||||
*/
|
*/
|
||||||
@ColumnInfo(name = "index") var index: Int=0,
|
@ColumnInfo(name = "index") val index: Int = 0,
|
||||||
|
|
||||||
|
|
||||||
) : Parcelable, BaseTable {
|
) : Parcelable, BaseTable {
|
||||||
|
|
26
app/src/main/java/li/songe/gkd/db/table/TriggerLog.kt
Normal file
26
app/src/main/java/li/songe/gkd/db/table/TriggerLog.kt
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
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>
|
||||||
|
}
|
|
@ -18,37 +18,37 @@ object Operator {
|
||||||
operator: String,
|
operator: String,
|
||||||
) =
|
) =
|
||||||
Expression(
|
Expression(
|
||||||
RoomAnnotation.getColumnName(T::class.java.name, name),
|
RoomAnnotation.getColumnName(T::class, name),
|
||||||
operator,
|
operator,
|
||||||
value,
|
value,
|
||||||
T::class
|
T::class
|
||||||
)
|
)
|
||||||
|
|
||||||
inline infix fun <reified T : Any, reified V> KMutableProperty1<T, V>.eq(value: V) =
|
inline infix fun <reified T : Any, reified V> KProperty1<T, V>.eq(value: V) =
|
||||||
baseOperator(value, "==")
|
baseOperator(value, "==")
|
||||||
|
|
||||||
inline infix fun <reified T : Any, reified V> KMutableProperty1<T, V>.neq(value: V) =
|
inline infix fun <reified T : Any, reified V> KProperty1<T, V>.neq(value: V) =
|
||||||
baseOperator(value, "!=")
|
baseOperator(value, "!=")
|
||||||
|
|
||||||
inline infix fun <reified T : Any, reified V> KMutableProperty1<T, V>.less(value: V) =
|
inline infix fun <reified T : Any, reified V> KProperty1<T, V>.less(value: V) =
|
||||||
baseOperator(value, "<")
|
baseOperator(value, "<")
|
||||||
|
|
||||||
inline infix fun <reified T : Any, reified V> KMutableProperty1<T, V>.lessEq(value: V) =
|
inline infix fun <reified T : Any, reified V> KProperty1<T, V>.lessEq(value: V) =
|
||||||
baseOperator(value, "<=")
|
baseOperator(value, "<=")
|
||||||
|
|
||||||
inline infix fun <reified T : Any, reified V> KMutableProperty1<T, V>.greater(value: V) =
|
inline infix fun <reified T : Any, reified V> KProperty1<T, V>.greater(value: V) =
|
||||||
baseOperator(value, ">")
|
baseOperator(value, ">")
|
||||||
|
|
||||||
inline infix fun <reified T : Any, reified V> KMutableProperty1<T, V>.greaterEq(value: V) =
|
inline infix fun <reified T : Any, reified V> KProperty1<T, V>.greaterEq(value: V) =
|
||||||
baseOperator(value, ">=")
|
baseOperator(value, ">=")
|
||||||
|
|
||||||
inline infix fun <reified T : Any, reified V> KMutableProperty1<T, V>.inList(value: List<V>) =
|
inline infix fun <reified T : Any, reified V> KProperty1<T, V>.inList(value: List<V>) =
|
||||||
baseOperator(value, "IN")
|
baseOperator(value, "IN")
|
||||||
|
|
||||||
inline infix fun <reified T : Any, reified V> KMutableProperty1<T, V>.glob(value: GlobString) =
|
inline infix fun <reified T : Any, reified V> KProperty1<T, V>.glob(value: GlobString) =
|
||||||
baseOperator(value, "GLOB")
|
baseOperator(value, "GLOB")
|
||||||
|
|
||||||
inline infix fun <reified T : Any, reified V> KMutableProperty1<T, V>.like(value: LikeString) =
|
inline infix fun <reified T : Any, reified V> KProperty1<T, V>.like(value: LikeString) =
|
||||||
baseOperator(value, "LIKE")
|
baseOperator(value, "LIKE")
|
||||||
|
|
||||||
inline fun <reified T : Any, V, V2> KProperty1<T, V>.baseOperator(
|
inline fun <reified T : Any, V, V2> KProperty1<T, V>.baseOperator(
|
||||||
|
@ -56,7 +56,7 @@ object Operator {
|
||||||
operator: String,
|
operator: String,
|
||||||
) =
|
) =
|
||||||
Expression(
|
Expression(
|
||||||
RoomAnnotation.getColumnName(T::class.java.name, name),
|
RoomAnnotation.getColumnName(T::class, name),
|
||||||
operator,
|
operator,
|
||||||
value,
|
value,
|
||||||
T::class
|
T::class
|
||||||
|
|
|
@ -1,42 +1,56 @@
|
||||||
package li.songe.gkd.db.util
|
package li.songe.gkd.db.util
|
||||||
|
|
||||||
import java.lang.Exception
|
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 {
|
object RoomAnnotation {
|
||||||
fun getTableName(className: String): String = when (className) {
|
|
||||||
"li.songe.gkd.db.table.SubsConfig" -> "subs_config"
|
fun getTableName(cls: KClass<*>): String = when (cls) {
|
||||||
"li.songe.gkd.db.table.SubsItem" -> "subs_item"
|
SubsConfig::class -> "subs_config"
|
||||||
"r-1682430013322" -> "avoid_compile_error"
|
SubsItem::class -> "subs_item"
|
||||||
else -> throw Exception("""not found className : $className""")
|
TriggerLog::class -> "trigger_log"
|
||||||
|
else -> throw Exception("""not found className : ${cls.qualifiedName}""")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getColumnName(className: String, propertyName: String): String = when (className) {
|
fun getColumnName(cls: KClass<*>, propertyName: String): String = when (cls) {
|
||||||
"li.songe.gkd.db.table.SubsConfig" -> when (propertyName) {
|
SubsConfig::class -> when (propertyName) {
|
||||||
"id" -> "id"
|
SubsConfig::id.name -> "id"
|
||||||
"ctime" -> "ctime"
|
SubsConfig::ctime.name -> "ctime"
|
||||||
"mtime" -> "mtime"
|
SubsConfig::mtime.name -> "mtime"
|
||||||
"type" -> "type"
|
SubsConfig::type.name -> "type"
|
||||||
"enable" -> "enable"
|
SubsConfig::enable.name -> "enable"
|
||||||
"subsItemId" -> "subs_item_id"
|
SubsConfig::subsItemId.name -> "subs_item_id"
|
||||||
"appId" -> "app_id"
|
SubsConfig::appId.name -> "app_id"
|
||||||
"groupKey" -> "group_key"
|
SubsConfig::groupKey.name -> "group_key"
|
||||||
"ruleKey" -> "rule_key"
|
SubsConfig::ruleKey.name -> "rule_key"
|
||||||
"r-1682430013322" -> "avoid_compile_error"
|
else -> error("""not found columnName : ${cls.qualifiedName}#$propertyName""")
|
||||||
else -> throw Exception("""not found columnName : $className#$propertyName""")
|
|
||||||
}
|
}
|
||||||
"li.songe.gkd.db.table.SubsItem" -> when (propertyName) {
|
|
||||||
"id" -> "id"
|
SubsItem::class -> when (propertyName) {
|
||||||
"ctime" -> "ctime"
|
SubsItem::id.name -> "id"
|
||||||
"mtime" -> "mtime"
|
SubsItem::ctime.name -> "ctime"
|
||||||
"enable" -> "enable"
|
SubsItem::mtime.name -> "mtime"
|
||||||
"name" -> "name"
|
SubsItem::enable.name -> "enable"
|
||||||
"updateUrl" -> "update_url"
|
SubsItem::name.name -> "name"
|
||||||
"filePath" -> "file_path"
|
SubsItem::updateUrl.name -> "update_url"
|
||||||
"index" -> "index"
|
SubsItem::filePath.name -> "file_path"
|
||||||
"r-1682430013322" -> "avoid_compile_error"
|
SubsItem::index.name -> "index"
|
||||||
else -> throw Exception("""not found columnName : $className#$propertyName""")
|
else -> error("""not found columnName : ${cls.qualifiedName}#$propertyName""")
|
||||||
}
|
}
|
||||||
"r-1682430013322" -> "avoid_compile_error"
|
|
||||||
else -> throw Exception("""not found className : $className""")
|
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}""")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -3,7 +3,7 @@ package li.songe.gkd.db.util
|
||||||
import androidx.sqlite.db.SimpleSQLiteQuery
|
import androidx.sqlite.db.SimpleSQLiteQuery
|
||||||
import li.songe.gkd.db.AppDatabase.Companion.db
|
import li.songe.gkd.db.AppDatabase.Companion.db
|
||||||
import li.songe.gkd.db.BaseDao
|
import li.songe.gkd.db.BaseDao
|
||||||
import li.songe.gkd.db.BaseTable
|
import li.songe.gkd.db.LogDatabase.Companion.logDb
|
||||||
import li.songe.gkd.db.table.*
|
import li.songe.gkd.db.table.*
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
@ -13,37 +13,13 @@ object RoomX {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
fun <T : Any> getBaseDao(cls: KClass<T>) = when (cls) {
|
fun <T : Any> getBaseDao(cls: KClass<T>) = when (cls) {
|
||||||
SubsItem::class -> db.subsItemRoomDao()
|
SubsItem::class -> db.subsItemRoomDao()
|
||||||
// SubsAppItem::class -> db.subsAppItemRoomDao()
|
|
||||||
// SubsGroupItem::class -> db.subsGroupItemRoomDao()
|
|
||||||
// SubsRuleItem::class -> db.subsRuleItemRoomDao()
|
|
||||||
SubsConfig::class -> db.subsConfigRoomDao()
|
SubsConfig::class -> db.subsConfigRoomDao()
|
||||||
else -> throw Exception("not found class dao : ${cls::class.java.name}")
|
TriggerLog::class -> logDb.triggerLogRoomDao()
|
||||||
|
else -> error("not found class dao : ${cls::class.java.name}")
|
||||||
} as BaseDao<T>
|
} as BaseDao<T>
|
||||||
|
|
||||||
fun databaseBeforeHook(vararg objects: Any) {
|
|
||||||
objects.forEach { /**/ when (it) {
|
|
||||||
is BaseTable -> {
|
|
||||||
it.mtime = System.currentTimeMillis()
|
|
||||||
}
|
|
||||||
else -> throw Exception("not found table class hook : ${it::class.java.name}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun databaseInsertAfterHook(objects: Array<out Any>, idList: List<Long>) {
|
|
||||||
objects.forEachIndexed { index, any -> /**/ when (any) {
|
|
||||||
is BaseTable -> {
|
|
||||||
// 插入数据后更新实体类的id
|
|
||||||
any.id = idList[index]
|
|
||||||
}
|
|
||||||
else -> throw Exception("not found table class hook : ${any::class.java.name}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend inline fun <reified T : Any> update(vararg objects: T): Int {
|
suspend inline fun <reified T : Any> update(vararg objects: T): Int {
|
||||||
databaseBeforeHook(*objects)
|
|
||||||
return getBaseDao(T::class).update(*objects)
|
return getBaseDao(T::class).update(*objects)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,10 +27,7 @@ object RoomX {
|
||||||
* 插入成功后, 自动改变入参对象的 id
|
* 插入成功后, 自动改变入参对象的 id
|
||||||
*/
|
*/
|
||||||
suspend inline fun <reified T : Any> insert(vararg objects: T): List<Long> {
|
suspend inline fun <reified T : Any> insert(vararg objects: T): List<Long> {
|
||||||
databaseBeforeHook(*objects)
|
return getBaseDao(T::class).insert(*objects)
|
||||||
return getBaseDao(T::class).insert(*objects).apply {
|
|
||||||
databaseInsertAfterHook(objects, this)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend inline fun <reified T : Any> delete(vararg objects: T) =
|
suspend inline fun <reified T : Any> delete(vararg objects: T) =
|
||||||
|
@ -66,7 +39,7 @@ object RoomX {
|
||||||
noinline block: (() -> Expression<*, *, T>)? = null
|
noinline block: (() -> Expression<*, *, T>)? = null
|
||||||
): List<T> {
|
): List<T> {
|
||||||
val expression = block?.invoke()
|
val expression = block?.invoke()
|
||||||
val tableName = RoomAnnotation.getTableName(T::class.java.name)
|
val tableName = RoomAnnotation.getTableName(T::class)
|
||||||
val sqlString = "SELECT * FROM $tableName" + (if (expression != null) {
|
val sqlString = "SELECT * FROM $tableName" + (if (expression != null) {
|
||||||
" WHERE ${expression.stringify()}"
|
" WHERE ${expression.stringify()}"
|
||||||
} else {
|
} else {
|
||||||
|
@ -90,7 +63,7 @@ object RoomX {
|
||||||
noinline block: (() -> Expression<*, *, T>)? = null
|
noinline block: (() -> Expression<*, *, T>)? = null
|
||||||
): List<Int> {
|
): List<Int> {
|
||||||
val expression = block?.invoke()
|
val expression = block?.invoke()
|
||||||
val tableName = RoomAnnotation.getTableName(T::class.java.name)
|
val tableName = RoomAnnotation.getTableName(T::class)
|
||||||
val sqlString = "DELETE FROM $tableName" + (if (expression != null) {
|
val sqlString = "DELETE FROM $tableName" + (if (expression != null) {
|
||||||
" WHERE ${expression.stringify()}"
|
" WHERE ${expression.stringify()}"
|
||||||
} else {
|
} else {
|
||||||
|
@ -107,42 +80,5 @@ object RoomX {
|
||||||
val baseDao = getBaseDao(T::class)
|
val baseDao = getBaseDao(T::class)
|
||||||
return baseDao.delete(SimpleSQLiteQuery(sqlString))
|
return baseDao.delete(SimpleSQLiteQuery(sqlString))
|
||||||
}
|
}
|
||||||
|
|
||||||
// inline fun <reified T : Any> selectFlow(
|
|
||||||
// limit: Int? = null,
|
|
||||||
// offset: Int? = null,
|
|
||||||
// noinline block: (() -> Expression<*, *, T>)? = null
|
|
||||||
// ): Flow<List<T>> {
|
|
||||||
// val expression = block?.invoke()
|
|
||||||
// val tableName = RoomAnnotation.getTableName(T::class.java.name)
|
|
||||||
// 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.queryFlow(SimpleSQLiteQuery(sqlString))
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
// fun testExample() = runBlocking {
|
|
||||||
// select { SubsItem::filePath like likeString().any(".json") }.forEach {
|
|
||||||
// LogUtils.d(it)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// selectFlow { SubsItem::description like likeString().any(".json") }.distinctUntilChanged()
|
|
||||||
// .collect {
|
|
||||||
// LogUtils.d(it.firstOrNull())
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ 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.CompositionFbService
|
import li.songe.gkd.composition.CompositionFbService
|
||||||
import li.songe.gkd.composition.Hook.useMessage
|
import li.songe.gkd.composition.CompositionExt.useMessage
|
||||||
import li.songe.gkd.composition.InvokeMessage
|
import li.songe.gkd.composition.InvokeMessage
|
||||||
import li.songe.gkd.debug.server.HttpService
|
import li.songe.gkd.debug.server.HttpService
|
||||||
|
|
||||||
|
|
|
@ -31,13 +31,14 @@ import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
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.Hook.useMessage
|
import li.songe.gkd.composition.CompositionExt.useMessage
|
||||||
import li.songe.gkd.composition.InvokeMessage
|
import li.songe.gkd.composition.InvokeMessage
|
||||||
import li.songe.gkd.debug.Ext.captureSnapshot
|
import li.songe.gkd.debug.Ext.captureSnapshot
|
||||||
import li.songe.gkd.debug.Ext.screenshotDir
|
import li.songe.gkd.debug.Ext.screenshotDir
|
||||||
import li.songe.gkd.debug.Ext.snapshotDir
|
import li.songe.gkd.debug.Ext.snapshotDir
|
||||||
import li.songe.gkd.debug.Ext.windowDir
|
import li.songe.gkd.debug.Ext.windowDir
|
||||||
import li.songe.gkd.debug.FloatingService
|
import li.songe.gkd.debug.FloatingService
|
||||||
|
import li.songe.gkd.debug.server.api.Device
|
||||||
import li.songe.gkd.util.Ext.getIpAddressInLocalNetwork
|
import li.songe.gkd.util.Ext.getIpAddressInLocalNetwork
|
||||||
import li.songe.gkd.util.Singleton
|
import li.songe.gkd.util.Singleton
|
||||||
import li.songe.gkd.util.Storage
|
import li.songe.gkd.util.Storage
|
||||||
|
@ -78,24 +79,26 @@ class HttpService : CompositionService({
|
||||||
|
|
||||||
routing {
|
routing {
|
||||||
route("/api/rpc") {
|
route("/api/rpc") {
|
||||||
|
get("/device") {
|
||||||
|
call.respond(Device.singleton)
|
||||||
|
}
|
||||||
get("/capture") {
|
get("/capture") {
|
||||||
removeBubbles()
|
removeBubbles()
|
||||||
delay(200)
|
delay(200)
|
||||||
try {
|
try {
|
||||||
call.respond(captureSnapshot())
|
call.respond(captureSnapshot())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
showBubbles()
|
|
||||||
throw e
|
throw e
|
||||||
}
|
} finally {
|
||||||
showBubbles()
|
showBubbles()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
get("/snapshot") {
|
get("/snapshot") {
|
||||||
val id = call.request.queryParameters["id"]?.toLongOrNull()
|
val id = call.request.queryParameters["id"]?.toLongOrNull()
|
||||||
?: throw RpcError("miss id")
|
?: throw RpcError("miss id")
|
||||||
call.response.cacheControl(CacheControl.MaxAge(3600))
|
call.response.cacheControl(CacheControl.MaxAge(3600))
|
||||||
call.respondFile(snapshotDir, "/${id}.json")
|
call.respondFile(snapshotDir, "/${id}.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
get("/window") {
|
get("/window") {
|
||||||
val id = call.request.queryParameters["id"]?.toLongOrNull()
|
val id = call.request.queryParameters["id"]?.toLongOrNull()
|
||||||
?: throw RpcError("miss id")
|
?: throw RpcError("miss id")
|
||||||
|
@ -110,7 +113,6 @@ class HttpService : CompositionService({
|
||||||
call.respondFile(screenshotDir, "/${id}.png")
|
call.respondFile(screenshotDir, "/${id}.png")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
listOf("/", "/index.html").forEach { p ->
|
listOf("/", "/index.html").forEach { p ->
|
||||||
get(p) {
|
get(p) {
|
||||||
val response = Singleton.client.get("$proxyUrl${call.request.uri}")
|
val response = Singleton.client.get("$proxyUrl${call.request.uri}")
|
||||||
|
|
|
@ -8,7 +8,7 @@ data class RpcError(
|
||||||
val code: Int = 0,
|
val code: Int = 0,
|
||||||
) : Exception(message) {
|
) : Exception(message) {
|
||||||
companion object {
|
companion object {
|
||||||
const val HeaderKey = "X-Rpc-Result"
|
const val HeaderKey = "X_Rpc_Result"
|
||||||
const val HeaderOkValue = "ok"
|
const val HeaderOkValue = "ok"
|
||||||
const val HeaderErrorValue = "error"
|
const val HeaderErrorValue = "error"
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,23 +11,32 @@ import io.ktor.server.response.respond
|
||||||
|
|
||||||
val RpcErrorHeaderPlugin = createApplicationPlugin(name = "RpcErrorHeaderPlugin") {
|
val RpcErrorHeaderPlugin = createApplicationPlugin(name = "RpcErrorHeaderPlugin") {
|
||||||
onCall { call ->
|
onCall { call ->
|
||||||
Log.d("Ktor", "Request Path: ${call.request.uri}")
|
Log.d("Ktor", "onCall: ${call.request.uri}")
|
||||||
}
|
}
|
||||||
on(CallFailed) { call, cause ->
|
on(CallFailed) { call, cause ->
|
||||||
if (cause is RpcError) {
|
when (cause) {
|
||||||
|
is RpcError -> {
|
||||||
// 主动抛出的错误
|
// 主动抛出的错误
|
||||||
LogUtils.d(call.request.uri, cause.code, cause.message)
|
LogUtils.d(call.request.uri, cause.code, cause.message)
|
||||||
call.response.header(RpcError.HeaderKey, RpcError.HeaderErrorValue)
|
call.response.header(RpcError.HeaderKey, RpcError.HeaderErrorValue)
|
||||||
call.respond(cause)
|
call.respond(cause)
|
||||||
} else if (cause is Exception) {
|
}
|
||||||
|
|
||||||
|
is Exception -> {
|
||||||
// 未知错误
|
// 未知错误
|
||||||
LogUtils.d(call.request.uri, cause.message)
|
LogUtils.d(call.request.uri, cause.message)
|
||||||
cause.printStackTrace()
|
cause.printStackTrace()
|
||||||
call.respond(HttpStatusCode.InternalServerError, cause)
|
call.respond(HttpStatusCode.InternalServerError, cause)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
cause.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
onCallRespond { call, _ ->
|
onCallRespond { call, _ ->
|
||||||
if (call.response.status() == HttpStatusCode.OK &&
|
val status=call.response.status() ?: HttpStatusCode.OK
|
||||||
|
if (status == HttpStatusCode.OK &&
|
||||||
!call.response.headers.contains(
|
!call.response.headers.contains(
|
||||||
RpcError.HeaderKey
|
RpcError.HeaderKey
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,12 +6,12 @@ import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Attr(
|
data class Attr(
|
||||||
val id: String?,
|
val id: String? = null,
|
||||||
val className: String?,
|
val className: String? = null,
|
||||||
val childCount: Int,
|
val childCount: Int = 0,
|
||||||
val text: String?,
|
val text: String? = null,
|
||||||
val isClickable: Boolean,
|
val isClickable: Boolean = false,
|
||||||
val desc: String?,
|
val desc: String? = null,
|
||||||
val left: Int,
|
val left: Int,
|
||||||
val top: Int,
|
val top: Int,
|
||||||
val right: Int,
|
val right: Int,
|
||||||
|
|
18
app/src/main/java/li/songe/gkd/debug/server/api/Device.kt
Normal file
18
app/src/main/java/li/songe/gkd/debug/server/api/Device.kt
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
package li.songe.gkd.debug.server.api
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Device(
|
||||||
|
val device: String = Build.DEVICE,
|
||||||
|
val model: String = Build.MODEL,
|
||||||
|
val manufacturer: String = Build.MANUFACTURER,
|
||||||
|
val brand: String = Build.BRAND,
|
||||||
|
val sdkInt: Int = Build.VERSION.SDK_INT,
|
||||||
|
val release: String = Build.VERSION.RELEASE,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
val singleton by lazy { Device() }
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,7 @@ package li.songe.gkd.debug.server.api
|
||||||
|
|
||||||
import android.view.accessibility.AccessibilityNodeInfo
|
import android.view.accessibility.AccessibilityNodeInfo
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import li.songe.selector.forEach
|
import li.songe.selector_android.forEach
|
||||||
import java.util.ArrayDeque
|
import java.util.ArrayDeque
|
||||||
|
|
||||||
|
|
||||||
|
|
56
app/src/main/java/li/songe/gkd/hooks/Ext.kt
Normal file
56
app/src/main/java/li/songe/gkd/hooks/Ext.kt
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
package li.songe.gkd.hooks
|
||||||
|
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
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 com.blankj.utilcode.util.LogUtils
|
||||||
|
import com.journeyapps.barcodescanner.ScanContract
|
||||||
|
import com.journeyapps.barcodescanner.ScanIntentResult
|
||||||
|
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.util.Singleton
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun useNavigateForQrcodeResult(): suspend () -> ScanIntentResult {
|
||||||
|
val resolve = remember {
|
||||||
|
Value { _: ScanIntentResult -> }
|
||||||
|
}
|
||||||
|
val scanLauncher =
|
||||||
|
rememberLauncherForActivityResult(ScanContract()) { result ->
|
||||||
|
resolve.value(result)
|
||||||
|
}
|
||||||
|
return remember {
|
||||||
|
suspend {
|
||||||
|
scanLauncher.launch(ScanOptions().apply {
|
||||||
|
setOrientationLocked(false)
|
||||||
|
setBeepEnabled(false)
|
||||||
|
})
|
||||||
|
suspendCoroutine { continuation ->
|
||||||
|
resolve.value = { s -> continuation.resume(s) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun useFetchSubs(): suspend (String) -> String {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
var loading by remember { mutableStateOf(false) }
|
||||||
|
return remember {
|
||||||
|
{ url ->
|
||||||
|
loading
|
||||||
|
Singleton.client.get(url).bodyAsText()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
40
app/src/main/java/li/songe/gkd/selector/AbNode.kt
Normal file
40
app/src/main/java/li/songe/gkd/selector/AbNode.kt
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package li.songe.gkd.selector
|
||||||
|
|
||||||
|
import android.view.accessibility.AccessibilityNodeInfo
|
||||||
|
import li.songe.selector_core.Node
|
||||||
|
|
||||||
|
@JvmInline
|
||||||
|
value class AbNode(val value: AccessibilityNodeInfo) : Node {
|
||||||
|
override val parent: Node?
|
||||||
|
get() = value.parent?.let { AbNode(it) }
|
||||||
|
override val children: Sequence<Node?>
|
||||||
|
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
|
||||||
|
"isChecked" -> value.isChecked
|
||||||
|
"index" -> value.getIndex()
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
34
app/src/main/java/li/songe/gkd/selector/AbNodeExt.kt
Normal file
34
app/src/main/java/li/songe/gkd/selector/AbNodeExt.kt
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
package li.songe.gkd.selector
|
||||||
|
|
||||||
|
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 null
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
if (child != null) {
|
||||||
|
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) =
|
||||||
|
(AbNode(this).querySelectorAll(selector) as Sequence<AbNode>)
|
|
@ -17,7 +17,6 @@ import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
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
|
||||||
|
@ -44,10 +43,10 @@ val DebugPage = Page {
|
||||||
val launcher = LocalLauncher.current
|
val launcher = LocalLauncher.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
var httpServerRunning by usePollState { HttpService.isRunning() }
|
val httpServerRunning by usePollState { HttpService.isRunning() }
|
||||||
var screenshotRunning by usePollState { ScreenshotService.isRunning() }
|
val screenshotRunning by usePollState { ScreenshotService.isRunning() }
|
||||||
var gkdAccessRunning by usePollState { GkdAbService.isRunning() }
|
val gkdAccessRunning by usePollState { GkdAbService.isRunning() }
|
||||||
var floatingRunning by usePollState {
|
val floatingRunning by usePollState {
|
||||||
FloatingService.isRunning() && Settings.canDrawOverlays(
|
FloatingService.isRunning() && Settings.canDrawOverlays(
|
||||||
context
|
context
|
||||||
)
|
)
|
||||||
|
@ -55,7 +54,7 @@ val DebugPage = Page {
|
||||||
|
|
||||||
|
|
||||||
val debugAvailable by remember {
|
val debugAvailable by remember {
|
||||||
derivedStateOf { httpServerRunning && screenshotRunning && gkdAccessRunning }
|
derivedStateOf { httpServerRunning }
|
||||||
}
|
}
|
||||||
|
|
||||||
val serverUrl by remember {
|
val serverUrl by remember {
|
||||||
|
@ -132,8 +131,8 @@ val DebugPage = Page {
|
||||||
launcher.launch(intent) { resultCode, _ ->
|
launcher.launch(intent) { resultCode, _ ->
|
||||||
if (resultCode != ComponentActivity.RESULT_OK) return@launch
|
if (resultCode != ComponentActivity.RESULT_OK) return@launch
|
||||||
if (!Settings.canDrawOverlays(context)) return@launch
|
if (!Settings.canDrawOverlays(context)) return@launch
|
||||||
val intent = Intent(context, FloatingService::class.java)
|
val intent1 = Intent(context, FloatingService::class.java)
|
||||||
ContextCompat.startForegroundService(context, intent)
|
ContextCompat.startForegroundService(context, intent1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -11,6 +11,9 @@ import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.material.Surface
|
import androidx.compose.material.Surface
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
|
@ -20,31 +23,34 @@ 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.R
|
||||||
import li.songe.gkd.db.table.SubsItem
|
import li.songe.gkd.db.table.SubsItem
|
||||||
|
import li.songe.gkd.util.Singleton
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SubsItemCard(
|
fun SubsItemCard(
|
||||||
data: SubsItem,
|
subsItem: SubsItem,
|
||||||
onShareClick: (() -> Unit)? = null,
|
onShareClick: (() -> Unit)? = null,
|
||||||
onEditClick: (() -> Unit)? = null,
|
onEditClick: (() -> Unit)? = null,
|
||||||
onDelClick: (() -> Unit)? = null,
|
onDelClick: (() -> Unit)? = null,
|
||||||
onRefreshClick: (() -> Unit)? = null,
|
onRefreshClick: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
|
val dateStr by remember(subsItem) {
|
||||||
|
derivedStateOf { "更新于:" + Singleton.simpleDateFormat.format(subsItem.mtime) }
|
||||||
|
}
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
.alpha(if (data.enable) 1f else .3f),
|
.alpha(if (subsItem.enable) 1f else .3f),
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Text(
|
Text(
|
||||||
text = data.name,
|
text = subsItem.name,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
softWrap = false,
|
softWrap = false,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = data.updateUrl,
|
text = dateStr,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
softWrap = false,
|
softWrap = false,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
|
|
9
app/src/main/java/li/songe/gkd/ui/component/Tabs.kt
Normal file
9
app/src/main/java/li/songe/gkd/ui/component/Tabs.kt
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package li.songe.gkd.ui.component
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Tabs() {
|
||||||
|
val x = rememberCoroutineScope()
|
||||||
|
}
|
|
@ -1,7 +1,5 @@
|
||||||
package li.songe.gkd.ui.home
|
package li.songe.gkd.ui.home
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
||||||
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
|
||||||
|
@ -10,17 +8,20 @@ import androidx.compose.material.AlertDialog
|
||||||
import androidx.compose.material.Button
|
import androidx.compose.material.Button
|
||||||
import androidx.compose.material.Card
|
import androidx.compose.material.Card
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TextField
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
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 androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
|
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.journeyapps.barcodescanner.ScanContract
|
import com.google.zxing.BarcodeFormat
|
||||||
import com.journeyapps.barcodescanner.ScanOptions
|
|
||||||
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
|
||||||
|
@ -32,8 +33,10 @@ import li.songe.gkd.db.table.SubsConfig
|
||||||
import li.songe.gkd.db.table.SubsItem
|
import li.songe.gkd.db.table.SubsItem
|
||||||
import li.songe.gkd.db.util.Operator.eq
|
import li.songe.gkd.db.util.Operator.eq
|
||||||
import li.songe.gkd.db.util.RoomX
|
import li.songe.gkd.db.util.RoomX
|
||||||
|
import li.songe.gkd.hooks.useNavigateForQrcodeResult
|
||||||
import li.songe.gkd.ui.SubsPage
|
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.util.Singleton
|
import li.songe.gkd.util.Singleton
|
||||||
import li.songe.gkd.util.ThrottleState
|
import li.songe.gkd.util.ThrottleState
|
||||||
import li.songe.router.LocalRouter
|
import li.songe.router.LocalRouter
|
||||||
|
@ -42,40 +45,24 @@ import java.io.File
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SubscriptionManagePage() {
|
fun SubscriptionManagePage() {
|
||||||
// https://medium.com/androiddevelopers/multiple-back-stacks-b714d974f134
|
|
||||||
// https://medium.com/androiddevelopers/animations-in-navigation-compose-36d48870776b
|
|
||||||
// https://tigeroakes.com/posts/react-to-compose-dictionary/#storybook--preview
|
|
||||||
// https://google.github.io/accompanist/
|
|
||||||
// https://foso.github.io/Jetpack-Compose-Playground/
|
|
||||||
// https://www.jetpackcompose.net/
|
|
||||||
// https://jetpackcompose.cn/docs/
|
|
||||||
// https://developer.android.com/jetpack/compose/performance
|
|
||||||
|
|
||||||
val context = LocalContext.current as Activity
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val router = LocalRouter.current
|
val router = LocalRouter.current
|
||||||
|
|
||||||
var subItemList by remember { mutableStateOf(listOf<SubsItem>()) }
|
var subItemList by remember { mutableStateOf(listOf<SubsItem>()) }
|
||||||
var shareSubItem: SubsItem? by remember { mutableStateOf(null) }
|
var shareSubItem: SubsItem? by remember { mutableStateOf(null) }
|
||||||
|
var shareQrcode: ImageBitmap? by remember { mutableStateOf(null) }
|
||||||
var deleteSubItem: SubsItem? by remember { mutableStateOf(null) }
|
var deleteSubItem: SubsItem? by remember { mutableStateOf(null) }
|
||||||
|
|
||||||
val scanLauncher =
|
|
||||||
rememberLauncherForActivityResult(contract = ScanContract(), onResult = { result ->
|
|
||||||
if (result.contents != null) {
|
|
||||||
// scope.launch {
|
|
||||||
// val newSubsItem = router.navigateForResult(
|
|
||||||
// SubsItemInsertPage, SubsItem(filePath = "", updateUrl = result.contents)
|
|
||||||
// ) ?: return@launch
|
|
||||||
// subItemList = subItemList.toMutableList().apply { add(newSubsItem) }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
var showAddDialog by remember { mutableStateOf(false) }
|
var showAddDialog by remember { mutableStateOf(false) }
|
||||||
|
var showLinkInputDialog by remember { mutableStateOf(false) }
|
||||||
val viewSubItemThrottle = ThrottleState.use(scope)
|
val viewSubItemThrottle = ThrottleState.use(scope)
|
||||||
val editSubItemThrottle = ThrottleState.use(scope)
|
val editSubItemThrottle = ThrottleState.use(scope)
|
||||||
val refreshSubItemThrottle = ThrottleState.use(scope, 250)
|
val refreshSubItemThrottle = ThrottleState.use(scope, 250)
|
||||||
|
val navigateForQrcodeResult = useNavigateForQrcodeResult()
|
||||||
|
|
||||||
|
var linkText by remember {
|
||||||
|
mutableStateOf("")
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
subItemList = RoomX.select<SubsItem>().sortedBy { it.index }
|
subItemList = RoomX.select<SubsItem>().sortedBy { it.index }
|
||||||
|
@ -95,6 +82,7 @@ fun SubscriptionManagePage() {
|
||||||
Text(
|
Text(
|
||||||
text = "共有${subItemList.size}条订阅,激活:${subItemList.count { it.enable }},禁用:${subItemList.count { !it.enable }}",
|
text = "共有${subItemList.size}条订阅,激活:${subItemList.count { it.enable }},禁用:${subItemList.count { !it.enable }}",
|
||||||
)
|
)
|
||||||
|
Row {
|
||||||
Image(painter = painterResource(R.drawable.ic_add),
|
Image(painter = painterResource(R.drawable.ic_add),
|
||||||
contentDescription = "",
|
contentDescription = "",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
@ -103,17 +91,55 @@ fun SubscriptionManagePage() {
|
||||||
}
|
}
|
||||||
.padding(4.dp)
|
.padding(4.dp)
|
||||||
.size(25.dp))
|
.size(25.dp))
|
||||||
|
Image(painter = painterResource(R.drawable.ic_refresh),
|
||||||
|
contentDescription = "",
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable {
|
||||||
|
scope.launchTry {
|
||||||
|
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)
|
||||||
|
.size(25.dp))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items(subItemList.size, { i -> subItemList[i].hashCode() }) { i ->
|
items(subItemList.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 = viewSubItemThrottle.invoke {
|
.clickable(onClick = { router.navigate(SubsPage, subItemList[i]) }),
|
||||||
router.navigate(SubsPage, subItemList[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),
|
||||||
|
@ -121,29 +147,32 @@ fun SubscriptionManagePage() {
|
||||||
SubsItemCard(subItemList[i], onShareClick = {
|
SubsItemCard(subItemList[i], onShareClick = {
|
||||||
shareSubItem = subItemList[i]
|
shareSubItem = subItemList[i]
|
||||||
}, onEditClick = editSubItemThrottle.invoke {
|
}, onEditClick = editSubItemThrottle.invoke {
|
||||||
// val newSubsItem =
|
|
||||||
// router.navigateForResult(SubsItemUpdatePage, subItemList[i])
|
|
||||||
// ?: return@invoke
|
|
||||||
// subItemList = subItemList.toMutableList().apply {
|
|
||||||
// set(i, newSubsItem)
|
|
||||||
// }
|
|
||||||
}, onDelClick = {
|
}, onDelClick = {
|
||||||
deleteSubItem = subItemList[i]
|
deleteSubItem = subItemList[i]
|
||||||
}, onRefreshClick = refreshSubItemThrottle.invoke {
|
}, onRefreshClick = refreshSubItemThrottle.invoke {
|
||||||
|
val oldItem = subItemList[i]
|
||||||
val subscriptionRaw = SubscriptionRaw.parse5(
|
val subscriptionRaw = SubscriptionRaw.parse5(
|
||||||
Singleton.client.get(subItemList[i].updateUrl).bodyAsText()
|
Singleton.client.get(oldItem.updateUrl).bodyAsText()
|
||||||
)
|
)
|
||||||
subItemList = subItemList.toMutableList().also {
|
if (subscriptionRaw.version <= oldItem.version) {
|
||||||
it[i] = it[i].copy(
|
ToastUtils.showShort("暂无更新:${oldItem.name}")
|
||||||
updateUrl = subscriptionRaw.updateUrl
|
return@invoke
|
||||||
?: subItemList[i].updateUrl,
|
|
||||||
name = subscriptionRaw.name
|
|
||||||
)
|
|
||||||
RoomX.update(it[i])
|
|
||||||
val f = File(it[i].filePath)
|
|
||||||
f.writeText(SubscriptionRaw.stringify(subscriptionRaw))
|
|
||||||
}
|
}
|
||||||
ToastUtils.showShort("更新成功")
|
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 {
|
}.catch {
|
||||||
if (!it.message.isNullOrEmpty()) {
|
if (!it.message.isNullOrEmpty()) {
|
||||||
ToastUtils.showShort(it.message)
|
ToastUtils.showShort(it.message)
|
||||||
|
@ -153,7 +182,7 @@ fun SubscriptionManagePage() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shareSubItem != null) {
|
shareSubItem?.let { _shareSubItem ->
|
||||||
Dialog(onDismissRequest = { shareSubItem = null }) {
|
Dialog(onDismissRequest = { shareSubItem = null }) {
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
|
@ -164,12 +193,25 @@ fun SubscriptionManagePage() {
|
||||||
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
|
||||||
|
.encodeBitmap(
|
||||||
|
_shareSubItem.updateUrl,
|
||||||
|
BarcodeFormat.QR_CODE,
|
||||||
|
500,
|
||||||
|
500
|
||||||
|
)
|
||||||
|
.asImageBitmap()
|
||||||
|
shareSubItem = null
|
||||||
|
}
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(8.dp))
|
.padding(8.dp))
|
||||||
Text(text = "导出至剪切板",
|
Text(text = "导出至剪切板",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable { }
|
.clickable {
|
||||||
|
ClipboardUtils.copyText(_shareSubItem.updateUrl)
|
||||||
|
shareSubItem = null
|
||||||
|
}
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(8.dp))
|
.padding(8.dp))
|
||||||
}
|
}
|
||||||
|
@ -177,6 +219,16 @@ fun SubscriptionManagePage() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shareQrcode?.let { _shareQrcode ->
|
||||||
|
Dialog(onDismissRequest = { shareQrcode = null }) {
|
||||||
|
Image(
|
||||||
|
bitmap = _shareQrcode,
|
||||||
|
contentDescription = "qrcode",
|
||||||
|
modifier = Modifier.size(400.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
val delSubItemThrottle = ThrottleState.use(scope)
|
val delSubItemThrottle = ThrottleState.use(scope)
|
||||||
if (deleteSubItem != null) {
|
if (deleteSubItem != null) {
|
||||||
|
@ -198,7 +250,6 @@ fun SubscriptionManagePage() {
|
||||||
}
|
}
|
||||||
subItemList = subItemList.toMutableList().also { it.remove(deleteSubItem) }
|
subItemList = subItemList.toMutableList().also { it.remove(deleteSubItem) }
|
||||||
deleteSubItem = null
|
deleteSubItem = null
|
||||||
|
|
||||||
}) {
|
}) {
|
||||||
Text("是")
|
Text("是")
|
||||||
}
|
}
|
||||||
|
@ -213,7 +264,7 @@ fun SubscriptionManagePage() {
|
||||||
}
|
}
|
||||||
if (showAddDialog) {
|
if (showAddDialog) {
|
||||||
val clickQrcodeThrottle = ThrottleState.use(scope)
|
val clickQrcodeThrottle = ThrottleState.use(scope)
|
||||||
Dialog(onDismissRequest = { showAddDialog = false },) {
|
Dialog(onDismissRequest = { showAddDialog = false }) {
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.width(250.dp)
|
.width(250.dp)
|
||||||
|
@ -225,23 +276,20 @@ fun SubscriptionManagePage() {
|
||||||
text = "二维码", modifier = Modifier
|
text = "二维码", modifier = Modifier
|
||||||
.clickable(onClick = clickQrcodeThrottle.invoke {
|
.clickable(onClick = clickQrcodeThrottle.invoke {
|
||||||
showAddDialog = false
|
showAddDialog = false
|
||||||
scanLauncher.launch(ScanOptions().apply {
|
val qrCode = navigateForQrcodeResult()
|
||||||
setOrientationLocked(false)
|
val contents = qrCode.contents
|
||||||
})
|
if (contents != null) {
|
||||||
|
showLinkInputDialog = true
|
||||||
|
linkText = contents
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
)
|
)
|
||||||
Text(text = "链接", modifier = Modifier
|
Text(text = "链接", modifier = Modifier
|
||||||
.clickable {
|
.clickable {
|
||||||
|
showLinkInputDialog = true
|
||||||
showAddDialog = false
|
showAddDialog = false
|
||||||
scope.launch {
|
|
||||||
// val newSubsItem =
|
|
||||||
// router.navigateForResult(SubsItemInsertPage) ?: return@launch
|
|
||||||
// subItemList = subItemList
|
|
||||||
// .toMutableList()
|
|
||||||
// .apply { add(newSubsItem) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(8.dp))
|
.padding(8.dp))
|
||||||
|
@ -249,4 +297,68 @@ fun SubscriptionManagePage() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (showLinkInputDialog) {
|
||||||
|
Dialog(onDismissRequest = { showLinkInputDialog = false;linkText = "" }) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.width(250.dp)
|
||||||
|
.background(Color.White)
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
Text(text = "请输入订阅链接")
|
||||||
|
TextField(
|
||||||
|
value = linkText,
|
||||||
|
onValueChange = { linkText = it },
|
||||||
|
singleLine = true
|
||||||
|
)
|
||||||
|
Button(onClick = {
|
||||||
|
showLinkInputDialog = false
|
||||||
|
if (subItemList.any { it.updateUrl == linkText }) {
|
||||||
|
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 = "添加")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -16,11 +16,11 @@ 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 com.dylanc.activityresult.launcher.StartActivityLauncher
|
import com.dylanc.activityresult.launcher.StartActivityLauncher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.CoroutineStart
|
import kotlinx.coroutines.CoroutineStart
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -192,13 +192,26 @@ object Ext {
|
||||||
context: CoroutineContext = EmptyCoroutineContext,
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
block: suspend CoroutineScope.() -> Unit,
|
block: suspend CoroutineScope.() -> Unit,
|
||||||
): Job {
|
) = launch(context, start) {
|
||||||
return this.launch(context, start) {
|
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
block()
|
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_TEST"
|
val channelId = "CHANNEL_TEST"
|
||||||
|
|
|
@ -4,18 +4,18 @@ import blue.endless.jankson.Jankson
|
||||||
import com.journeyapps.barcodescanner.BarcodeEncoder
|
import com.journeyapps.barcodescanner.BarcodeEncoder
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.engine.cio.CIO
|
import io.ktor.client.engine.cio.CIO
|
||||||
|
import io.ktor.client.plugins.HttpTimeout
|
||||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||||
import io.ktor.http.ContentType
|
import io.ktor.http.ContentType
|
||||||
import io.ktor.serialization.kotlinx.json.json
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
object Singleton {
|
object Singleton {
|
||||||
|
|
||||||
// @OptIn(ExperimentalSerializationApi::class)
|
|
||||||
val json by lazy {
|
val json by lazy {
|
||||||
Json {
|
Json {
|
||||||
// prettyPrint = true
|
|
||||||
// prettyPrintIndent = "\u0020".repeat(2)
|
|
||||||
isLenient = true
|
isLenient = true
|
||||||
ignoreUnknownKeys = true
|
ignoreUnknownKeys = true
|
||||||
}
|
}
|
||||||
|
@ -26,21 +26,13 @@ object Singleton {
|
||||||
install(ContentNegotiation) {
|
install(ContentNegotiation) {
|
||||||
json(json, ContentType.Any)
|
json(json, ContentType.Any)
|
||||||
}
|
}
|
||||||
|
install(HttpTimeout){
|
||||||
|
connectTimeoutMillis = 3000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// inline fun <reified T : Any> produce(data: T, block: (data: T) -> Unit): T {
|
val simpleDateFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
|
||||||
// val proxyData = Proxy.newProxyInstance(
|
|
||||||
// T::class.java.classLoader,
|
|
||||||
// arrayOf(),
|
|
||||||
// InvocationHandler { proxy, method, args ->
|
|
||||||
//
|
|
||||||
// }) as T
|
|
||||||
// block(proxyData)
|
|
||||||
// return proxyData
|
|
||||||
// }
|
|
||||||
|
|
||||||
val barcodeEncoder by lazy { BarcodeEncoder() }
|
val barcodeEncoder by lazy { BarcodeEncoder() }
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,16 +1,11 @@
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<!-- Base application theme. -->
|
<!-- Base application theme. -->
|
||||||
<style name="Theme.Gkd" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
<style name="Theme.Gkd" parent="Theme.AppCompat">
|
||||||
<!-- Primary brand color. -->
|
<!-- Primary brand color. -->
|
||||||
<item name="colorPrimary">@color/purple_200</item>
|
<item name="colorPrimary">#ededed</item>
|
||||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
|
||||||
<item name="colorOnPrimary">@color/black</item>
|
|
||||||
<!-- Secondary brand color. -->
|
|
||||||
<item name="colorSecondary">@color/teal_200</item>
|
|
||||||
<item name="colorSecondaryVariant">@color/teal_200</item>
|
|
||||||
<item name="colorOnSecondary">@color/black</item>
|
|
||||||
<!-- Status bar color. -->
|
<!-- Status bar color. -->
|
||||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
<item name="android:statusBarColor" tools:targetApi="l">@android:color/transparent</item>
|
||||||
|
<item name="android:windowLightStatusBar">true</item>
|
||||||
<!-- Customize your theme here. -->
|
<!-- Customize your theme here. -->
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
|
@ -1,16 +1,10 @@
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<!-- Base application theme. -->
|
<!-- Base application theme. -->
|
||||||
<style name="Theme.Gkd" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
<style name="Theme.Gkd" parent="Theme.AppCompat">
|
||||||
<!-- Primary brand color. -->
|
<!-- Primary brand color. -->
|
||||||
<item name="colorPrimary">#ededed</item>
|
<item name="colorPrimary">#ededed</item>
|
||||||
<item name="colorPrimaryVariant">#ededed</item>
|
|
||||||
<item name="colorOnPrimary">@color/white</item>
|
|
||||||
<!-- Secondary brand color. -->
|
|
||||||
<item name="colorSecondary">@color/teal_200</item>
|
|
||||||
<item name="colorSecondaryVariant">@color/teal_700</item>
|
|
||||||
<item name="colorOnSecondary">@color/black</item>
|
|
||||||
<!-- Status bar color. -->
|
<!-- Status bar color. -->
|
||||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
<item name="android:statusBarColor" tools:targetApi="l">@android:color/transparent</item>
|
||||||
<item name="android:windowLightStatusBar">true</item>
|
<item name="android:windowLightStatusBar">true</item>
|
||||||
<!-- Customize your theme here. -->
|
<!-- Customize your theme here. -->
|
||||||
</style>
|
</style>
|
||||||
|
@ -23,12 +17,4 @@
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
<!-- <style name="Entry" parent="Theme.AppCompat.Light.NoActionBar">-->
|
|
||||||
<!-- <item name="android:windowFullscreen">true</item>-->
|
|
||||||
<!-- <item name="android:windowIsTranslucent">true</item>-->
|
|
||||||
<!-- <item name="android:statusBarColor">@android:color/transparent</item>-->
|
|
||||||
<!-- <item name="android:windowTranslucentNavigation">true</item>-->
|
|
||||||
<!-- <item name="background">@android:color/transparent</item>-->
|
|
||||||
<!-- <item name="android:windowBackground">@android:color/transparent</item>-->
|
|
||||||
<!-- </style>-->
|
|
||||||
</resources>
|
</resources>
|
|
@ -1,6 +1,12 @@
|
||||||
package li.songe.gkd
|
package li.songe.gkd
|
||||||
|
|
||||||
import org.junit.Test
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import li.songe.gkd.debug.server.api.Window
|
||||||
|
import li.songe.gkd.util.Singleton
|
||||||
|
import li.songe.selector_core.Node
|
||||||
|
import li.songe.selector_core.Selector
|
||||||
|
import java.io.File
|
||||||
|
import li.songe.gkd.debug.server.api.Node as ApiNode
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Example local unit test, which will execute on the development machine (host).
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
|
@ -8,16 +14,82 @@ import org.junit.Test
|
||||||
* See [testing documentation](http://d.android.com/tools/testing).
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
*/
|
*/
|
||||||
class ExampleUnitTest {
|
class ExampleUnitTest {
|
||||||
@Test
|
// @Test
|
||||||
fun addition_isCorrect() {
|
fun check_selector() {
|
||||||
// assertEquals(4, 4L)
|
// println(Selector.parse("X View >n Text > Button[a=1][b=false][c=null][d!=`hello`] + A - X < Z"))
|
||||||
// println(MatchRule.parse("ImageView[text=hi][id=hi] >> WebView[text=hi] - TextView"))
|
// println(Selector.parse("A[a=1][a!=3][a*=3][a!*=3][a^=null]"))
|
||||||
// val testFile = File("D:/User/Documents/Project/gkd-subscription/subs.json")
|
// println(Selector.parse("@LinearLayout > TextView[id=`com.byted.pangle:id/tt_item_tv`][text=`不感兴趣`]"))
|
||||||
// val subsRaw = SubscriptionRaw.parse(testFile.readText())
|
|
||||||
// File("D:/User/Documents/Project/gkd-subscription/subs-2.json").writeText(
|
// val s1 = "ImageView < @FrameLayout < LinearLayout < RelativeLayout <n\n" +
|
||||||
// SubscriptionRaw.stringify(
|
// "LinearLayout < RelativeLayout + LinearLayout > RelativeLayout > TextView[text$=`广告`]"
|
||||||
// subsRaw
|
// val selector = Selector.parse(s1)
|
||||||
// )
|
//// Selector.parse("ImageView < @FrameLayout < LinearLayout < RelativeLayout <n LinearLayout < RelativeLayout + LinearLayout > RelativeLayout > TextView[text$=`广告`]")
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// val nodes =
|
||||||
|
// Singleton.json.decodeFromString<Window>(File("D:/User/Downloads/gkd/snapshot-1684381133305/window.json").readText()).nodes
|
||||||
|
// ?: emptyList()
|
||||||
|
//
|
||||||
|
// val simpleNodes = nodes.map { n ->
|
||||||
|
// SimpleNode(
|
||||||
|
// value = n
|
||||||
// )
|
// )
|
||||||
|
// }
|
||||||
|
// simpleNodes.forEach { simpleNode ->
|
||||||
|
// simpleNode.parent = simpleNodes.getOrNull(simpleNode.value.pid)?.apply {
|
||||||
|
// children.add(simpleNode)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// val rootWrapper = simpleNodes.map { SimpleNodeWrapper(it) }[0]
|
||||||
|
// println(rootWrapper.querySelector(selector))
|
||||||
|
}
|
||||||
|
|
||||||
|
class SimpleNode(
|
||||||
|
var parent: SimpleNode? = null,
|
||||||
|
val children: MutableList<SimpleNode> = mutableListOf(),
|
||||||
|
val value: ApiNode
|
||||||
|
) {
|
||||||
|
override fun toString(): String {
|
||||||
|
return value.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SimpleNodeWrapper(val value: SimpleNode) : Node {
|
||||||
|
|
||||||
|
override val parent: Node?
|
||||||
|
get() = value.parent?.let { SimpleNodeWrapper(it) }
|
||||||
|
override val children: Sequence<Node?>
|
||||||
|
get() = sequence {
|
||||||
|
value.children.forEach { yield(SimpleNodeWrapper(it)) }
|
||||||
|
}
|
||||||
|
override val name: CharSequence
|
||||||
|
get() = value.value.attr.className ?: ""
|
||||||
|
|
||||||
|
override fun attr(name: String): Any? {
|
||||||
|
val attr = value.value.attr
|
||||||
|
return when (name) {
|
||||||
|
"id" -> attr.id
|
||||||
|
"name" -> attr.className
|
||||||
|
"text" -> attr.text
|
||||||
|
"textLen" -> attr.text?.length
|
||||||
|
"desc" -> attr.desc
|
||||||
|
"descLen" -> attr.desc?.length
|
||||||
|
"isClickable" -> attr.isClickable
|
||||||
|
"isChecked" -> null
|
||||||
|
"index" -> {
|
||||||
|
val children = value.parent?.children ?: return null
|
||||||
|
children.forEachIndexed { index, simpleNode ->
|
||||||
|
if (simpleNode as SimpleNodeWrapper == this) {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
"_id" -> value.value.id
|
||||||
|
"_pid" -> value.value.pid
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -14,6 +14,7 @@ buildscript {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://youtrack.jetbrains.com/issue/KT-33191/
|
||||||
tasks.register<Delete>("clean").configure {
|
tasks.register<Delete>("clean").configure {
|
||||||
delete(rootProject.buildDir)
|
delete(rootProject.buildDir)
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,3 +22,4 @@ kotlin.code.style=official
|
||||||
#org.gradle.java.home=D\:/User/Documents/lisonge/.jdks/corretto-11.0.13
|
#org.gradle.java.home=D\:/User/Documents/lisonge/.jdks/corretto-11.0.13
|
||||||
#android.experimental.legacyTransform.forceNonIncremental=true
|
#android.experimental.legacyTransform.forceNonIncremental=true
|
||||||
android.debug.obsoleteApi=true
|
android.debug.obsoleteApi=true
|
||||||
|
kotlin.js.compiler=ir
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
package li.songe.selector.expression
|
|
||||||
|
|
||||||
sealed class ExpressionName
|
|
|
@ -5,7 +5,7 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "li.songe.selector"
|
namespace = "li.songe.selector_android"
|
||||||
compileSdk = libs.versions.android.compileSdk.get().toInt()
|
compileSdk = libs.versions.android.compileSdk.get().toInt()
|
||||||
buildToolsVersion = libs.versions.android.buildToolsVersion.get()
|
buildToolsVersion = libs.versions.android.buildToolsVersion.get()
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package li.songe.selector
|
package li.songe.selector_android
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="li.songe.selector">
|
package="li.songe.selector_android">
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
|
@ -1,12 +1,12 @@
|
||||||
package li.songe.selector
|
package li.songe.selector_android
|
||||||
|
|
||||||
import android.accessibilityservice.AccessibilityService
|
import android.accessibilityservice.AccessibilityService
|
||||||
import android.accessibilityservice.GestureDescription
|
import android.accessibilityservice.GestureDescription
|
||||||
import android.graphics.Path
|
import android.graphics.Path
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.view.accessibility.AccessibilityNodeInfo
|
import android.view.accessibility.AccessibilityNodeInfo
|
||||||
import li.songe.selector.parser.Transform
|
import li.songe.selector_android.parser.Transform
|
||||||
import li.songe.selector.wrapper.PropertySelectorWrapper
|
import li.songe.selector_android.wrapper.PropertySelectorWrapper
|
||||||
|
|
||||||
data class GkdSelector(val wrapper: PropertySelectorWrapper) {
|
data class GkdSelector(val wrapper: PropertySelectorWrapper) {
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ data class GkdSelector(val wrapper: PropertySelectorWrapper) {
|
||||||
return trackNodes.findLast { it != null } ?: child
|
return trackNodes.findLast { it != null } ?: child
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
nodeInfo.getChild(1)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package li.songe.selector
|
package li.songe.selector_android
|
||||||
|
|
||||||
import android.view.accessibility.AccessibilityNodeInfo
|
import android.view.accessibility.AccessibilityNodeInfo
|
||||||
import java.util.ArrayDeque
|
import java.util.ArrayDeque
|
||||||
|
@ -25,8 +25,8 @@ inline fun AccessibilityNodeInfo.forEachIndexed(action: (index: Int, childNode:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun AccessibilityNodeInfo.forEachAncestorIndexed(action: (depth: Int, ancestorNode: AccessibilityNodeInfo) -> Unit) {
|
inline fun AccessibilityNodeInfo?.forEachAncestorIndexed(action: (depth: Int, ancestorNode: AccessibilityNodeInfo) -> Unit) {
|
||||||
var p: AccessibilityNodeInfo? = this
|
var p = this
|
||||||
var depth = 0
|
var depth = 0
|
||||||
while (true) {
|
while (true) {
|
||||||
val p2 = p?.parent
|
val p2 = p?.parent
|
||||||
|
@ -129,3 +129,5 @@ fun AccessibilityNodeInfo.getBrother(dep: Int, elder: Boolean = true): Accessibi
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package li.songe.selector.expression
|
package li.songe.selector_android.expression
|
||||||
|
|
||||||
import android.view.accessibility.AccessibilityNodeInfo
|
import android.view.accessibility.AccessibilityNodeInfo
|
||||||
import li.songe.selector.operator.Operator
|
import li.songe.selector_android.operator.Operator
|
||||||
|
|
||||||
data class BinaryExpression(val name: String, val operator: Operator, val value: Any?) {
|
data class BinaryExpression(val name: String, val operator: Operator, val value: Any?) {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package li.songe.selector.operator
|
package li.songe.selector_android.operator
|
||||||
|
|
||||||
import android.view.accessibility.AccessibilityNodeInfo
|
import android.view.accessibility.AccessibilityNodeInfo
|
||||||
import li.songe.selector.expression.BinaryExpression
|
import li.songe.selector_android.expression.BinaryExpression
|
||||||
|
|
||||||
|
|
||||||
object End : Operator("$=") {
|
object End : Operator("$=") {
|
|
@ -1,9 +1,9 @@
|
||||||
package li.songe.selector.operator
|
package li.songe.selector_android.operator
|
||||||
|
|
||||||
import android.view.accessibility.AccessibilityNodeInfo
|
import android.view.accessibility.AccessibilityNodeInfo
|
||||||
import li.songe.selector.expression.BinaryExpression
|
import li.songe.selector_android.expression.BinaryExpression
|
||||||
import li.songe.selector.getDepth
|
import li.songe.selector_android.getDepth
|
||||||
import li.songe.selector.getIndex
|
import li.songe.selector_android.getIndex
|
||||||
|
|
||||||
object Equal : Operator("=") {
|
object Equal : Operator("=") {
|
||||||
override fun match(expression: BinaryExpression): (nodeInfo: AccessibilityNodeInfo) -> Boolean {
|
override fun match(expression: BinaryExpression): (nodeInfo: AccessibilityNodeInfo) -> Boolean {
|
|
@ -1,7 +1,7 @@
|
||||||
package li.songe.selector.operator
|
package li.songe.selector_android.operator
|
||||||
|
|
||||||
import android.view.accessibility.AccessibilityNodeInfo
|
import android.view.accessibility.AccessibilityNodeInfo
|
||||||
import li.songe.selector.expression.BinaryExpression
|
import li.songe.selector_android.expression.BinaryExpression
|
||||||
|
|
||||||
|
|
||||||
object Include : Operator("*=") {
|
object Include : Operator("*=") {
|
|
@ -1,9 +1,9 @@
|
||||||
package li.songe.selector.operator
|
package li.songe.selector_android.operator
|
||||||
|
|
||||||
import android.view.accessibility.AccessibilityNodeInfo
|
import android.view.accessibility.AccessibilityNodeInfo
|
||||||
import li.songe.selector.expression.BinaryExpression
|
import li.songe.selector_android.expression.BinaryExpression
|
||||||
import li.songe.selector.getDepth
|
import li.songe.selector_android.getDepth
|
||||||
import li.songe.selector.getIndex
|
import li.songe.selector_android.getIndex
|
||||||
|
|
||||||
object Less : Operator("<") {
|
object Less : Operator("<") {
|
||||||
override fun match(expression: BinaryExpression): (nodeInfo: AccessibilityNodeInfo) -> Boolean {
|
override fun match(expression: BinaryExpression): (nodeInfo: AccessibilityNodeInfo) -> Boolean {
|
|
@ -1,9 +1,9 @@
|
||||||
package li.songe.selector.operator
|
package li.songe.selector_android.operator
|
||||||
|
|
||||||
import android.view.accessibility.AccessibilityNodeInfo
|
import android.view.accessibility.AccessibilityNodeInfo
|
||||||
import li.songe.selector.expression.BinaryExpression
|
import li.songe.selector_android.expression.BinaryExpression
|
||||||
import li.songe.selector.getDepth
|
import li.songe.selector_android.getDepth
|
||||||
import li.songe.selector.getIndex
|
import li.songe.selector_android.getIndex
|
||||||
|
|
||||||
|
|
||||||
object LessEqual : Operator("<=") {
|
object LessEqual : Operator("<=") {
|
|
@ -1,9 +1,9 @@
|
||||||
package li.songe.selector.operator
|
package li.songe.selector_android.operator
|
||||||
|
|
||||||
import android.view.accessibility.AccessibilityNodeInfo
|
import android.view.accessibility.AccessibilityNodeInfo
|
||||||
import li.songe.selector.expression.BinaryExpression
|
import li.songe.selector_android.expression.BinaryExpression
|
||||||
import li.songe.selector.getDepth
|
import li.songe.selector_android.getDepth
|
||||||
import li.songe.selector.getIndex
|
import li.songe.selector_android.getIndex
|
||||||
|
|
||||||
object More : Operator(">") {
|
object More : Operator(">") {
|
||||||
override fun match(expression: BinaryExpression): (nodeInfo: AccessibilityNodeInfo) -> Boolean {
|
override fun match(expression: BinaryExpression): (nodeInfo: AccessibilityNodeInfo) -> Boolean {
|
|
@ -1,9 +1,9 @@
|
||||||
package li.songe.selector.operator
|
package li.songe.selector_android.operator
|
||||||
|
|
||||||
import android.view.accessibility.AccessibilityNodeInfo
|
import android.view.accessibility.AccessibilityNodeInfo
|
||||||
import li.songe.selector.expression.BinaryExpression
|
import li.songe.selector_android.expression.BinaryExpression
|
||||||
import li.songe.selector.getDepth
|
import li.songe.selector_android.getDepth
|
||||||
import li.songe.selector.getIndex
|
import li.songe.selector_android.getIndex
|
||||||
|
|
||||||
|
|
||||||
object MoreEqual : Operator(">=") {
|
object MoreEqual : Operator(">=") {
|
|
@ -1,9 +1,9 @@
|
||||||
package li.songe.selector.operator
|
package li.songe.selector_android.operator
|
||||||
|
|
||||||
import android.view.accessibility.AccessibilityNodeInfo
|
import android.view.accessibility.AccessibilityNodeInfo
|
||||||
import li.songe.selector.expression.BinaryExpression
|
import li.songe.selector_android.expression.BinaryExpression
|
||||||
import li.songe.selector.getDepth
|
import li.songe.selector_android.getDepth
|
||||||
import li.songe.selector.getIndex
|
import li.songe.selector_android.getIndex
|
||||||
|
|
||||||
|
|
||||||
object NotEqual : Operator("!=") {
|
object NotEqual : Operator("!=") {
|
|
@ -1,7 +1,7 @@
|
||||||
package li.songe.selector.operator
|
package li.songe.selector_android.operator
|
||||||
|
|
||||||
import android.view.accessibility.AccessibilityNodeInfo
|
import android.view.accessibility.AccessibilityNodeInfo
|
||||||
import li.songe.selector.expression.BinaryExpression
|
import li.songe.selector_android.expression.BinaryExpression
|
||||||
|
|
||||||
sealed class Operator(private val key: String) {
|
sealed class Operator(private val key: String) {
|
||||||
override fun toString() = key
|
override fun toString() = key
|
|
@ -1,7 +1,7 @@
|
||||||
package li.songe.selector.operator
|
package li.songe.selector_android.operator
|
||||||
|
|
||||||
import android.view.accessibility.AccessibilityNodeInfo
|
import android.view.accessibility.AccessibilityNodeInfo
|
||||||
import li.songe.selector.expression.BinaryExpression
|
import li.songe.selector_android.expression.BinaryExpression
|
||||||
|
|
||||||
|
|
||||||
object Start : Operator("^=") {
|
object Start : Operator("^=") {
|
|
@ -1,4 +1,4 @@
|
||||||
package li.songe.selector.parser
|
package li.songe.selector_android.parser
|
||||||
|
|
||||||
open class GkdParser<T>(
|
open class GkdParser<T>(
|
||||||
val prefix: String = "",
|
val prefix: String = "",
|
|
@ -1,3 +1,3 @@
|
||||||
package li.songe.selector.parser
|
package li.songe.selector_android.parser
|
||||||
|
|
||||||
data class GkdParserResult<T>(val data: T, val length: Int = 0)
|
data class GkdParserResult<T>(val data: T, val length: Int = 0)
|
|
@ -1,4 +1,4 @@
|
||||||
package li.songe.selector.parser
|
package li.songe.selector_android.parser
|
||||||
|
|
||||||
data class GkdSyntaxError(val expectedValue: String, val position: Int, val source: String) :
|
data class GkdSyntaxError(val expectedValue: String, val position: Int, val source: String) :
|
||||||
Exception(
|
Exception(
|
|
@ -1,20 +1,20 @@
|
||||||
package li.songe.selector.parser
|
package li.songe.selector_android.parser
|
||||||
|
|
||||||
import li.songe.selector.GkdSelector
|
import li.songe.selector_android.GkdSelector
|
||||||
import li.songe.selector.expression.BinaryExpression
|
import li.songe.selector_android.expression.BinaryExpression
|
||||||
import li.songe.selector.operator.End
|
import li.songe.selector_android.operator.End
|
||||||
import li.songe.selector.operator.Equal
|
import li.songe.selector_android.operator.Equal
|
||||||
import li.songe.selector.operator.Include
|
import li.songe.selector_android.operator.Include
|
||||||
import li.songe.selector.operator.Less
|
import li.songe.selector_android.operator.Less
|
||||||
import li.songe.selector.operator.LessEqual
|
import li.songe.selector_android.operator.LessEqual
|
||||||
import li.songe.selector.operator.More
|
import li.songe.selector_android.operator.More
|
||||||
import li.songe.selector.operator.MoreEqual
|
import li.songe.selector_android.operator.MoreEqual
|
||||||
import li.songe.selector.operator.NotEqual
|
import li.songe.selector_android.operator.NotEqual
|
||||||
import li.songe.selector.operator.Start
|
import li.songe.selector_android.operator.Start
|
||||||
import li.songe.selector.selector.CombinatorSelector
|
import li.songe.selector_android.selector.CombinatorSelector
|
||||||
import li.songe.selector.selector.PropertySelector
|
import li.songe.selector_android.selector.PropertySelector
|
||||||
import li.songe.selector.wrapper.CombinatorSelectorWrapper
|
import li.songe.selector_android.wrapper.CombinatorSelectorWrapper
|
||||||
import li.songe.selector.wrapper.PropertySelectorWrapper
|
import li.songe.selector_android.wrapper.PropertySelectorWrapper
|
||||||
|
|
||||||
internal object Transform {
|
internal object Transform {
|
||||||
val whiteCharParser = GkdParser("\u0020\t\r\n") { source, offset, prefix ->
|
val whiteCharParser = GkdParser("\u0020\t\r\n") { source, offset, prefix ->
|
|
@ -1,4 +1,4 @@
|
||||||
package li.songe.selector.selector
|
package li.songe.selector_android.selector
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 关系连接选择器
|
* 关系连接选择器
|
|
@ -1,6 +1,6 @@
|
||||||
package li.songe.selector.selector
|
package li.songe.selector_android.selector
|
||||||
|
|
||||||
import li.songe.selector.expression.BinaryExpression
|
import li.songe.selector_android.expression.BinaryExpression
|
||||||
|
|
||||||
|
|
||||||
data class PropertySelector(
|
data class PropertySelector(
|
|
@ -1,15 +1,15 @@
|
||||||
package li.songe.selector.wrapper
|
package li.songe.selector_android.wrapper
|
||||||
|
|
||||||
import android.view.accessibility.AccessibilityNodeInfo
|
import android.view.accessibility.AccessibilityNodeInfo
|
||||||
import li.songe.selector.forEachAncestorIndexed
|
import li.songe.selector_android.forEachAncestorIndexed
|
||||||
import li.songe.selector.forEachElderBrotherIndexed
|
import li.songe.selector_android.forEachElderBrotherIndexed
|
||||||
import li.songe.selector.forEachIndexed
|
import li.songe.selector_android.forEachIndexed
|
||||||
import li.songe.selector.forEachYoungerBrotherIndexed
|
import li.songe.selector_android.forEachYoungerBrotherIndexed
|
||||||
import li.songe.selector.getAncestor
|
import li.songe.selector_android.getAncestor
|
||||||
import li.songe.selector.getBrother
|
import li.songe.selector_android.getBrother
|
||||||
import li.songe.selector.getDepth
|
import li.songe.selector_android.getDepth
|
||||||
import li.songe.selector.getIndex
|
import li.songe.selector_android.getIndex
|
||||||
import li.songe.selector.selector.CombinatorSelector
|
import li.songe.selector_android.selector.CombinatorSelector
|
||||||
|
|
||||||
data class CombinatorSelectorWrapper(
|
data class CombinatorSelectorWrapper(
|
||||||
private val combinatorSelector: CombinatorSelector,
|
private val combinatorSelector: CombinatorSelector,
|
|
@ -1,7 +1,7 @@
|
||||||
package li.songe.selector.wrapper
|
package li.songe.selector_android.wrapper
|
||||||
|
|
||||||
import android.view.accessibility.AccessibilityNodeInfo
|
import android.view.accessibility.AccessibilityNodeInfo
|
||||||
import li.songe.selector.selector.PropertySelector
|
import li.songe.selector_android.selector.PropertySelector
|
||||||
|
|
||||||
data class PropertySelectorWrapper(
|
data class PropertySelectorWrapper(
|
||||||
private val propertySelector: PropertySelector,
|
private val propertySelector: PropertySelector,
|
|
@ -1,6 +1,6 @@
|
||||||
package li.songe.selector
|
package li.songe.selector_android
|
||||||
|
|
||||||
import li.songe.selector.parser.Transform.gkdSelectorParser
|
import li.songe.selector_android.parser.Transform.gkdSelectorParser
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
1
selector_core/.gitignore
vendored
Normal file
1
selector_core/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/build
|
12
selector_core/build.gradle.kts
Normal file
12
selector_core/build.gradle.kts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
plugins {
|
||||||
|
id("java-library")
|
||||||
|
id("org.jetbrains.kotlin.jvm")
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
}
|
73
selector_core/src/main/java/li/songe/selector_core/Node.kt
Normal file
73
selector_core/src/main/java/li/songe/selector_core/Node.kt
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
package li.songe.selector_core
|
||||||
|
|
||||||
|
interface Node {
|
||||||
|
val parent: Node?
|
||||||
|
val children: Sequence<Node?>
|
||||||
|
|
||||||
|
val name: CharSequence
|
||||||
|
|
||||||
|
/**
|
||||||
|
* constant traversal
|
||||||
|
*/
|
||||||
|
fun getChild(offset: Int) = children.elementAtOrNull(offset)
|
||||||
|
|
||||||
|
fun attr(name: String): Any?
|
||||||
|
|
||||||
|
val ancestors: Sequence<Node>
|
||||||
|
get() = sequence {
|
||||||
|
var parentVar: Node? = parent ?: return@sequence
|
||||||
|
while (parentVar != null) {
|
||||||
|
yield(parentVar)
|
||||||
|
parentVar = parentVar.parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAncestor(offset: Int) = ancestors.elementAtOrNull(offset)
|
||||||
|
|
||||||
|
// if index=3, traverse 2,1,0
|
||||||
|
val beforeBrothers: Sequence<Node?>
|
||||||
|
get() = sequence {
|
||||||
|
val parentVal = parent ?: return@sequence
|
||||||
|
val list = parentVal.children.takeWhile { it != this@Node }.toMutableList()
|
||||||
|
list.reverse()
|
||||||
|
yieldAll(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBeforeBrother(offset: Int) = beforeBrothers.elementAtOrNull(offset)
|
||||||
|
|
||||||
|
// if index=3, traverse 4,5,6...
|
||||||
|
val afterBrothers: Sequence<Node?>
|
||||||
|
get() = sequence {
|
||||||
|
val parentVal = parent ?: return@sequence
|
||||||
|
yieldAll(parentVal.children.dropWhile { it == this@Node })
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAfterBrother(offset: Int) = afterBrothers.elementAtOrNull(offset)
|
||||||
|
|
||||||
|
val descendants: Sequence<Node>
|
||||||
|
get() = sequence {
|
||||||
|
val stack = mutableListOf<Node>()
|
||||||
|
stack.add(this@Node)
|
||||||
|
do {
|
||||||
|
val top = stack.removeLast()
|
||||||
|
yield(top)
|
||||||
|
for (childNode in top.children) {
|
||||||
|
if (childNode != null) {
|
||||||
|
stack.add(childNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (stack.isNotEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun querySelector(selector: Selector) = querySelectorAll(selector).firstOrNull()
|
||||||
|
|
||||||
|
fun querySelectorAll(selector: Selector) = sequence {
|
||||||
|
descendants.forEach { node ->
|
||||||
|
val r = selector.match(node)
|
||||||
|
if (r != null)
|
||||||
|
yield(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
package li.songe.selector_core
|
||||||
|
|
||||||
|
import li.songe.selector_core.data.PropertyWrapper
|
||||||
|
import li.songe.selector_core.parser.ParserSet
|
||||||
|
|
||||||
|
data class Selector(private val propertyWrapper: PropertyWrapper) {
|
||||||
|
override fun toString() = propertyWrapper.toString()
|
||||||
|
// val segments by lazy {
|
||||||
|
// sequence {
|
||||||
|
// var c = propertyWrapper.to
|
||||||
|
// yield(propertyWrapper.propertySegment)
|
||||||
|
// while (c != null) {
|
||||||
|
// yield(c!!.connectSegment)
|
||||||
|
// yield(c!!.to.propertySegment)
|
||||||
|
// c = c!!.to.to
|
||||||
|
// }
|
||||||
|
// }.toList().reversed()
|
||||||
|
// }
|
||||||
|
|
||||||
|
fun match(node: Node): Node? {
|
||||||
|
val text= node.attr("text") as CharSequence?
|
||||||
|
|
||||||
|
val trackNodes = propertyWrapper.match(node) ?: return null
|
||||||
|
return trackNodes.lastOrNull() ?: node
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun parse(source: String) = ParserSet.selectorParser(source)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package li.songe.selector_core.data
|
||||||
|
|
||||||
|
import li.songe.selector_core.Node
|
||||||
|
|
||||||
|
data class BinaryExpression(val name: String, val operator: CompareOperator, val value: Any?) {
|
||||||
|
fun match(node: Node) = operator.compare(node.attr(name), value)
|
||||||
|
override fun toString() = "[${name}${operator}${
|
||||||
|
if (value is String) {
|
||||||
|
"`${value.replace("`", "\\`")}`"
|
||||||
|
} else {
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}]"
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
package li.songe.selector_core.data
|
||||||
|
|
||||||
|
sealed class CompareOperator(val key: String) {
|
||||||
|
override fun toString() = key
|
||||||
|
abstract fun compare(a: Any?, b: Any?): Boolean
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val allSubClasses = listOf(
|
||||||
|
Equal,
|
||||||
|
NotEqual,
|
||||||
|
Start,
|
||||||
|
NotStart,
|
||||||
|
Include,
|
||||||
|
NotInclude,
|
||||||
|
End,
|
||||||
|
NotEnd,
|
||||||
|
Less,
|
||||||
|
LessEqual,
|
||||||
|
More,
|
||||||
|
MoreEqual
|
||||||
|
).sortedBy { -it.key.length }
|
||||||
|
}
|
||||||
|
|
||||||
|
object Equal : CompareOperator("=") {
|
||||||
|
override fun compare(a: Any?, b: Any?): Boolean {
|
||||||
|
return if (a is CharSequence && b is CharSequence) a.contentEquals(b) else a == b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object NotEqual : CompareOperator("!=") {
|
||||||
|
override fun compare(a: Any?, b: Any?) = !Equal.compare(a, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
object Start : CompareOperator("^=") {
|
||||||
|
override fun compare(a: Any?, b: Any?): Boolean {
|
||||||
|
return if (a is CharSequence && b is CharSequence) a.startsWith(b) else false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object NotStart : CompareOperator("!^=") {
|
||||||
|
override fun compare(a: Any?, b: Any?) = !Start.compare(a, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
object Include : CompareOperator("*=") {
|
||||||
|
override fun compare(a: Any?, b: Any?): Boolean {
|
||||||
|
return if (a is CharSequence && b is CharSequence) a.contains(b) else false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object NotInclude : CompareOperator("!*=") {
|
||||||
|
override fun compare(a: Any?, b: Any?) = !Include.compare(a, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
object End : CompareOperator("$=") {
|
||||||
|
override fun compare(a: Any?, b: Any?): Boolean {
|
||||||
|
return if (a is CharSequence && b is CharSequence) a.endsWith(b) else false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object NotEnd : CompareOperator("!$=") {
|
||||||
|
override fun compare(a: Any?, b: Any?) = !End.compare(a, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
object Less : CompareOperator("<") {
|
||||||
|
override fun compare(a: Any?, b: Any?): Boolean {
|
||||||
|
return if (a is Int && b is Int) a < b else false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object LessEqual : CompareOperator("<=") {
|
||||||
|
override fun compare(a: Any?, b: Any?): Boolean {
|
||||||
|
return if (a is Int && b is Int) a <= b else false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object More : CompareOperator(">") {
|
||||||
|
override fun compare(a: Any?, b: Any?): Boolean {
|
||||||
|
return if (a is Int && b is Int) a > b else false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object MoreEqual : CompareOperator(">=") {
|
||||||
|
override fun compare(a: Any?, b: Any?): Boolean {
|
||||||
|
return if (a is Int && b is Int) a >= b else false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
package li.songe.selector_core.data
|
||||||
|
|
||||||
|
import li.songe.selector_core.Node
|
||||||
|
|
||||||
|
sealed class ConnectOperator(val key: String) {
|
||||||
|
override fun toString() = key
|
||||||
|
abstract fun traversal(node: Node): Sequence<Node?>
|
||||||
|
abstract fun traversal(node: Node, offset: Int): Node?
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val allSubClasses = listOf(
|
||||||
|
BeforeBrother,
|
||||||
|
AfterBrother,
|
||||||
|
Ancestor,
|
||||||
|
Child
|
||||||
|
).sortedBy { -it.key.length }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A + B, 1,2,3,A,B,7,8
|
||||||
|
*/
|
||||||
|
object BeforeBrother : ConnectOperator("+") {
|
||||||
|
override fun traversal(node: Node) = node.beforeBrothers
|
||||||
|
override fun traversal(node: Node, offset: Int): Node? = node.getBeforeBrother(offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A - B, 1,2,3,B,A,7,8
|
||||||
|
*/
|
||||||
|
object AfterBrother : ConnectOperator("-") {
|
||||||
|
override fun traversal(node: Node) = node.afterBrothers
|
||||||
|
override fun traversal(node: Node, offset: Int): Node? = node.getAfterBrother(offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A > B, A is the ancestor of B
|
||||||
|
*/
|
||||||
|
object Ancestor : ConnectOperator(">") {
|
||||||
|
override fun traversal(node: Node) = node.ancestors
|
||||||
|
override fun traversal(node: Node, offset: Int): Node? = node.getAncestor(offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A < B, A is the child of B
|
||||||
|
*/
|
||||||
|
object Child : ConnectOperator("<") {
|
||||||
|
override fun traversal(node: Node) = node.children
|
||||||
|
override fun traversal(node: Node, offset: Int): Node? = node.getChild(offset)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package li.songe.selector_core.data
|
||||||
|
|
||||||
|
import li.songe.selector_core.Node
|
||||||
|
|
||||||
|
data class ConnectSegment(
|
||||||
|
val operator: ConnectOperator = ConnectOperator.Ancestor,
|
||||||
|
val polynomialExpression: PolynomialExpression = PolynomialExpression()
|
||||||
|
) {
|
||||||
|
override fun toString(): String {
|
||||||
|
if (operator == ConnectOperator.Ancestor && polynomialExpression.a == 1 && polynomialExpression.b == 0) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return operator.toString() + polynomialExpression.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun traversal(node: Node): Sequence<Node?> {
|
||||||
|
if (polynomialExpression.isConstant) {
|
||||||
|
return sequence {
|
||||||
|
val node1 = operator.traversal(node, polynomialExpression.b1)
|
||||||
|
if (node1 != null) {
|
||||||
|
yield(node1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return polynomialExpression.traversal(operator.traversal(node))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package li.songe.selector_core.data
|
||||||
|
|
||||||
|
import li.songe.selector_core.Node
|
||||||
|
|
||||||
|
data class ConnectWrapper(
|
||||||
|
val connectSegment: ConnectSegment,
|
||||||
|
val to: PropertyWrapper,
|
||||||
|
) {
|
||||||
|
override fun toString(): String {
|
||||||
|
return (to.toString() + "\u0020" + connectSegment.toString()).trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun match(
|
||||||
|
node: Node,
|
||||||
|
trackNodes: MutableList<Node> = mutableListOf(),
|
||||||
|
): List<Node>? {
|
||||||
|
connectSegment.traversal(node).forEach {
|
||||||
|
if (it == null) return@forEach
|
||||||
|
val r = to.match(it, trackNodes)
|
||||||
|
if (r != null) return r
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package li.songe.selector_core.data
|
||||||
|
|
||||||
|
import li.songe.selector_core.Node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* an+b
|
||||||
|
*/
|
||||||
|
data class PolynomialExpression(val a: Int = 0, val b: Int = 1) {
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
if (a == 0 && b == 0) return "0"
|
||||||
|
if (b == 0) {
|
||||||
|
if (a == 1) return "n"
|
||||||
|
return if (a > 0) {
|
||||||
|
"${a}n"
|
||||||
|
} else {
|
||||||
|
"(${a}n)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (a == 0) {
|
||||||
|
if (b == 1) return ""
|
||||||
|
return if (b > 0) {
|
||||||
|
b.toString()
|
||||||
|
} else {
|
||||||
|
"(${b})"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val bOp = if (b >= 0) "+" else ""
|
||||||
|
return "(${a}n${bOp}${b})"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [nth-child](https://developer.mozilla.org/zh-CN/docs/Web/CSS/:nth-child)
|
||||||
|
*/
|
||||||
|
val b1 = b - 1
|
||||||
|
|
||||||
|
val traversal: (Sequence<Node?>) -> Sequence<Node?> =
|
||||||
|
if (a <= 0 && b <= 0) ({ emptySequence() })
|
||||||
|
else ({ sequence ->
|
||||||
|
sequence.filterIndexed { x, _ -> (x - b1) % a == 0 && (x - b1) / a > 0 }
|
||||||
|
})
|
||||||
|
|
||||||
|
val isConstant = a == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3n+1, 1,4,7
|
||||||
|
// -n+9, 9,8,7,...,1
|
||||||
|
// an+b=x, n=(x-b)/a
|
|
@ -0,0 +1,35 @@
|
||||||
|
package li.songe.selector_core.data
|
||||||
|
|
||||||
|
import li.songe.selector_core.Node
|
||||||
|
|
||||||
|
data class PropertySegment(
|
||||||
|
/**
|
||||||
|
* 此属性选择器是否被 @ 标记
|
||||||
|
*/
|
||||||
|
val match: Boolean,
|
||||||
|
val name: String,
|
||||||
|
val expressions: List<BinaryExpression>,
|
||||||
|
) {
|
||||||
|
override fun toString(): String {
|
||||||
|
val matchTag = if (match) "@" else ""
|
||||||
|
return matchTag + name + expressions.joinToString("")
|
||||||
|
}
|
||||||
|
|
||||||
|
val matchName: (node: Node) -> Boolean =
|
||||||
|
if (name.isBlank() || name == "*")
|
||||||
|
({ true })
|
||||||
|
else ({ node ->
|
||||||
|
val str = node.name
|
||||||
|
str.contentEquals(name) ||
|
||||||
|
(str.endsWith(name) && str[str.length - name.length - 1] == '.')
|
||||||
|
})
|
||||||
|
|
||||||
|
val matchExpressions: (node: Node) -> Boolean = { node ->
|
||||||
|
expressions.all { ex -> ex.match(node) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun match(node: Node): Boolean {
|
||||||
|
return matchName(node) && matchExpressions(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package li.songe.selector_core.data
|
||||||
|
|
||||||
|
import li.songe.selector_core.Node
|
||||||
|
|
||||||
|
data class PropertyWrapper(
|
||||||
|
val propertySegment: PropertySegment,
|
||||||
|
val to: ConnectWrapper? = null,
|
||||||
|
) {
|
||||||
|
override fun toString(): String {
|
||||||
|
return (if (to != null) {
|
||||||
|
to.toString() + "\u0020"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}) + propertySegment.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun match(
|
||||||
|
node: Node,
|
||||||
|
trackNodes: MutableList<Node> = mutableListOf(),
|
||||||
|
): List<Node>? {
|
||||||
|
if (!propertySegment.match(node)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (propertySegment.match || trackNodes.isEmpty()) {
|
||||||
|
trackNodes.add(node)
|
||||||
|
}
|
||||||
|
if (to == null) {
|
||||||
|
return trackNodes
|
||||||
|
}
|
||||||
|
return to.match(node, trackNodes)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package li.songe.selector_core.parser
|
||||||
|
|
||||||
|
internal open class Parser<T>(
|
||||||
|
val prefix: String = "",
|
||||||
|
private val temp: (source: String, offset: Int, prefix: String) -> ParserResult<T>
|
||||||
|
) : (String, Int) -> ParserResult<T> {
|
||||||
|
override fun invoke(source: String, offset: Int) = temp(source, offset, prefix)
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
package li.songe.selector_core.parser
|
||||||
|
|
||||||
|
internal data class ParserResult<T>(val data: T, val length: Int = 0)
|
|
@ -0,0 +1,387 @@
|
||||||
|
package li.songe.selector_core.parser
|
||||||
|
|
||||||
|
import li.songe.selector_core.Selector
|
||||||
|
import li.songe.selector_core.data.BinaryExpression
|
||||||
|
import li.songe.selector_core.data.CompareOperator
|
||||||
|
import li.songe.selector_core.data.ConnectOperator
|
||||||
|
import li.songe.selector_core.data.ConnectSegment
|
||||||
|
import li.songe.selector_core.data.ConnectWrapper
|
||||||
|
import li.songe.selector_core.data.PolynomialExpression
|
||||||
|
import li.songe.selector_core.data.PropertySegment
|
||||||
|
import li.songe.selector_core.data.PropertyWrapper
|
||||||
|
|
||||||
|
internal object ParserSet {
|
||||||
|
val whiteCharParser = Parser("\u0020\t\r\n") { source, offset, prefix ->
|
||||||
|
var i = offset
|
||||||
|
var data = ""
|
||||||
|
while (i < source.length && prefix.contains(source[i])) {
|
||||||
|
data += source[i]
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
ParserResult(data, i - offset)
|
||||||
|
}
|
||||||
|
val whiteCharStrictParser = Parser("\u0020\t\r\n") { source, offset, prefix ->
|
||||||
|
SyntaxError.assert(source, offset, prefix, "whitespace")
|
||||||
|
whiteCharParser(source, offset)
|
||||||
|
}
|
||||||
|
val nameParser =
|
||||||
|
Parser("*1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM_") { source, offset, prefix ->
|
||||||
|
var i = offset
|
||||||
|
val s0 = source.getOrNull(i)
|
||||||
|
if (s0 != null && !prefix.contains(s0)) {
|
||||||
|
return@Parser ParserResult("")
|
||||||
|
}
|
||||||
|
SyntaxError.assert(source, i, prefix, "*0-9a-zA-Z_")
|
||||||
|
var data = source[i].toString()
|
||||||
|
i++
|
||||||
|
if (data == "*") { // 范匹配
|
||||||
|
return@Parser ParserResult(data, i - offset)
|
||||||
|
}
|
||||||
|
val center = "1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM_."
|
||||||
|
while (i < source.length) {
|
||||||
|
// . 不能在开头和结尾
|
||||||
|
if (data[i - offset - 1] == '.') {
|
||||||
|
SyntaxError.assert(source, i, prefix, "[0-9a-zA-Z_]")
|
||||||
|
}
|
||||||
|
if (center.contains(source[i])) {
|
||||||
|
data += source[i]
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
ParserResult(data, i - offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
val combinatorOperatorParser =
|
||||||
|
Parser(ConnectOperator.allSubClasses.joinToString("") { it.key }) { source, offset, _ ->
|
||||||
|
val operator = ConnectOperator.allSubClasses.find { subOperator ->
|
||||||
|
source.startsWith(
|
||||||
|
subOperator.key,
|
||||||
|
offset
|
||||||
|
)
|
||||||
|
} ?: SyntaxError.throwError(source, offset, "ConnectOperator")
|
||||||
|
return@Parser ParserResult(operator, operator.key.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
val integerParser = Parser("1234567890") { source, offset, prefix ->
|
||||||
|
var i = offset
|
||||||
|
SyntaxError.assert(source, i, prefix, "number")
|
||||||
|
var s = ""
|
||||||
|
while (prefix.contains(source[i])) {
|
||||||
|
s += source[i]
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
ParserResult(s.toInt(), i - offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// [+-][a][n[^b]]
|
||||||
|
val monomialParser = Parser("+-1234567890n") { source, offset, prefix ->
|
||||||
|
var i = offset
|
||||||
|
SyntaxError.assert(source, i, prefix)
|
||||||
|
/**
|
||||||
|
* one of 1, -1
|
||||||
|
*/
|
||||||
|
val signal = when (source[i]) {
|
||||||
|
'+' -> {
|
||||||
|
i++
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
'-' -> {
|
||||||
|
i++
|
||||||
|
-1
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> 1
|
||||||
|
}
|
||||||
|
i += whiteCharParser(source, i).length
|
||||||
|
// [a][n[^b]]
|
||||||
|
SyntaxError.assert(source, i, integerParser.prefix + "n")
|
||||||
|
val coefficient =
|
||||||
|
if (integerParser.prefix.contains(source[i])) {
|
||||||
|
val coefficientResult = integerParser(source, i)
|
||||||
|
i += coefficientResult.length
|
||||||
|
coefficientResult.data
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
} * signal
|
||||||
|
// [n[^b]]
|
||||||
|
if (i < source.length && source[i] == 'n') {
|
||||||
|
i++
|
||||||
|
if (i < source.length && source[i] == '^') {
|
||||||
|
i++
|
||||||
|
val powerResult = integerParser(source, i)
|
||||||
|
i += powerResult.length
|
||||||
|
return@Parser ParserResult(Pair(powerResult.data, coefficient), i - offset)
|
||||||
|
} else {
|
||||||
|
return@Parser ParserResult(Pair(1, coefficient), i - offset)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return@Parser ParserResult(Pair(0, coefficient), i - offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ([+-][a][n[^b]] [+-][a][n[^b]])
|
||||||
|
val expressionParser = Parser("(0123456789n") { source, offset, prefix ->
|
||||||
|
var i = offset
|
||||||
|
SyntaxError.assert(source, i, prefix)
|
||||||
|
val monomialResultList = mutableListOf<ParserResult<Pair<Int, Int>>>()
|
||||||
|
when (source[i]) {
|
||||||
|
'(' -> {
|
||||||
|
i++
|
||||||
|
i += whiteCharParser(source, i).length
|
||||||
|
SyntaxError.assert(source, i, monomialParser.prefix)
|
||||||
|
while (source[i] != ')') {
|
||||||
|
if (monomialResultList.size > 0) {
|
||||||
|
SyntaxError.assert(source, i, "+-")
|
||||||
|
}
|
||||||
|
val monomialResult = monomialParser(source, i)
|
||||||
|
monomialResultList.add(monomialResult)
|
||||||
|
i += monomialResult.length
|
||||||
|
i += whiteCharParser(source, i).length
|
||||||
|
if (i >= source.length) {
|
||||||
|
SyntaxError.assert(source, i, ")")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
val monomialResult = monomialParser(source, i)
|
||||||
|
monomialResultList.add(monomialResult)
|
||||||
|
i += monomialResult.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val map = mutableMapOf<Int, Int>()
|
||||||
|
monomialResultList.forEach { monomialResult ->
|
||||||
|
val (power, coefficient) = monomialResult.data
|
||||||
|
map[power] = (map[power] ?: 0) + coefficient
|
||||||
|
}
|
||||||
|
map.mapKeys { power ->
|
||||||
|
if (power.key > 1) {
|
||||||
|
SyntaxError.throwError(source, offset, "power must be 0 or 1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ParserResult(PolynomialExpression(map[1] ?: 0, map[0] ?: 0), i - offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// [+-><](a*n^b)
|
||||||
|
val combinatorParser = Parser(combinatorOperatorParser.prefix) { source, offset, _ ->
|
||||||
|
var i = offset
|
||||||
|
val operatorResult = combinatorOperatorParser(source, i)
|
||||||
|
i += operatorResult.length
|
||||||
|
var expressionResult: ParserResult<PolynomialExpression>? = null
|
||||||
|
if (i < source.length && expressionParser.prefix.contains(source[i])) {
|
||||||
|
expressionResult = expressionParser(source, i)
|
||||||
|
i += expressionResult.length
|
||||||
|
}
|
||||||
|
ParserResult(
|
||||||
|
ConnectSegment(
|
||||||
|
operatorResult.data,
|
||||||
|
expressionResult?.data ?: PolynomialExpression()
|
||||||
|
), i - offset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val attrOperatorParser =
|
||||||
|
Parser(CompareOperator.allSubClasses.joinToString("") { it.key }) { source, offset, _ ->
|
||||||
|
val operator = CompareOperator.allSubClasses.find { SubOperator ->
|
||||||
|
source.startsWith(SubOperator.key, offset)
|
||||||
|
} ?: SyntaxError.throwError(source, offset, "CompareOperator")
|
||||||
|
ParserResult(operator, operator.key.length)
|
||||||
|
}
|
||||||
|
val stringParser = Parser("`") { source, offset, prefix ->
|
||||||
|
var i = offset
|
||||||
|
SyntaxError.assert(source, i, prefix)
|
||||||
|
i++
|
||||||
|
var data = ""
|
||||||
|
while (source[i] != '`') {
|
||||||
|
if (i == source.length - 1) {
|
||||||
|
SyntaxError.assert(source, i, "`")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (source[i] == '\\') {
|
||||||
|
i++
|
||||||
|
SyntaxError.assert(source, i)
|
||||||
|
if (source[i] == '`') {
|
||||||
|
data += source[i]
|
||||||
|
SyntaxError.assert(source, i + 1)
|
||||||
|
} else {
|
||||||
|
data += '\\' + source[i].toString()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data += source[i]
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
ParserResult(data, i - offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
val propertyParser =
|
||||||
|
Parser((('0'..'9') + ('a'..'z') + ('A'..'Z')).joinToString("") + "_") { source, offset, prefix ->
|
||||||
|
var i = offset
|
||||||
|
SyntaxError.assert(source, i, prefix)
|
||||||
|
var data = source[i].toString()
|
||||||
|
i++
|
||||||
|
while (i < source.length) {
|
||||||
|
if (!prefix.contains(source[i])) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
data += source[i]
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
ParserResult(data, i - offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
val valueParser = Parser("tfn`1234567890") { source, offset, prefix ->
|
||||||
|
var i = offset
|
||||||
|
SyntaxError.assert(source, i, prefix)
|
||||||
|
val value: Any? = when (source[i]) {
|
||||||
|
't' -> {
|
||||||
|
i++
|
||||||
|
"rue".forEach { c ->
|
||||||
|
SyntaxError.assert(source, i, c.toString())
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
'f' -> {
|
||||||
|
i++
|
||||||
|
"alse".forEach { c ->
|
||||||
|
SyntaxError.assert(source, i, c.toString())
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
'n' -> {
|
||||||
|
i++
|
||||||
|
"ull".forEach { c ->
|
||||||
|
SyntaxError.assert(source, i, c.toString())
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
'`' -> {
|
||||||
|
val s = stringParser(source, i)
|
||||||
|
i += s.length
|
||||||
|
s.data
|
||||||
|
}
|
||||||
|
|
||||||
|
in "1234567890" -> {
|
||||||
|
val n = integerParser(source, i)
|
||||||
|
i += n.length
|
||||||
|
n.data
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
SyntaxError.throwError(source, i, prefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ParserResult(value, i - offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
val attrParser = Parser("[") { source, offset, prefix ->
|
||||||
|
var i = offset
|
||||||
|
SyntaxError.assert(source, i, prefix)
|
||||||
|
i++
|
||||||
|
val parserResult = propertyParser(source, i)
|
||||||
|
i += parserResult.length
|
||||||
|
val operatorResult = attrOperatorParser(source, i)
|
||||||
|
i += operatorResult.length
|
||||||
|
val valueResult = valueParser(source, i)
|
||||||
|
i += valueResult.length
|
||||||
|
SyntaxError.assert(source, i, "]")
|
||||||
|
i++
|
||||||
|
ParserResult(
|
||||||
|
BinaryExpression(
|
||||||
|
parserResult.data,
|
||||||
|
operatorResult.data,
|
||||||
|
valueResult.data
|
||||||
|
), i - offset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val selectorUnitParser = Parser { source, offset, _ ->
|
||||||
|
var i = offset
|
||||||
|
var match = false
|
||||||
|
if (source.getOrNull(i) == '@') {
|
||||||
|
match = true
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
val nameResult = nameParser(source, i)
|
||||||
|
i += nameResult.length
|
||||||
|
val attrList = mutableListOf<BinaryExpression>()
|
||||||
|
while (i < source.length && source[i] == '[') {
|
||||||
|
val attrResult = attrParser(source, i)
|
||||||
|
i += attrResult.length
|
||||||
|
attrList.add(attrResult.data)
|
||||||
|
}
|
||||||
|
if (nameResult.length == 0 && attrList.size == 0) {
|
||||||
|
SyntaxError.throwError(source, i, "[")
|
||||||
|
}
|
||||||
|
ParserResult(PropertySegment(match, nameResult.data, attrList), i - offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
val connectSelectorParser = Parser { source, offset, _ ->
|
||||||
|
var i = offset
|
||||||
|
i += whiteCharParser(source, i).length
|
||||||
|
val topSelector = selectorUnitParser(source, i)
|
||||||
|
i += topSelector.length
|
||||||
|
val selectorList = mutableListOf<Pair<ConnectSegment, PropertySegment>>()
|
||||||
|
while (i < source.length && whiteCharParser.prefix.contains(source[i])) {
|
||||||
|
i += whiteCharStrictParser(source, i).length
|
||||||
|
val combinator = if (combinatorParser.prefix.contains((source[i]))) {
|
||||||
|
val combinatorResult = combinatorParser(source, i)
|
||||||
|
i += combinatorResult.length
|
||||||
|
i += whiteCharStrictParser(source, i).length
|
||||||
|
combinatorResult.data
|
||||||
|
} else {
|
||||||
|
ConnectSegment(polynomialExpression = PolynomialExpression(1, 0))
|
||||||
|
}
|
||||||
|
val selectorResult = selectorUnitParser(source, i)
|
||||||
|
i += selectorResult.length
|
||||||
|
selectorList.add(combinator to selectorResult.data)
|
||||||
|
}
|
||||||
|
ParserResult(topSelector.data to selectorList, i - offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
val endParser = Parser { source, offset, _ ->
|
||||||
|
if (offset != source.length) {
|
||||||
|
SyntaxError.throwError(source, offset, "end")
|
||||||
|
}
|
||||||
|
ParserResult(Unit, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
val selectorParser: (String) -> Selector = { source ->
|
||||||
|
var i = 0
|
||||||
|
i += whiteCharParser(source, i).length
|
||||||
|
val combinatorSelectorResult = connectSelectorParser(source, i)
|
||||||
|
i += combinatorSelectorResult.length
|
||||||
|
|
||||||
|
i += whiteCharParser(source, i).length
|
||||||
|
i += endParser(source, i).length
|
||||||
|
val data = combinatorSelectorResult.data
|
||||||
|
val propertySelectorList = mutableListOf<PropertySegment>()
|
||||||
|
val combinatorSelectorList = mutableListOf<ConnectSegment>()
|
||||||
|
propertySelectorList.add(data.first)
|
||||||
|
data.second.forEach {
|
||||||
|
propertySelectorList.add(it.second)
|
||||||
|
combinatorSelectorList.add(it.first)
|
||||||
|
}
|
||||||
|
val wrapperList = mutableListOf(PropertyWrapper(propertySelectorList.first()))
|
||||||
|
combinatorSelectorList.forEachIndexed { index, combinatorSelector ->
|
||||||
|
val combinatorSelectorWrapper =
|
||||||
|
ConnectWrapper(combinatorSelector, wrapperList.last())
|
||||||
|
val propertySelectorWrapper =
|
||||||
|
PropertyWrapper(propertySelectorList[index + 1], combinatorSelectorWrapper)
|
||||||
|
wrapperList.add(propertySelectorWrapper)
|
||||||
|
}
|
||||||
|
Selector(wrapperList.last())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package li.songe.selector_core.parser
|
||||||
|
|
||||||
|
data class SyntaxError(val expectedValue: String, val position: Int, val source: String) :
|
||||||
|
Exception(
|
||||||
|
"expected $expectedValue in selector at position $position, but got ${
|
||||||
|
source.getOrNull(
|
||||||
|
position
|
||||||
|
)
|
||||||
|
}"
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun assert(source: String, offset: Int, value: String = "", expectedValue: String? = null) {
|
||||||
|
if (offset >= source.length || (value.isNotEmpty() && !value.contains(source[offset]))) {
|
||||||
|
throw SyntaxError(expectedValue ?: value, offset, source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun throwError(source: String, offset: Int, expectedValue: String = ""): Nothing {
|
||||||
|
throw SyntaxError(expectedValue, offset, source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,17 @@
|
||||||
rootProject.name = "gkd"
|
rootProject.name = "gkd"
|
||||||
include(":app")
|
include(":app")
|
||||||
include(":selector")
|
|
||||||
include(":router")
|
include(":router")
|
||||||
|
include(":selector_core")
|
||||||
|
include(":selector_android")
|
||||||
|
|
||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
maven("https://plugins.gradle.org/m2/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dependencyResolutionManagement {
|
dependencyResolutionManagement {
|
||||||
|
// https://youtrack.jetbrains.com/issue/KT-55620
|
||||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
repositories {
|
repositories {
|
||||||
mavenLocal()
|
mavenLocal()
|
||||||
|
@ -13,7 +21,6 @@ dependencyResolutionManagement {
|
||||||
}
|
}
|
||||||
versionCatalogs {
|
versionCatalogs {
|
||||||
create("libs") {
|
create("libs") {
|
||||||
// 当前 android 项目 kotlin 的版本
|
|
||||||
library("android.gradle", "com.android.tools.build:gradle:7.3.1")
|
library("android.gradle", "com.android.tools.build:gradle:7.3.1")
|
||||||
|
|
||||||
version("android.compileSdk", "33")
|
version("android.compileSdk", "33")
|
||||||
|
@ -21,6 +28,7 @@ dependencyResolutionManagement {
|
||||||
version("android.targetSdk", "33")
|
version("android.targetSdk", "33")
|
||||||
version("android.buildToolsVersion", "33.0.0")
|
version("android.buildToolsVersion", "33.0.0")
|
||||||
|
|
||||||
|
// 当前 android 项目 kotlin 的版本
|
||||||
library("kotlin.gradle.plugin", "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.20")
|
library("kotlin.gradle.plugin", "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.20")
|
||||||
library("kotlin.serialization", "org.jetbrains.kotlin:kotlin-serialization:1.8.20")
|
library("kotlin.serialization", "org.jetbrains.kotlin:kotlin-serialization:1.8.20")
|
||||||
// library("kotlin.stdlib", "org.jetbrains.kotlin:kotlin-stdlib:1.8.10")
|
// library("kotlin.stdlib", "org.jetbrains.kotlin:kotlin-stdlib:1.8.10")
|
||||||
|
@ -58,6 +66,8 @@ dependencyResolutionManagement {
|
||||||
library("others.zxing.android.embedded", "com.journeyapps:zxing-android-embedded:4.3.0")
|
library("others.zxing.android.embedded", "com.journeyapps:zxing-android-embedded:4.3.0")
|
||||||
library("others.floating.bubble.view", "io.github.torrydo:floating-bubble-view:0.5.2")
|
library("others.floating.bubble.view", "io.github.torrydo:floating-bubble-view:0.5.2")
|
||||||
|
|
||||||
|
|
||||||
|
library("androidx.localbroadcastmanager", "androidx.localbroadcastmanager:localbroadcastmanager:1.1.0")
|
||||||
library("androidx.appcompat", "androidx.appcompat:appcompat:1.6.1")
|
library("androidx.appcompat", "androidx.appcompat:appcompat:1.6.1")
|
||||||
library("androidx.core.ktx", "androidx.core:core-ktx:1.10.0")
|
library("androidx.core.ktx", "androidx.core:core-ktx:1.10.0")
|
||||||
library(
|
library(
|
||||||
|
@ -72,7 +82,6 @@ dependencyResolutionManagement {
|
||||||
library("androidx.room.compiler", "androidx.room:room-compiler:2.5.1")
|
library("androidx.room.compiler", "androidx.room:room-compiler:2.5.1")
|
||||||
library("androidx.room.ktx", "androidx.room:room-ktx:2.5.1")
|
library("androidx.room.ktx", "androidx.room:room-ktx:2.5.1")
|
||||||
|
|
||||||
library("google.material", "com.google.android.material:material:1.8.0")
|
|
||||||
library(
|
library(
|
||||||
"google.accompanist.drawablepainter",
|
"google.accompanist.drawablepainter",
|
||||||
"com.google.accompanist:accompanist-drawablepainter:0.23.1"
|
"com.google.accompanist:accompanist-drawablepainter:0.23.1"
|
||||||
|
@ -113,13 +122,9 @@ dependencyResolutionManagement {
|
||||||
"org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5"
|
"org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// https://developer.android.com/reference/kotlin/org/json/package-summary
|
||||||
library("org.json", "org.json:json:20210307")
|
library("org.json", "org.json:json:20210307")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pluginManagement {
|
|
||||||
repositories {
|
|
||||||
maven("https://plugins.gradle.org/m2/")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user