mirror of
https://github.com/gkd-kit/gkd.git
synced 2024-11-16 11:42:22 +08:00
feat: selector_core
This commit is contained in:
parent
dcb86b3e94
commit
0038d5eb98
|
@ -9,6 +9,7 @@ plugins {
|
|||
|
||||
@Suppress("UnstableApiUsage")
|
||||
android {
|
||||
namespace = "li.songe.gkd"
|
||||
compileSdk = libs.versions.android.compileSdk.get().toInt()
|
||||
buildToolsVersion = libs.versions.android.buildToolsVersion.get()
|
||||
|
||||
|
@ -35,7 +36,6 @@ android {
|
|||
|
||||
lint {
|
||||
disable.add("ModifierFactoryUnreferencedReceiver")
|
||||
// baseline = file("lint-baseline.xml")
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
|
@ -85,6 +85,7 @@ android {
|
|||
freeCompilerArgs = freeCompilerArgs + "-opt-in=kotlin.RequiresOptIn"
|
||||
}
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
compose = true
|
||||
}
|
||||
composeOptions {
|
||||
|
@ -108,12 +109,14 @@ android {
|
|||
}
|
||||
|
||||
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(libs.androidx.appcompat)
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.localbroadcastmanager)
|
||||
|
||||
implementation(libs.compose.ui)
|
||||
implementation(libs.compose.material)
|
||||
|
@ -146,7 +149,6 @@ dependencies {
|
|||
implementation(libs.ktor.client.content.negotiation)
|
||||
implementation(libs.ktor.serialization.kotlinx.json)
|
||||
|
||||
implementation(libs.google.material)
|
||||
implementation(libs.google.accompanist.drawablepainter)
|
||||
implementation(libs.google.accompanist.placeholder.material)
|
||||
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "5e3c352578a63c3fccbb5e3fba31c89d",
|
||||
"identityHash": "2083d8585fffd897fde3733958e356f8",
|
||||
"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, `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": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
|
@ -33,8 +33,8 @@
|
|||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "comment",
|
||||
"columnName": "comment",
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
|
@ -44,6 +44,12 @@
|
|||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "version",
|
||||
"columnName": "version",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "filePath",
|
||||
"columnName": "file_path",
|
||||
|
@ -58,10 +64,10 @@
|
|||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
|
@ -136,10 +142,10 @@
|
|||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
|
@ -148,7 +154,7 @@
|
|||
"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, '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"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="li.songe.gkd">
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
|
@ -19,6 +18,7 @@
|
|||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
|
||||
|
||||
|
||||
<application
|
||||
android:name="li.songe.gkd.App"
|
||||
android:allowBackup="true"
|
||||
|
@ -61,7 +61,7 @@
|
|||
</activity>
|
||||
|
||||
<service
|
||||
android:name="li.songe.gkd.accessibility.GkdAbService"
|
||||
android:name=".accessibility.GkdAbService"
|
||||
android:exported="false"
|
||||
android:label="@string/accessibility_service_label"
|
||||
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
|
||||
MMKV.initialize(this)
|
||||
LogUtils.d(Storage.settings)
|
||||
if (!Storage.settings.enableConsoleLogOut){
|
||||
LogUtils.d("关闭日志控制台输出")
|
||||
}
|
||||
LogUtils.getConfig().apply {
|
||||
isLog2FileSwitch = true
|
||||
saveDays = 30
|
||||
|
|
|
@ -6,7 +6,7 @@ import androidx.compose.runtime.CompositionLocalProvider
|
|||
import com.blankj.utilcode.util.LogUtils
|
||||
import com.dylanc.activityresult.launcher.StartActivityLauncher
|
||||
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.theme.MainTheme
|
||||
import li.songe.gkd.util.Ext.LocalLauncher
|
||||
|
|
|
@ -2,20 +2,28 @@ package li.songe.gkd.accessibility
|
|||
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import com.blankj.utilcode.util.LogUtils
|
||||
import com.blankj.utilcode.util.NetworkUtils
|
||||
import com.blankj.utilcode.util.ScreenUtils
|
||||
import com.blankj.utilcode.util.ServiceUtils
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import kotlinx.coroutines.delay
|
||||
import li.songe.gkd.composition.CompositionAbService
|
||||
import li.songe.gkd.composition.Hook.useLifeCycleLog
|
||||
import li.songe.gkd.composition.Hook.useScope
|
||||
import li.songe.gkd.composition.CompositionExt.useLifeCycleLog
|
||||
import li.songe.gkd.composition.CompositionExt.useScope
|
||||
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.util.Ext.buildRuleManager
|
||||
import li.songe.gkd.util.Ext.getActivityIdByShizuku
|
||||
import li.songe.gkd.util.Ext.getSubsFileLastModified
|
||||
import li.songe.gkd.util.Ext.launchWhile
|
||||
import li.songe.gkd.util.Singleton
|
||||
import li.songe.gkd.util.Storage
|
||||
import li.songe.selector.GkdSelector
|
||||
import li.songe.selector_android.GkdSelector
|
||||
import java.io.File
|
||||
|
||||
class GkdAbService : CompositionAbService({
|
||||
useLifeCycleLog()
|
||||
|
@ -28,12 +36,14 @@ class GkdAbService : CompositionAbService({
|
|||
onDestroy { service = null }
|
||||
|
||||
KeepAliveService.start(context)
|
||||
onDestroy {
|
||||
KeepAliveService.stop(context)
|
||||
}
|
||||
|
||||
var serviceConnected = false
|
||||
onServiceConnected { serviceConnected = true }
|
||||
onInterrupt { serviceConnected = false }
|
||||
|
||||
|
||||
onAccessibilityEvent { event ->
|
||||
val activityId = event?.className?.toString() ?: return@onAccessibilityEvent
|
||||
val rootAppId = rootInActiveWindow?.packageName?.toString() ?: return@onAccessibilityEvent
|
||||
|
@ -105,6 +115,36 @@ class GkdAbService : CompositionAbService({
|
|||
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()
|
||||
set(value) {
|
||||
|
|
|
@ -5,7 +5,7 @@ import android.content.Intent
|
|||
import kotlinx.coroutines.delay
|
||||
import li.songe.gkd.App
|
||||
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.launchWhile
|
||||
|
||||
|
@ -21,5 +21,9 @@ class KeepAliveService : CompositionService({
|
|||
fun start(context: Context = App.context) {
|
||||
context.startForegroundService(Intent(context, KeepAliveService::class.java))
|
||||
}
|
||||
|
||||
fun stop(context: Context = App.context) {
|
||||
context.stopService(Intent(context, KeepAliveService::class.java))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,8 +16,8 @@ import kotlinx.serialization.encodeToString
|
|||
import li.songe.gkd.util.Singleton
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
object Hook {
|
||||
fun CanOnDestroy.useScope( context: CoroutineContext=Dispatchers.Default): CoroutineScope {
|
||||
object CompositionExt {
|
||||
fun CanOnDestroy.useScope(context: CoroutineContext = Dispatchers.Default): CoroutineScope {
|
||||
val scope = CoroutineScope(context)
|
||||
onDestroy { scope.cancel() }
|
||||
return scope
|
||||
|
@ -39,6 +39,7 @@ object Hook {
|
|||
}
|
||||
}
|
||||
val filter = IntentFilter(packageName)
|
||||
|
||||
val broadcastManager = LocalBroadcastManager.getInstance(this)
|
||||
broadcastManager.registerReceiver(receiver, filter)
|
||||
val sendMessage: (InvokeMessage) -> Unit = { message ->
|
|
@ -1,14 +1,15 @@
|
|||
package li.songe.gkd.data
|
||||
|
||||
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(
|
||||
/**
|
||||
* length>0
|
||||
*/
|
||||
val matches: List<GkdSelector> = emptyList(),
|
||||
val excludeMatches: List<GkdSelector> = emptyList(),
|
||||
val matches: List<Selector> = emptyList(),
|
||||
val excludeMatches: List<Selector> = emptyList(),
|
||||
/**
|
||||
* 任意一个元素是上次触发过的
|
||||
*/
|
||||
|
@ -34,11 +35,11 @@ data class Rule(
|
|||
fun query(nodeInfo: AccessibilityNodeInfo?): AccessibilityNodeInfo? {
|
||||
if (nodeInfo == null) return null
|
||||
var target: AccessibilityNodeInfo? = null
|
||||
for (gkd in matches) {
|
||||
target = gkd.collect(nodeInfo) ?: return null
|
||||
for (selector in matches) {
|
||||
target = nodeInfo.querySelector(selector) ?: return null
|
||||
}
|
||||
for (gkd in excludeMatches) {
|
||||
if (gkd.collect(nodeInfo) != null) return null
|
||||
for (selector in excludeMatches) {
|
||||
if (nodeInfo.querySelector(selector) != null) return null
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package li.songe.gkd.data
|
||||
|
||||
import li.songe.selector.GkdSelector
|
||||
import li.songe.selector_core.Selector
|
||||
|
||||
class RuleManager(vararg subscriptionRawArray: SubscriptionRaw) {
|
||||
|
||||
|
@ -42,13 +42,14 @@ class RuleManager(vararg subscriptionRawArray: SubscriptionRaw) {
|
|||
?: appRaw.excludeActivityIds
|
||||
?: emptyList()).toSet()
|
||||
|
||||
|
||||
ruleGroupList.add(
|
||||
Rule(
|
||||
cd = cd,
|
||||
index = count,
|
||||
matches = ruleRaw.matches.map { GkdSelector.gkdSelectorParser(it) },
|
||||
matches = ruleRaw.matches.map { Selector.parse(it) },
|
||||
excludeMatches = ruleRaw.excludeMatches.map {
|
||||
GkdSelector.gkdSelectorParser(
|
||||
Selector.parse(
|
||||
it
|
||||
)
|
||||
},
|
||||
|
|
|
@ -7,7 +7,7 @@ import kotlinx.serialization.Serializable
|
|||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.*
|
||||
import li.songe.gkd.util.Singleton
|
||||
import li.songe.selector.GkdSelector
|
||||
import li.songe.selector_android.GkdSelector
|
||||
|
||||
|
||||
@Parcelize
|
||||
|
@ -200,7 +200,7 @@ data class SubscriptionRaw(
|
|||
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
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
|
||||
|
||||
@Database(
|
||||
version = 3,
|
||||
version = 1,
|
||||
entities = [SubsItem::class, SubsConfig::class],
|
||||
autoMigrations = [
|
||||
// AutoMigration(from = 1, to = 2),
|
||||
|
|
|
@ -5,7 +5,6 @@ import androidx.room.Insert
|
|||
import androidx.room.RawQuery
|
||||
import androidx.room.Update
|
||||
import androidx.sqlite.db.SupportSQLiteQuery
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface BaseDao<T : Any> {
|
||||
@Insert
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package li.songe.gkd.db
|
||||
|
||||
interface BaseTable {
|
||||
var id: Long
|
||||
var ctime: Long
|
||||
var mtime: Long
|
||||
val id: Long
|
||||
val ctime: 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(
|
||||
tableName = "subs_config",
|
||||
// indices = [Index(value = ["url"], unique = true)]
|
||||
)
|
||||
@Parcelize
|
||||
data class SubsConfig(
|
||||
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") override var id: Long = 0,
|
||||
@ColumnInfo(name = "ctime") override var ctime: Long = System.currentTimeMillis(),
|
||||
@ColumnInfo(name = "mtime") override var mtime: Long = System.currentTimeMillis(),
|
||||
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") override val id: Long = 0,
|
||||
@ColumnInfo(name = "ctime") override val ctime: Long = System.currentTimeMillis(),
|
||||
@ColumnInfo(name = "mtime") override val mtime: Long = System.currentTimeMillis(),
|
||||
|
||||
/**
|
||||
* 0 - app
|
||||
* 1 - group
|
||||
* 2 - rule
|
||||
*/
|
||||
@ColumnInfo(name = "type") var type: Int = 0,
|
||||
@ColumnInfo(name = "enable") var enable: Boolean = true,
|
||||
@ColumnInfo(name = "type") val type: Int = 0,
|
||||
@ColumnInfo(name = "enable") val enable: Boolean = true,
|
||||
|
||||
@ColumnInfo(name = "subs_item_id") var subsItemId: Long = -1,
|
||||
@ColumnInfo(name = "app_id") var appId: String = "",
|
||||
@ColumnInfo(name = "group_key") var groupKey: Int = -1,
|
||||
@ColumnInfo(name = "rule_key") var ruleKey: Int = -1,
|
||||
@ColumnInfo(name = "subs_item_id") val subsItemId: Long = -1,
|
||||
@ColumnInfo(name = "app_id") val appId: String = "",
|
||||
@ColumnInfo(name = "group_key") val groupKey: Int = -1,
|
||||
@ColumnInfo(name = "rule_key") val ruleKey: Int = -1,
|
||||
) : BaseTable, Parcelable {
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -19,30 +19,36 @@ data class SubsItem(
|
|||
/**
|
||||
* 当主键是0时,autoGenerate将覆盖此字段,插入数据库后 需要用返回值手动更新此字段
|
||||
*/
|
||||
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") override var id: Long = 0,
|
||||
@ColumnInfo(name = "ctime") override var ctime: Long = System.currentTimeMillis(),
|
||||
@ColumnInfo(name = "mtime") override var mtime: Long = System.currentTimeMillis(),
|
||||
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") override val id: Long = 0,
|
||||
@ColumnInfo(name = "ctime") override val ctime: Long = System.currentTimeMillis(),
|
||||
@ColumnInfo(name = "mtime") override val mtime: Long = System.currentTimeMillis(),
|
||||
|
||||
@ColumnInfo(name = "enable") 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 {
|
||||
|
|
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,
|
||||
) =
|
||||
Expression(
|
||||
RoomAnnotation.getColumnName(T::class.java.name, name),
|
||||
RoomAnnotation.getColumnName(T::class, name),
|
||||
operator,
|
||||
value,
|
||||
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, "==")
|
||||
|
||||
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, "!=")
|
||||
|
||||
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, "<")
|
||||
|
||||
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, "<=")
|
||||
|
||||
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, ">")
|
||||
|
||||
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, ">=")
|
||||
|
||||
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")
|
||||
|
||||
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")
|
||||
|
||||
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")
|
||||
|
||||
inline fun <reified T : Any, V, V2> KProperty1<T, V>.baseOperator(
|
||||
|
@ -56,7 +56,7 @@ object Operator {
|
|||
operator: String,
|
||||
) =
|
||||
Expression(
|
||||
RoomAnnotation.getColumnName(T::class.java.name, name),
|
||||
RoomAnnotation.getColumnName(T::class, name),
|
||||
operator,
|
||||
value,
|
||||
T::class
|
||||
|
|
|
@ -1,42 +1,56 @@
|
|||
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 {
|
||||
fun getTableName(className: String): String = when (className) {
|
||||
"li.songe.gkd.db.table.SubsConfig" -> "subs_config"
|
||||
"li.songe.gkd.db.table.SubsItem" -> "subs_item"
|
||||
"r-1682430013322" -> "avoid_compile_error"
|
||||
else -> throw Exception("""not found className : $className""")
|
||||
|
||||
fun getTableName(cls: KClass<*>): String = when (cls) {
|
||||
SubsConfig::class -> "subs_config"
|
||||
SubsItem::class -> "subs_item"
|
||||
TriggerLog::class -> "trigger_log"
|
||||
else -> throw Exception("""not found className : ${cls.qualifiedName}""")
|
||||
}
|
||||
|
||||
fun getColumnName(className: String, propertyName: String): String = when (className) {
|
||||
"li.songe.gkd.db.table.SubsConfig" -> when (propertyName) {
|
||||
"id" -> "id"
|
||||
"ctime" -> "ctime"
|
||||
"mtime" -> "mtime"
|
||||
"type" -> "type"
|
||||
"enable" -> "enable"
|
||||
"subsItemId" -> "subs_item_id"
|
||||
"appId" -> "app_id"
|
||||
"groupKey" -> "group_key"
|
||||
"ruleKey" -> "rule_key"
|
||||
"r-1682430013322" -> "avoid_compile_error"
|
||||
else -> throw Exception("""not found columnName : $className#$propertyName""")
|
||||
fun getColumnName(cls: KClass<*>, propertyName: String): String = when (cls) {
|
||||
SubsConfig::class -> when (propertyName) {
|
||||
SubsConfig::id.name -> "id"
|
||||
SubsConfig::ctime.name -> "ctime"
|
||||
SubsConfig::mtime.name -> "mtime"
|
||||
SubsConfig::type.name -> "type"
|
||||
SubsConfig::enable.name -> "enable"
|
||||
SubsConfig::subsItemId.name -> "subs_item_id"
|
||||
SubsConfig::appId.name -> "app_id"
|
||||
SubsConfig::groupKey.name -> "group_key"
|
||||
SubsConfig::ruleKey.name -> "rule_key"
|
||||
else -> error("""not found columnName : ${cls.qualifiedName}#$propertyName""")
|
||||
}
|
||||
"li.songe.gkd.db.table.SubsItem" -> when (propertyName) {
|
||||
"id" -> "id"
|
||||
"ctime" -> "ctime"
|
||||
"mtime" -> "mtime"
|
||||
"enable" -> "enable"
|
||||
"name" -> "name"
|
||||
"updateUrl" -> "update_url"
|
||||
"filePath" -> "file_path"
|
||||
"index" -> "index"
|
||||
"r-1682430013322" -> "avoid_compile_error"
|
||||
else -> throw Exception("""not found columnName : $className#$propertyName""")
|
||||
|
||||
SubsItem::class -> when (propertyName) {
|
||||
SubsItem::id.name -> "id"
|
||||
SubsItem::ctime.name -> "ctime"
|
||||
SubsItem::mtime.name -> "mtime"
|
||||
SubsItem::enable.name -> "enable"
|
||||
SubsItem::name.name -> "name"
|
||||
SubsItem::updateUrl.name -> "update_url"
|
||||
SubsItem::filePath.name -> "file_path"
|
||||
SubsItem::index.name -> "index"
|
||||
else -> error("""not found columnName : ${cls.qualifiedName}#$propertyName""")
|
||||
}
|
||||
"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 li.songe.gkd.db.AppDatabase.Companion.db
|
||||
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 kotlin.reflect.KClass
|
||||
|
||||
|
@ -13,37 +13,13 @@ object RoomX {
|
|||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T : Any> getBaseDao(cls: KClass<T>) = when (cls) {
|
||||
SubsItem::class -> db.subsItemRoomDao()
|
||||
// SubsAppItem::class -> db.subsAppItemRoomDao()
|
||||
// SubsGroupItem::class -> db.subsGroupItemRoomDao()
|
||||
// SubsRuleItem::class -> db.subsRuleItemRoomDao()
|
||||
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>
|
||||
|
||||
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 {
|
||||
databaseBeforeHook(*objects)
|
||||
return getBaseDao(T::class).update(*objects)
|
||||
}
|
||||
|
||||
|
@ -51,10 +27,7 @@ object RoomX {
|
|||
* 插入成功后, 自动改变入参对象的 id
|
||||
*/
|
||||
suspend inline fun <reified T : Any> insert(vararg objects: T): List<Long> {
|
||||
databaseBeforeHook(*objects)
|
||||
return getBaseDao(T::class).insert(*objects).apply {
|
||||
databaseInsertAfterHook(objects, this)
|
||||
}
|
||||
return getBaseDao(T::class).insert(*objects)
|
||||
}
|
||||
|
||||
suspend inline fun <reified T : Any> delete(vararg objects: T) =
|
||||
|
@ -66,7 +39,7 @@ object RoomX {
|
|||
noinline block: (() -> Expression<*, *, T>)? = null
|
||||
): List<T> {
|
||||
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) {
|
||||
" WHERE ${expression.stringify()}"
|
||||
} else {
|
||||
|
@ -90,7 +63,7 @@ object RoomX {
|
|||
noinline block: (() -> Expression<*, *, T>)? = null
|
||||
): List<Int> {
|
||||
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) {
|
||||
" WHERE ${expression.stringify()}"
|
||||
} else {
|
||||
|
@ -107,42 +80,5 @@ object RoomX {
|
|||
val baseDao = getBaseDao(T::class)
|
||||
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.R
|
||||
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.debug.server.HttpService
|
||||
|
||||
|
|
|
@ -31,13 +31,14 @@ import kotlinx.coroutines.delay
|
|||
import kotlinx.coroutines.launch
|
||||
import li.songe.gkd.App
|
||||
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.debug.Ext.captureSnapshot
|
||||
import li.songe.gkd.debug.Ext.screenshotDir
|
||||
import li.songe.gkd.debug.Ext.snapshotDir
|
||||
import li.songe.gkd.debug.Ext.windowDir
|
||||
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.Singleton
|
||||
import li.songe.gkd.util.Storage
|
||||
|
@ -78,16 +79,19 @@ class HttpService : CompositionService({
|
|||
|
||||
routing {
|
||||
route("/api/rpc") {
|
||||
get("/device") {
|
||||
call.respond(Device.singleton)
|
||||
}
|
||||
get("/capture") {
|
||||
removeBubbles()
|
||||
delay(200)
|
||||
try {
|
||||
call.respond(captureSnapshot())
|
||||
} catch (e: Exception) {
|
||||
showBubbles()
|
||||
throw e
|
||||
} finally {
|
||||
showBubbles()
|
||||
}
|
||||
showBubbles()
|
||||
}
|
||||
get("/snapshot") {
|
||||
val id = call.request.queryParameters["id"]?.toLongOrNull()
|
||||
|
@ -95,7 +99,6 @@ class HttpService : CompositionService({
|
|||
call.response.cacheControl(CacheControl.MaxAge(3600))
|
||||
call.respondFile(snapshotDir, "/${id}.json")
|
||||
}
|
||||
|
||||
get("/window") {
|
||||
val id = call.request.queryParameters["id"]?.toLongOrNull()
|
||||
?: throw RpcError("miss id")
|
||||
|
@ -110,7 +113,6 @@ class HttpService : CompositionService({
|
|||
call.respondFile(screenshotDir, "/${id}.png")
|
||||
}
|
||||
}
|
||||
|
||||
listOf("/", "/index.html").forEach { p ->
|
||||
get(p) {
|
||||
val response = Singleton.client.get("$proxyUrl${call.request.uri}")
|
||||
|
|
|
@ -8,7 +8,7 @@ data class RpcError(
|
|||
val code: Int = 0,
|
||||
) : Exception(message) {
|
||||
companion object {
|
||||
const val HeaderKey = "X-Rpc-Result"
|
||||
const val HeaderKey = "X_Rpc_Result"
|
||||
const val HeaderOkValue = "ok"
|
||||
const val HeaderErrorValue = "error"
|
||||
}
|
||||
|
|
|
@ -11,23 +11,32 @@ import io.ktor.server.response.respond
|
|||
|
||||
val RpcErrorHeaderPlugin = createApplicationPlugin(name = "RpcErrorHeaderPlugin") {
|
||||
onCall { call ->
|
||||
Log.d("Ktor", "Request Path: ${call.request.uri}")
|
||||
Log.d("Ktor", "onCall: ${call.request.uri}")
|
||||
}
|
||||
on(CallFailed) { call, cause ->
|
||||
if (cause is RpcError) {
|
||||
// 主动抛出的错误
|
||||
LogUtils.d(call.request.uri, cause.code, cause.message)
|
||||
call.response.header(RpcError.HeaderKey, RpcError.HeaderErrorValue)
|
||||
call.respond(cause)
|
||||
} else if (cause is Exception) {
|
||||
// 未知错误
|
||||
LogUtils.d(call.request.uri, cause.message)
|
||||
cause.printStackTrace()
|
||||
call.respond(HttpStatusCode.InternalServerError, cause)
|
||||
when (cause) {
|
||||
is RpcError -> {
|
||||
// 主动抛出的错误
|
||||
LogUtils.d(call.request.uri, cause.code, cause.message)
|
||||
call.response.header(RpcError.HeaderKey, RpcError.HeaderErrorValue)
|
||||
call.respond(cause)
|
||||
}
|
||||
|
||||
is Exception -> {
|
||||
// 未知错误
|
||||
LogUtils.d(call.request.uri, cause.message)
|
||||
cause.printStackTrace()
|
||||
call.respond(HttpStatusCode.InternalServerError, cause)
|
||||
}
|
||||
|
||||
else -> {
|
||||
cause.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
onCallRespond { call, _ ->
|
||||
if (call.response.status() == HttpStatusCode.OK &&
|
||||
val status=call.response.status() ?: HttpStatusCode.OK
|
||||
if (status == HttpStatusCode.OK &&
|
||||
!call.response.headers.contains(
|
||||
RpcError.HeaderKey
|
||||
)
|
||||
|
|
|
@ -6,12 +6,12 @@ import kotlinx.serialization.Serializable
|
|||
|
||||
@Serializable
|
||||
data class Attr(
|
||||
val id: String?,
|
||||
val className: String?,
|
||||
val childCount: Int,
|
||||
val text: String?,
|
||||
val isClickable: Boolean,
|
||||
val desc: String?,
|
||||
val id: String? = null,
|
||||
val className: String? = null,
|
||||
val childCount: Int = 0,
|
||||
val text: String? = null,
|
||||
val isClickable: Boolean = false,
|
||||
val desc: String? = null,
|
||||
val left: Int,
|
||||
val top: 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 kotlinx.serialization.Serializable
|
||||
import li.songe.selector.forEach
|
||||
import li.songe.selector_android.forEach
|
||||
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.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
@ -44,10 +43,10 @@ val DebugPage = Page {
|
|||
val launcher = LocalLauncher.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
var httpServerRunning by usePollState { HttpService.isRunning() }
|
||||
var screenshotRunning by usePollState { ScreenshotService.isRunning() }
|
||||
var gkdAccessRunning by usePollState { GkdAbService.isRunning() }
|
||||
var floatingRunning by usePollState {
|
||||
val httpServerRunning by usePollState { HttpService.isRunning() }
|
||||
val screenshotRunning by usePollState { ScreenshotService.isRunning() }
|
||||
val gkdAccessRunning by usePollState { GkdAbService.isRunning() }
|
||||
val floatingRunning by usePollState {
|
||||
FloatingService.isRunning() && Settings.canDrawOverlays(
|
||||
context
|
||||
)
|
||||
|
@ -55,7 +54,7 @@ val DebugPage = Page {
|
|||
|
||||
|
||||
val debugAvailable by remember {
|
||||
derivedStateOf { httpServerRunning && screenshotRunning && gkdAccessRunning }
|
||||
derivedStateOf { httpServerRunning }
|
||||
}
|
||||
|
||||
val serverUrl by remember {
|
||||
|
@ -132,8 +131,8 @@ val DebugPage = Page {
|
|||
launcher.launch(intent) { resultCode, _ ->
|
||||
if (resultCode != ComponentActivity.RESULT_OK) return@launch
|
||||
if (!Settings.canDrawOverlays(context)) return@launch
|
||||
val intent = Intent(context, FloatingService::class.java)
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
val intent1 = Intent(context, FloatingService::class.java)
|
||||
ContextCompat.startForegroundService(context, intent1)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -11,6 +11,9 @@ import androidx.compose.foundation.layout.width
|
|||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
|
@ -20,31 +23,34 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.unit.dp
|
||||
import li.songe.gkd.R
|
||||
import li.songe.gkd.db.table.SubsItem
|
||||
import li.songe.gkd.util.Singleton
|
||||
|
||||
@Composable
|
||||
fun SubsItemCard(
|
||||
data: SubsItem,
|
||||
subsItem: SubsItem,
|
||||
onShareClick: (() -> Unit)? = null,
|
||||
onEditClick: (() -> Unit)? = null,
|
||||
onDelClick: (() -> Unit)? = null,
|
||||
onRefreshClick: (() -> Unit)? = null,
|
||||
) {
|
||||
|
||||
val dateStr by remember(subsItem) {
|
||||
derivedStateOf { "更新于:" + Singleton.simpleDateFormat.format(subsItem.mtime) }
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.alpha(if (data.enable) 1f else .3f),
|
||||
.alpha(if (subsItem.enable) 1f else .3f),
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = data.name,
|
||||
text = subsItem.name,
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = data.updateUrl,
|
||||
text = dateStr,
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
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
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
|
@ -10,17 +8,20 @@ import androidx.compose.material.AlertDialog
|
|||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextField
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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.unit.dp
|
||||
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.journeyapps.barcodescanner.ScanContract
|
||||
import com.journeyapps.barcodescanner.ScanOptions
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
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.util.Operator.eq
|
||||
import li.songe.gkd.db.util.RoomX
|
||||
import li.songe.gkd.hooks.useNavigateForQrcodeResult
|
||||
import li.songe.gkd.ui.SubsPage
|
||||
import li.songe.gkd.ui.component.SubsItemCard
|
||||
import li.songe.gkd.util.Ext.launchTry
|
||||
import li.songe.gkd.util.Singleton
|
||||
import li.songe.gkd.util.ThrottleState
|
||||
import li.songe.router.LocalRouter
|
||||
|
@ -42,40 +45,24 @@ import java.io.File
|
|||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
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 router = LocalRouter.current
|
||||
|
||||
var subItemList by remember { mutableStateOf(listOf<SubsItem>()) }
|
||||
var shareSubItem: SubsItem? by remember { mutableStateOf(null) }
|
||||
var shareQrcode: ImageBitmap? 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 showLinkInputDialog by remember { mutableStateOf(false) }
|
||||
val viewSubItemThrottle = ThrottleState.use(scope)
|
||||
val editSubItemThrottle = ThrottleState.use(scope)
|
||||
val refreshSubItemThrottle = ThrottleState.use(scope, 250)
|
||||
val navigateForQrcodeResult = useNavigateForQrcodeResult()
|
||||
|
||||
var linkText by remember {
|
||||
mutableStateOf("")
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
subItemList = RoomX.select<SubsItem>().sortedBy { it.index }
|
||||
|
@ -95,25 +82,64 @@ fun SubscriptionManagePage() {
|
|||
Text(
|
||||
text = "共有${subItemList.size}条订阅,激活:${subItemList.count { it.enable }},禁用:${subItemList.count { !it.enable }}",
|
||||
)
|
||||
Image(painter = painterResource(R.drawable.ic_add),
|
||||
contentDescription = "",
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
showAddDialog = true
|
||||
}
|
||||
.padding(4.dp)
|
||||
.size(25.dp))
|
||||
Row {
|
||||
Image(painter = painterResource(R.drawable.ic_add),
|
||||
contentDescription = "",
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
showAddDialog = true
|
||||
}
|
||||
.padding(4.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(
|
||||
modifier = Modifier
|
||||
.animateItemPlacement()
|
||||
.padding(vertical = 3.dp, horizontal = 8.dp)
|
||||
.clickable(onClick = viewSubItemThrottle.invoke {
|
||||
router.navigate(SubsPage, subItemList[i])
|
||||
}),
|
||||
.clickable(onClick = { router.navigate(SubsPage, subItemList[i]) }),
|
||||
elevation = 0.dp,
|
||||
border = BorderStroke(1.dp, Color(0xfff6f6f6)),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
|
@ -121,31 +147,34 @@ fun SubscriptionManagePage() {
|
|||
SubsItemCard(subItemList[i], onShareClick = {
|
||||
shareSubItem = subItemList[i]
|
||||
}, onEditClick = editSubItemThrottle.invoke {
|
||||
// val newSubsItem =
|
||||
// router.navigateForResult(SubsItemUpdatePage, subItemList[i])
|
||||
// ?: return@invoke
|
||||
// subItemList = subItemList.toMutableList().apply {
|
||||
// set(i, newSubsItem)
|
||||
// }
|
||||
}, onDelClick = {
|
||||
deleteSubItem = subItemList[i]
|
||||
}, onRefreshClick = refreshSubItemThrottle.invoke {
|
||||
val oldItem = subItemList[i]
|
||||
val subscriptionRaw = SubscriptionRaw.parse5(
|
||||
Singleton.client.get(subItemList[i].updateUrl).bodyAsText()
|
||||
Singleton.client.get(oldItem.updateUrl).bodyAsText()
|
||||
)
|
||||
subItemList = subItemList.toMutableList().also {
|
||||
it[i] = it[i].copy(
|
||||
updateUrl = subscriptionRaw.updateUrl
|
||||
?: subItemList[i].updateUrl,
|
||||
name = subscriptionRaw.name
|
||||
)
|
||||
RoomX.update(it[i])
|
||||
val f = File(it[i].filePath)
|
||||
f.writeText(SubscriptionRaw.stringify(subscriptionRaw))
|
||||
if (subscriptionRaw.version <= oldItem.version) {
|
||||
ToastUtils.showShort("暂无更新:${oldItem.name}")
|
||||
return@invoke
|
||||
}
|
||||
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 {
|
||||
if(!it.message.isNullOrEmpty()) {
|
||||
if (!it.message.isNullOrEmpty()) {
|
||||
ToastUtils.showShort(it.message)
|
||||
}
|
||||
})
|
||||
|
@ -153,7 +182,7 @@ fun SubscriptionManagePage() {
|
|||
}
|
||||
}
|
||||
|
||||
if (shareSubItem != null) {
|
||||
shareSubItem?.let { _shareSubItem ->
|
||||
Dialog(onDismissRequest = { shareSubItem = null }) {
|
||||
Box(
|
||||
Modifier
|
||||
|
@ -164,12 +193,25 @@ fun SubscriptionManagePage() {
|
|||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(text = "二维码",
|
||||
modifier = Modifier
|
||||
.clickable { }
|
||||
.clickable {
|
||||
shareQrcode = Singleton.barcodeEncoder
|
||||
.encodeBitmap(
|
||||
_shareSubItem.updateUrl,
|
||||
BarcodeFormat.QR_CODE,
|
||||
500,
|
||||
500
|
||||
)
|
||||
.asImageBitmap()
|
||||
shareSubItem = null
|
||||
}
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp))
|
||||
Text(text = "导出至剪切板",
|
||||
modifier = Modifier
|
||||
.clickable { }
|
||||
.clickable {
|
||||
ClipboardUtils.copyText(_shareSubItem.updateUrl)
|
||||
shareSubItem = null
|
||||
}
|
||||
.fillMaxWidth()
|
||||
.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)
|
||||
if (deleteSubItem != null) {
|
||||
|
@ -198,7 +250,6 @@ fun SubscriptionManagePage() {
|
|||
}
|
||||
subItemList = subItemList.toMutableList().also { it.remove(deleteSubItem) }
|
||||
deleteSubItem = null
|
||||
|
||||
}) {
|
||||
Text("是")
|
||||
}
|
||||
|
@ -213,7 +264,7 @@ fun SubscriptionManagePage() {
|
|||
}
|
||||
if (showAddDialog) {
|
||||
val clickQrcodeThrottle = ThrottleState.use(scope)
|
||||
Dialog(onDismissRequest = { showAddDialog = false },) {
|
||||
Dialog(onDismissRequest = { showAddDialog = false }) {
|
||||
Box(
|
||||
Modifier
|
||||
.width(250.dp)
|
||||
|
@ -225,23 +276,20 @@ fun SubscriptionManagePage() {
|
|||
text = "二维码", modifier = Modifier
|
||||
.clickable(onClick = clickQrcodeThrottle.invoke {
|
||||
showAddDialog = false
|
||||
scanLauncher.launch(ScanOptions().apply {
|
||||
setOrientationLocked(false)
|
||||
})
|
||||
val qrCode = navigateForQrcodeResult()
|
||||
val contents = qrCode.contents
|
||||
if (contents != null) {
|
||||
showLinkInputDialog = true
|
||||
linkText = contents
|
||||
}
|
||||
})
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp)
|
||||
)
|
||||
Text(text = "链接", modifier = Modifier
|
||||
.clickable {
|
||||
showLinkInputDialog = true
|
||||
showAddDialog = false
|
||||
scope.launch {
|
||||
// val newSubsItem =
|
||||
// router.navigateForResult(SubsItemInsertPage) ?: return@launch
|
||||
// subItemList = subItemList
|
||||
// .toMutableList()
|
||||
// .apply { add(newSubsItem) }
|
||||
}
|
||||
}
|
||||
.fillMaxWidth()
|
||||
.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.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.blankj.utilcode.util.ToastUtils
|
||||
import com.dylanc.activityresult.launcher.StartActivityLauncher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -192,14 +192,27 @@ object Ext {
|
|||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
block: suspend CoroutineScope.() -> Unit,
|
||||
): Job {
|
||||
return this.launch(context, start) {
|
||||
while (isActive) {
|
||||
block()
|
||||
}
|
||||
) = launch(context, start) {
|
||||
while (isActive) {
|
||||
block()
|
||||
}
|
||||
}
|
||||
|
||||
fun CoroutineScope.launchTry(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
block: suspend CoroutineScope.() -> Unit,
|
||||
) = launch(context, start) {
|
||||
try {
|
||||
block()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
ToastUtils.showShort(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
fun createNotificationChannel(context: Service) {
|
||||
val channelId = "CHANNEL_TEST"
|
||||
val intent = Intent(context, MainActivity::class.java).apply {
|
||||
|
|
|
@ -4,18 +4,18 @@ import blue.endless.jankson.Jankson
|
|||
import com.journeyapps.barcodescanner.BarcodeEncoder
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.cio.CIO
|
||||
import io.ktor.client.plugins.HttpTimeout
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
object Singleton {
|
||||
|
||||
// @OptIn(ExperimentalSerializationApi::class)
|
||||
val json by lazy {
|
||||
Json {
|
||||
// prettyPrint = true
|
||||
// prettyPrintIndent = "\u0020".repeat(2)
|
||||
isLenient = true
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
|
@ -26,21 +26,13 @@ object Singleton {
|
|||
install(ContentNegotiation) {
|
||||
json(json, ContentType.Any)
|
||||
}
|
||||
install(HttpTimeout){
|
||||
connectTimeoutMillis = 3000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// inline fun <reified T : Any> produce(data: T, block: (data: T) -> Unit): T {
|
||||
// val proxyData = Proxy.newProxyInstance(
|
||||
// T::class.java.classLoader,
|
||||
// arrayOf(),
|
||||
// InvocationHandler { proxy, method, args ->
|
||||
//
|
||||
// }) as T
|
||||
// block(proxyData)
|
||||
// return proxyData
|
||||
// }
|
||||
val simpleDateFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
|
||||
|
||||
val barcodeEncoder by lazy { BarcodeEncoder() }
|
||||
|
||||
|
||||
}
|
|
@ -1,16 +1,11 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.Gkd" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<style name="Theme.Gkd" parent="Theme.AppCompat">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_200</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>
|
||||
<item name="colorPrimary">#ededed</item>
|
||||
<!-- 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. -->
|
||||
</style>
|
||||
</resources>
|
|
@ -1,16 +1,10 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.Gkd" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<style name="Theme.Gkd" parent="Theme.AppCompat">
|
||||
<!-- Primary brand color. -->
|
||||
<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. -->
|
||||
<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. -->
|
||||
</style>
|
||||
|
@ -23,12 +17,4 @@
|
|||
</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>
|
|
@ -1,6 +1,12 @@
|
|||
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).
|
||||
|
@ -8,16 +14,82 @@ import org.junit.Test
|
|||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
// assertEquals(4, 4L)
|
||||
// println(MatchRule.parse("ImageView[text=hi][id=hi] >> WebView[text=hi] - TextView"))
|
||||
// val testFile = File("D:/User/Documents/Project/gkd-subscription/subs.json")
|
||||
// val subsRaw = SubscriptionRaw.parse(testFile.readText())
|
||||
// File("D:/User/Documents/Project/gkd-subscription/subs-2.json").writeText(
|
||||
// SubscriptionRaw.stringify(
|
||||
// subsRaw
|
||||
// @Test
|
||||
fun check_selector() {
|
||||
// println(Selector.parse("X View >n Text > Button[a=1][b=false][c=null][d!=`hello`] + A - X < Z"))
|
||||
// println(Selector.parse("A[a=1][a!=3][a*=3][a!*=3][a^=null]"))
|
||||
// println(Selector.parse("@LinearLayout > TextView[id=`com.byted.pangle:id/tt_item_tv`][text=`不感兴趣`]"))
|
||||
|
||||
// val s1 = "ImageView < @FrameLayout < LinearLayout < RelativeLayout <n\n" +
|
||||
// "LinearLayout < RelativeLayout + LinearLayout > RelativeLayout > TextView[text$=`广告`]"
|
||||
// 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 {
|
||||
delete(rootProject.buildDir)
|
||||
}
|
||||
|
|
|
@ -22,3 +22,4 @@ kotlin.code.style=official
|
|||
#org.gradle.java.home=D\:/User/Documents/lisonge/.jdks/corretto-11.0.13
|
||||
#android.experimental.legacyTransform.forceNonIncremental=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 {
|
||||
namespace = "li.songe.selector"
|
||||
namespace = "li.songe.selector_android"
|
||||
compileSdk = libs.versions.android.compileSdk.get().toInt()
|
||||
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.ext.junit.runners.AndroidJUnit4
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="li.songe.selector">
|
||||
package="li.songe.selector_android">
|
||||
|
||||
</manifest>
|
|
@ -1,12 +1,12 @@
|
|||
package li.songe.selector
|
||||
package li.songe.selector_android
|
||||
|
||||
import android.accessibilityservice.AccessibilityService
|
||||
import android.accessibilityservice.GestureDescription
|
||||
import android.graphics.Path
|
||||
import android.graphics.Rect
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import li.songe.selector.parser.Transform
|
||||
import li.songe.selector.wrapper.PropertySelectorWrapper
|
||||
import li.songe.selector_android.parser.Transform
|
||||
import li.songe.selector_android.wrapper.PropertySelectorWrapper
|
||||
|
||||
data class GkdSelector(val wrapper: PropertySelectorWrapper) {
|
||||
|
||||
|
@ -19,6 +19,7 @@ data class GkdSelector(val wrapper: PropertySelectorWrapper) {
|
|||
return trackNodes.findLast { it != null } ?: child
|
||||
}
|
||||
}
|
||||
nodeInfo.getChild(1)
|
||||
return null
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package li.songe.selector
|
||||
package li.songe.selector_android
|
||||
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
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) {
|
||||
var p: AccessibilityNodeInfo? = this
|
||||
inline fun AccessibilityNodeInfo?.forEachAncestorIndexed(action: (depth: Int, ancestorNode: AccessibilityNodeInfo) -> Unit) {
|
||||
var p = this
|
||||
var depth = 0
|
||||
while (true) {
|
||||
val p2 = p?.parent
|
||||
|
@ -129,3 +129,5 @@ fun AccessibilityNodeInfo.getBrother(dep: Int, elder: Boolean = true): Accessibi
|
|||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package li.songe.selector.expression
|
||||
package li.songe.selector_android.expression
|
||||
|
||||
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?) {
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package li.songe.selector.operator
|
||||
package li.songe.selector_android.operator
|
||||
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import li.songe.selector.expression.BinaryExpression
|
||||
import li.songe.selector_android.expression.BinaryExpression
|
||||
|
||||
|
||||
object End : Operator("$=") {
|
|
@ -1,9 +1,9 @@
|
|||
package li.songe.selector.operator
|
||||
package li.songe.selector_android.operator
|
||||
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import li.songe.selector.expression.BinaryExpression
|
||||
import li.songe.selector.getDepth
|
||||
import li.songe.selector.getIndex
|
||||
import li.songe.selector_android.expression.BinaryExpression
|
||||
import li.songe.selector_android.getDepth
|
||||
import li.songe.selector_android.getIndex
|
||||
|
||||
object Equal : Operator("=") {
|
||||
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 li.songe.selector.expression.BinaryExpression
|
||||
import li.songe.selector_android.expression.BinaryExpression
|
||||
|
||||
|
||||
object Include : Operator("*=") {
|
|
@ -1,9 +1,9 @@
|
|||
package li.songe.selector.operator
|
||||
package li.songe.selector_android.operator
|
||||
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import li.songe.selector.expression.BinaryExpression
|
||||
import li.songe.selector.getDepth
|
||||
import li.songe.selector.getIndex
|
||||
import li.songe.selector_android.expression.BinaryExpression
|
||||
import li.songe.selector_android.getDepth
|
||||
import li.songe.selector_android.getIndex
|
||||
|
||||
object Less : Operator("<") {
|
||||
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 li.songe.selector.expression.BinaryExpression
|
||||
import li.songe.selector.getDepth
|
||||
import li.songe.selector.getIndex
|
||||
import li.songe.selector_android.expression.BinaryExpression
|
||||
import li.songe.selector_android.getDepth
|
||||
import li.songe.selector_android.getIndex
|
||||
|
||||
|
||||
object LessEqual : Operator("<=") {
|
|
@ -1,9 +1,9 @@
|
|||
package li.songe.selector.operator
|
||||
package li.songe.selector_android.operator
|
||||
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import li.songe.selector.expression.BinaryExpression
|
||||
import li.songe.selector.getDepth
|
||||
import li.songe.selector.getIndex
|
||||
import li.songe.selector_android.expression.BinaryExpression
|
||||
import li.songe.selector_android.getDepth
|
||||
import li.songe.selector_android.getIndex
|
||||
|
||||
object More : Operator(">") {
|
||||
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 li.songe.selector.expression.BinaryExpression
|
||||
import li.songe.selector.getDepth
|
||||
import li.songe.selector.getIndex
|
||||
import li.songe.selector_android.expression.BinaryExpression
|
||||
import li.songe.selector_android.getDepth
|
||||
import li.songe.selector_android.getIndex
|
||||
|
||||
|
||||
object MoreEqual : Operator(">=") {
|
|
@ -1,9 +1,9 @@
|
|||
package li.songe.selector.operator
|
||||
package li.songe.selector_android.operator
|
||||
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import li.songe.selector.expression.BinaryExpression
|
||||
import li.songe.selector.getDepth
|
||||
import li.songe.selector.getIndex
|
||||
import li.songe.selector_android.expression.BinaryExpression
|
||||
import li.songe.selector_android.getDepth
|
||||
import li.songe.selector_android.getIndex
|
||||
|
||||
|
||||
object NotEqual : Operator("!=") {
|
|
@ -1,7 +1,7 @@
|
|||
package li.songe.selector.operator
|
||||
package li.songe.selector_android.operator
|
||||
|
||||
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) {
|
||||
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 li.songe.selector.expression.BinaryExpression
|
||||
import li.songe.selector_android.expression.BinaryExpression
|
||||
|
||||
|
||||
object Start : Operator("^=") {
|
|
@ -1,4 +1,4 @@
|
|||
package li.songe.selector.parser
|
||||
package li.songe.selector_android.parser
|
||||
|
||||
open class GkdParser<T>(
|
||||
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)
|
|
@ -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) :
|
||||
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.expression.BinaryExpression
|
||||
import li.songe.selector.operator.End
|
||||
import li.songe.selector.operator.Equal
|
||||
import li.songe.selector.operator.Include
|
||||
import li.songe.selector.operator.Less
|
||||
import li.songe.selector.operator.LessEqual
|
||||
import li.songe.selector.operator.More
|
||||
import li.songe.selector.operator.MoreEqual
|
||||
import li.songe.selector.operator.NotEqual
|
||||
import li.songe.selector.operator.Start
|
||||
import li.songe.selector.selector.CombinatorSelector
|
||||
import li.songe.selector.selector.PropertySelector
|
||||
import li.songe.selector.wrapper.CombinatorSelectorWrapper
|
||||
import li.songe.selector.wrapper.PropertySelectorWrapper
|
||||
import li.songe.selector_android.GkdSelector
|
||||
import li.songe.selector_android.expression.BinaryExpression
|
||||
import li.songe.selector_android.operator.End
|
||||
import li.songe.selector_android.operator.Equal
|
||||
import li.songe.selector_android.operator.Include
|
||||
import li.songe.selector_android.operator.Less
|
||||
import li.songe.selector_android.operator.LessEqual
|
||||
import li.songe.selector_android.operator.More
|
||||
import li.songe.selector_android.operator.MoreEqual
|
||||
import li.songe.selector_android.operator.NotEqual
|
||||
import li.songe.selector_android.operator.Start
|
||||
import li.songe.selector_android.selector.CombinatorSelector
|
||||
import li.songe.selector_android.selector.PropertySelector
|
||||
import li.songe.selector_android.wrapper.CombinatorSelectorWrapper
|
||||
import li.songe.selector_android.wrapper.PropertySelectorWrapper
|
||||
|
||||
internal object Transform {
|
||||
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(
|
|
@ -1,15 +1,15 @@
|
|||
package li.songe.selector.wrapper
|
||||
package li.songe.selector_android.wrapper
|
||||
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import li.songe.selector.forEachAncestorIndexed
|
||||
import li.songe.selector.forEachElderBrotherIndexed
|
||||
import li.songe.selector.forEachIndexed
|
||||
import li.songe.selector.forEachYoungerBrotherIndexed
|
||||
import li.songe.selector.getAncestor
|
||||
import li.songe.selector.getBrother
|
||||
import li.songe.selector.getDepth
|
||||
import li.songe.selector.getIndex
|
||||
import li.songe.selector.selector.CombinatorSelector
|
||||
import li.songe.selector_android.forEachAncestorIndexed
|
||||
import li.songe.selector_android.forEachElderBrotherIndexed
|
||||
import li.songe.selector_android.forEachIndexed
|
||||
import li.songe.selector_android.forEachYoungerBrotherIndexed
|
||||
import li.songe.selector_android.getAncestor
|
||||
import li.songe.selector_android.getBrother
|
||||
import li.songe.selector_android.getDepth
|
||||
import li.songe.selector_android.getIndex
|
||||
import li.songe.selector_android.selector.CombinatorSelector
|
||||
|
||||
data class CombinatorSelectorWrapper(
|
||||
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 li.songe.selector.selector.PropertySelector
|
||||
import li.songe.selector_android.selector.PropertySelector
|
||||
|
||||
data class PropertySelectorWrapper(
|
||||
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.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"
|
||||
include(":app")
|
||||
include(":selector")
|
||||
include(":router")
|
||||
include(":selector_core")
|
||||
include(":selector_android")
|
||||
|
||||
pluginManagement {
|
||||
repositories {
|
||||
maven("https://plugins.gradle.org/m2/")
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
// https://youtrack.jetbrains.com/issue/KT-55620
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
mavenLocal()
|
||||
|
@ -13,7 +21,6 @@ dependencyResolutionManagement {
|
|||
}
|
||||
versionCatalogs {
|
||||
create("libs") {
|
||||
// 当前 android 项目 kotlin 的版本
|
||||
library("android.gradle", "com.android.tools.build:gradle:7.3.1")
|
||||
|
||||
version("android.compileSdk", "33")
|
||||
|
@ -21,6 +28,7 @@ dependencyResolutionManagement {
|
|||
version("android.targetSdk", "33")
|
||||
version("android.buildToolsVersion", "33.0.0")
|
||||
|
||||
// 当前 android 项目 kotlin 的版本
|
||||
library("kotlin.gradle.plugin", "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.20")
|
||||
library("kotlin.serialization", "org.jetbrains.kotlin:kotlin-serialization:1.8.20")
|
||||
// library("kotlin.stdlib", "org.jetbrains.kotlin:kotlin-stdlib:1.8.10")
|
||||
|
@ -58,6 +66,8 @@ dependencyResolutionManagement {
|
|||
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("androidx.localbroadcastmanager", "androidx.localbroadcastmanager:localbroadcastmanager:1.1.0")
|
||||
library("androidx.appcompat", "androidx.appcompat:appcompat:1.6.1")
|
||||
library("androidx.core.ktx", "androidx.core:core-ktx:1.10.0")
|
||||
library(
|
||||
|
@ -72,7 +82,6 @@ dependencyResolutionManagement {
|
|||
library("androidx.room.compiler", "androidx.room:room-compiler: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(
|
||||
"google.accompanist.drawablepainter",
|
||||
"com.google.accompanist:accompanist-drawablepainter:0.23.1"
|
||||
|
@ -113,13 +122,9 @@ dependencyResolutionManagement {
|
|||
"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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pluginManagement {
|
||||
repositories {
|
||||
maven("https://plugins.gradle.org/m2/")
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user