feat: selector_core

This commit is contained in:
lisonge 2023-05-18 15:34:49 +08:00
parent dcb86b3e94
commit 0038d5eb98
91 changed files with 1775 additions and 1341 deletions

View File

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

View File

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

View File

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

View File

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

View 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')"
]
}
}

View File

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

View File

@ -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*=`跳过`]"
}
]
}
]
}
]
}

View File

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

View File

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

View File

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

View File

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

View File

@ -16,7 +16,7 @@ import kotlinx.serialization.encodeToString
import li.songe.gkd.util.Singleton
import kotlin.coroutines.CoroutineContext
object Hook {
object CompositionExt {
fun CanOnDestroy.useScope(context: CoroutineContext = Dispatchers.Default): CoroutineScope {
val scope = CoroutineScope(context)
onDestroy { scope.cancel() }
@ -39,6 +39,7 @@ object Hook {
}
}
val filter = IntentFilter(packageName)
val broadcastManager = LocalBroadcastManager.getInstance(this)
broadcastManager.registerReceiver(receiver, filter)
val sendMessage: (InvokeMessage) -> Unit = { message ->

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,24 +79,26 @@ 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()
}
}
get("/snapshot") {
val id = call.request.queryParameters["id"]?.toLongOrNull()
?: throw RpcError("miss id")
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}")

View File

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

View File

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

View File

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

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

View File

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

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

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

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

View File

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

View File

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

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

View File

@ -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,6 +82,7 @@ fun SubscriptionManagePage() {
Text(
text = "共有${subItemList.size}条订阅,激活:${subItemList.count { it.enable }},禁用:${subItemList.count { !it.enable }}",
)
Row {
Image(painter = painterResource(R.drawable.ic_add),
contentDescription = "",
modifier = Modifier
@ -103,17 +91,55 @@ fun SubscriptionManagePage() {
}
.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,29 +147,32 @@ 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()) {
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 = "添加")
}
}
}
}
}
}

View File

@ -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,13 +192,26 @@ object Ext {
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit,
): Job {
return this.launch(context, start) {
) = 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"

View File

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

View File

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

View File

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

View File

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

View File

@ -14,6 +14,7 @@ buildscript {
}
}
// https://youtrack.jetbrains.com/issue/KT-33191/
tasks.register<Delete>("clean").configure {
delete(rootProject.buildDir)
}

View File

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

View File

@ -1,3 +0,0 @@
package li.songe.selector.expression
sealed class ExpressionName

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
package li.songe.selector.parser
package li.songe.selector_android.parser
open class GkdParser<T>(
val prefix: String = "",

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
package li.songe.selector.selector
package li.songe.selector_android.selector
/**
* 关系连接选择器

View File

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

View File

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

View File

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

View File

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

@ -0,0 +1 @@
/build

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
package li.songe.selector_core.parser
internal data class ParserResult<T>(val data: T, val length: Int = 0)

View File

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

View File

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

View File

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