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") @Suppress("UnstableApiUsage")
android { android {
namespace = "li.songe.gkd"
compileSdk = libs.versions.android.compileSdk.get().toInt() compileSdk = libs.versions.android.compileSdk.get().toInt()
buildToolsVersion = libs.versions.android.buildToolsVersion.get() buildToolsVersion = libs.versions.android.buildToolsVersion.get()
@ -35,7 +36,6 @@ android {
lint { lint {
disable.add("ModifierFactoryUnreferencedReceiver") disable.add("ModifierFactoryUnreferencedReceiver")
// baseline = file("lint-baseline.xml")
} }
signingConfigs { signingConfigs {
@ -85,6 +85,7 @@ android {
freeCompilerArgs = freeCompilerArgs + "-opt-in=kotlin.RequiresOptIn" freeCompilerArgs = freeCompilerArgs + "-opt-in=kotlin.RequiresOptIn"
} }
buildFeatures { buildFeatures {
buildConfig = true
compose = true compose = true
} }
composeOptions { composeOptions {
@ -108,12 +109,14 @@ android {
} }
dependencies { dependencies {
implementation(project(mapOf("path" to ":selector"))) implementation(project(mapOf("path" to ":selector_core")))
implementation(project(mapOf("path" to ":selector_android")))
implementation(project(mapOf("path" to ":router"))) implementation(project(mapOf("path" to ":router")))
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.localbroadcastmanager)
implementation(libs.compose.ui) implementation(libs.compose.ui)
implementation(libs.compose.material) implementation(libs.compose.material)
@ -146,7 +149,6 @@ dependencies {
implementation(libs.ktor.client.content.negotiation) implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.serialization.kotlinx.json) implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.google.material)
implementation(libs.google.accompanist.drawablepainter) implementation(libs.google.accompanist.drawablepainter)
implementation(libs.google.accompanist.placeholder.material) implementation(libs.google.accompanist.placeholder.material)

View File

@ -2,11 +2,11 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 1, "version": 1,
"identityHash": "5e3c352578a63c3fccbb5e3fba31c89d", "identityHash": "2083d8585fffd897fde3733958e356f8",
"entities": [ "entities": [
{ {
"tableName": "subs_item", "tableName": "subs_item",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `comment` TEXT NOT NULL, `update_url` TEXT NOT NULL, `file_path` TEXT NOT NULL, `index` INTEGER NOT NULL)", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `name` TEXT NOT NULL, `update_url` TEXT NOT NULL, `version` INTEGER NOT NULL, `file_path` TEXT NOT NULL, `index` INTEGER NOT NULL)",
"fields": [ "fields": [
{ {
"fieldPath": "id", "fieldPath": "id",
@ -33,8 +33,8 @@
"notNull": true "notNull": true
}, },
{ {
"fieldPath": "comment", "fieldPath": "name",
"columnName": "comment", "columnName": "name",
"affinity": "TEXT", "affinity": "TEXT",
"notNull": true "notNull": true
}, },
@ -44,6 +44,12 @@
"affinity": "TEXT", "affinity": "TEXT",
"notNull": true "notNull": true
}, },
{
"fieldPath": "version",
"columnName": "version",
"affinity": "INTEGER",
"notNull": true
},
{ {
"fieldPath": "filePath", "fieldPath": "filePath",
"columnName": "file_path", "columnName": "file_path",
@ -58,10 +64,10 @@
} }
], ],
"primaryKey": { "primaryKey": {
"autoGenerate": true,
"columnNames": [ "columnNames": [
"id" "id"
], ]
"autoGenerate": true
}, },
"indices": [ "indices": [
{ {
@ -136,10 +142,10 @@
} }
], ],
"primaryKey": { "primaryKey": {
"autoGenerate": true,
"columnNames": [ "columnNames": [
"id" "id"
], ]
"autoGenerate": true
}, },
"indices": [], "indices": [],
"foreignKeys": [] "foreignKeys": []
@ -148,7 +154,7 @@
"views": [], "views": [],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5e3c352578a63c3fccbb5e3fba31c89d')" "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2083d8585fffd897fde3733958e356f8')"
] ]
} }
} }

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"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools">
package="li.songe.gkd">
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
@ -19,6 +18,7 @@
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<application <application
android:name="li.songe.gkd.App" android:name="li.songe.gkd.App"
android:allowBackup="true" android:allowBackup="true"
@ -61,7 +61,7 @@
</activity> </activity>
<service <service
android:name="li.songe.gkd.accessibility.GkdAbService" android:name=".accessibility.GkdAbService"
android:exported="false" android:exported="false"
android:label="@string/accessibility_service_label" android:label="@string/accessibility_service_label"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"> android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">

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 context = this
MMKV.initialize(this) MMKV.initialize(this)
LogUtils.d(Storage.settings) LogUtils.d(Storage.settings)
if (!Storage.settings.enableConsoleLogOut){
LogUtils.d("关闭日志控制台输出")
}
LogUtils.getConfig().apply { LogUtils.getConfig().apply {
isLog2FileSwitch = true isLog2FileSwitch = true
saveDays = 30 saveDays = 30

View File

@ -6,7 +6,7 @@ import androidx.compose.runtime.CompositionLocalProvider
import com.blankj.utilcode.util.LogUtils import com.blankj.utilcode.util.LogUtils
import com.dylanc.activityresult.launcher.StartActivityLauncher import com.dylanc.activityresult.launcher.StartActivityLauncher
import li.songe.gkd.composition.CompositionActivity import li.songe.gkd.composition.CompositionActivity
import li.songe.gkd.composition.Hook.useLifeCycleLog import li.songe.gkd.composition.CompositionExt.useLifeCycleLog
import li.songe.gkd.ui.home.HomePage import li.songe.gkd.ui.home.HomePage
import li.songe.gkd.ui.theme.MainTheme import li.songe.gkd.ui.theme.MainTheme
import li.songe.gkd.util.Ext.LocalLauncher import li.songe.gkd.util.Ext.LocalLauncher

View File

@ -2,20 +2,28 @@ package li.songe.gkd.accessibility
import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityEvent
import com.blankj.utilcode.util.LogUtils import com.blankj.utilcode.util.LogUtils
import com.blankj.utilcode.util.NetworkUtils
import com.blankj.utilcode.util.ScreenUtils import com.blankj.utilcode.util.ScreenUtils
import com.blankj.utilcode.util.ServiceUtils import com.blankj.utilcode.util.ServiceUtils
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import li.songe.gkd.composition.CompositionAbService import li.songe.gkd.composition.CompositionAbService
import li.songe.gkd.composition.Hook.useLifeCycleLog import li.songe.gkd.composition.CompositionExt.useLifeCycleLog
import li.songe.gkd.composition.Hook.useScope import li.songe.gkd.composition.CompositionExt.useScope
import li.songe.gkd.data.RuleManager import li.songe.gkd.data.RuleManager
import li.songe.gkd.data.SubscriptionRaw
import li.songe.gkd.db.table.SubsItem
import li.songe.gkd.db.util.RoomX
import li.songe.gkd.debug.server.api.Node import li.songe.gkd.debug.server.api.Node
import li.songe.gkd.util.Ext.buildRuleManager import li.songe.gkd.util.Ext.buildRuleManager
import li.songe.gkd.util.Ext.getActivityIdByShizuku import li.songe.gkd.util.Ext.getActivityIdByShizuku
import li.songe.gkd.util.Ext.getSubsFileLastModified import li.songe.gkd.util.Ext.getSubsFileLastModified
import li.songe.gkd.util.Ext.launchWhile import li.songe.gkd.util.Ext.launchWhile
import li.songe.gkd.util.Singleton
import li.songe.gkd.util.Storage import li.songe.gkd.util.Storage
import li.songe.selector.GkdSelector import li.songe.selector_android.GkdSelector
import java.io.File
class GkdAbService : CompositionAbService({ class GkdAbService : CompositionAbService({
useLifeCycleLog() useLifeCycleLog()
@ -28,12 +36,14 @@ class GkdAbService : CompositionAbService({
onDestroy { service = null } onDestroy { service = null }
KeepAliveService.start(context) KeepAliveService.start(context)
onDestroy {
KeepAliveService.stop(context)
}
var serviceConnected = false var serviceConnected = false
onServiceConnected { serviceConnected = true } onServiceConnected { serviceConnected = true }
onInterrupt { serviceConnected = false } onInterrupt { serviceConnected = false }
onAccessibilityEvent { event -> onAccessibilityEvent { event ->
val activityId = event?.className?.toString() ?: return@onAccessibilityEvent val activityId = event?.className?.toString() ?: return@onAccessibilityEvent
val rootAppId = rootInActiveWindow?.packageName?.toString() ?: return@onAccessibilityEvent val rootAppId = rootInActiveWindow?.packageName?.toString() ?: return@onAccessibilityEvent
@ -105,6 +115,36 @@ class GkdAbService : CompositionAbService({
delay(200) delay(200)
} }
scope.launchWhile {
delay(5000)
RoomX.select<SubsItem>().map { subsItem ->
if (!NetworkUtils.isAvailable()) return@map
try {
val text = Singleton.client.get(subsItem.updateUrl).bodyAsText()
val subscriptionRaw = SubscriptionRaw.parse5(text)
if (subscriptionRaw.version <= subsItem.version) {
return@map
}
val newItem = subsItem.copy(
updateUrl = subscriptionRaw.updateUrl
?: subsItem.updateUrl,
name = subscriptionRaw.name,
mtime = System.currentTimeMillis()
)
RoomX.update(newItem)
File(newItem.filePath).writeText(
SubscriptionRaw.stringify(
subscriptionRaw
)
)
LogUtils.d("更新订阅文件:${subsItem.name}")
} catch (e: Exception) {
e.printStackTrace()
}
}
delay(30 * 60_000)
}
}) { }) {
private var nodeSnapshot = NodeSnapshot() private var nodeSnapshot = NodeSnapshot()
set(value) { set(value) {

View File

@ -5,7 +5,7 @@ import android.content.Intent
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import li.songe.gkd.App import li.songe.gkd.App
import li.songe.gkd.composition.CompositionService import li.songe.gkd.composition.CompositionService
import li.songe.gkd.composition.Hook.useScope import li.songe.gkd.composition.CompositionExt.useScope
import li.songe.gkd.util.Ext.createNotificationChannel import li.songe.gkd.util.Ext.createNotificationChannel
import li.songe.gkd.util.Ext.launchWhile import li.songe.gkd.util.Ext.launchWhile
@ -21,5 +21,9 @@ class KeepAliveService : CompositionService({
fun start(context: Context = App.context) { fun start(context: Context = App.context) {
context.startForegroundService(Intent(context, KeepAliveService::class.java)) context.startForegroundService(Intent(context, KeepAliveService::class.java))
} }
fun stop(context: Context = App.context) {
context.stopService(Intent(context, KeepAliveService::class.java))
}
} }
} }

View File

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

View File

@ -1,14 +1,15 @@
package li.songe.gkd.data package li.songe.gkd.data
import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo
import li.songe.selector.GkdSelector import li.songe.gkd.selector.querySelector
import li.songe.selector_core.Selector
data class Rule( data class Rule(
/** /**
* length>0 * length>0
*/ */
val matches: List<GkdSelector> = emptyList(), val matches: List<Selector> = emptyList(),
val excludeMatches: List<GkdSelector> = emptyList(), val excludeMatches: List<Selector> = emptyList(),
/** /**
* 任意一个元素是上次触发过的 * 任意一个元素是上次触发过的
*/ */
@ -34,11 +35,11 @@ data class Rule(
fun query(nodeInfo: AccessibilityNodeInfo?): AccessibilityNodeInfo? { fun query(nodeInfo: AccessibilityNodeInfo?): AccessibilityNodeInfo? {
if (nodeInfo == null) return null if (nodeInfo == null) return null
var target: AccessibilityNodeInfo? = null var target: AccessibilityNodeInfo? = null
for (gkd in matches) { for (selector in matches) {
target = gkd.collect(nodeInfo) ?: return null target = nodeInfo.querySelector(selector) ?: return null
} }
for (gkd in excludeMatches) { for (selector in excludeMatches) {
if (gkd.collect(nodeInfo) != null) return null if (nodeInfo.querySelector(selector) != null) return null
} }
return target return target
} }

View File

@ -1,6 +1,6 @@
package li.songe.gkd.data package li.songe.gkd.data
import li.songe.selector.GkdSelector import li.songe.selector_core.Selector
class RuleManager(vararg subscriptionRawArray: SubscriptionRaw) { class RuleManager(vararg subscriptionRawArray: SubscriptionRaw) {
@ -42,13 +42,14 @@ class RuleManager(vararg subscriptionRawArray: SubscriptionRaw) {
?: appRaw.excludeActivityIds ?: appRaw.excludeActivityIds
?: emptyList()).toSet() ?: emptyList()).toSet()
ruleGroupList.add( ruleGroupList.add(
Rule( Rule(
cd = cd, cd = cd,
index = count, index = count,
matches = ruleRaw.matches.map { GkdSelector.gkdSelectorParser(it) }, matches = ruleRaw.matches.map { Selector.parse(it) },
excludeMatches = ruleRaw.excludeMatches.map { excludeMatches = ruleRaw.excludeMatches.map {
GkdSelector.gkdSelectorParser( Selector.parse(
it it
) )
}, },

View File

@ -7,7 +7,7 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import li.songe.gkd.util.Singleton import li.songe.gkd.util.Singleton
import li.songe.selector.GkdSelector import li.songe.selector_android.GkdSelector
@Parcelize @Parcelize
@ -200,7 +200,7 @@ data class SubscriptionRaw(
fun stringify(source: SubscriptionRaw) = Singleton.json.encodeToString(source) fun stringify(source: SubscriptionRaw) = Singleton.json.encodeToString(source)
private fun parse(source: String): SubscriptionRaw { fun parse(source: String): SubscriptionRaw {
return jsonToSubscriptionRaw(Singleton.json.parseToJsonElement(source).jsonObject) return jsonToSubscriptionRaw(Singleton.json.parseToJsonElement(source).jsonObject)
} }

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 import java.io.File
@Database( @Database(
version = 3, version = 1,
entities = [SubsItem::class, SubsConfig::class], entities = [SubsItem::class, SubsConfig::class],
autoMigrations = [ autoMigrations = [
// AutoMigration(from = 1, to = 2), // AutoMigration(from = 1, to = 2),

View File

@ -5,7 +5,6 @@ import androidx.room.Insert
import androidx.room.RawQuery import androidx.room.RawQuery
import androidx.room.Update import androidx.room.Update
import androidx.sqlite.db.SupportSQLiteQuery import androidx.sqlite.db.SupportSQLiteQuery
import kotlinx.coroutines.flow.Flow
interface BaseDao<T : Any> { interface BaseDao<T : Any> {
@Insert @Insert

View File

@ -1,7 +1,7 @@
package li.songe.gkd.db package li.songe.gkd.db
interface BaseTable { interface BaseTable {
var id: Long val id: Long
var ctime: Long val ctime: Long
var mtime: Long val mtime: Long
} }

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( @Entity(
tableName = "subs_config", tableName = "subs_config",
// indices = [Index(value = ["url"], unique = true)]
) )
@Parcelize @Parcelize
data class SubsConfig( data class SubsConfig(
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") override var id: Long = 0, @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") override val id: Long = 0,
@ColumnInfo(name = "ctime") override var ctime: Long = System.currentTimeMillis(), @ColumnInfo(name = "ctime") override val ctime: Long = System.currentTimeMillis(),
@ColumnInfo(name = "mtime") override var mtime: Long = System.currentTimeMillis(), @ColumnInfo(name = "mtime") override val mtime: Long = System.currentTimeMillis(),
/** /**
* 0 - app * 0 - app
* 1 - group * 1 - group
* 2 - rule * 2 - rule
*/ */
@ColumnInfo(name = "type") var type: Int = 0, @ColumnInfo(name = "type") val type: Int = 0,
@ColumnInfo(name = "enable") var enable: Boolean = true, @ColumnInfo(name = "enable") val enable: Boolean = true,
@ColumnInfo(name = "subs_item_id") var subsItemId: Long = -1, @ColumnInfo(name = "subs_item_id") val subsItemId: Long = -1,
@ColumnInfo(name = "app_id") var appId: String = "", @ColumnInfo(name = "app_id") val appId: String = "",
@ColumnInfo(name = "group_key") var groupKey: Int = -1, @ColumnInfo(name = "group_key") val groupKey: Int = -1,
@ColumnInfo(name = "rule_key") var ruleKey: Int = -1, @ColumnInfo(name = "rule_key") val ruleKey: Int = -1,
) : BaseTable, Parcelable { ) : BaseTable, Parcelable {
companion object { companion object {

View File

@ -19,30 +19,36 @@ data class SubsItem(
/** /**
* 当主键是0时,autoGenerate将覆盖此字段,插入数据库后 需要用返回值手动更新此字段 * 当主键是0时,autoGenerate将覆盖此字段,插入数据库后 需要用返回值手动更新此字段
*/ */
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") override var id: Long = 0, @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") override val id: Long = 0,
@ColumnInfo(name = "ctime") override var ctime: Long = System.currentTimeMillis(), @ColumnInfo(name = "ctime") override val ctime: Long = System.currentTimeMillis(),
@ColumnInfo(name = "mtime") override var mtime: Long = System.currentTimeMillis(), @ColumnInfo(name = "mtime") override val mtime: Long = System.currentTimeMillis(),
@ColumnInfo(name = "enable") var enable: Boolean = true, @ColumnInfo(name = "enable") val enable: Boolean = true,
/** /**
* 用户自定义备注 * 订阅文件 name 属性
*/ */
@ColumnInfo(name = "name") var name: String = "", @ColumnInfo(name = "name") val name: String = "",
/** /**
* 订阅文件下载地址,也是更新链接 * 订阅文件下载地址,也是更新链接
*/ */
@ColumnInfo(name = "update_url") var updateUrl: String, @ColumnInfo(name = "update_url") val updateUrl: String = "",
/**
* 订阅文件下载地址,也是更新链接
*/
@ColumnInfo(name = "version") val version: Int = 0,
/** /**
* 订阅文件下载后存放的路径 * 订阅文件下载后存放的路径
*/ */
@ColumnInfo(name = "file_path") var filePath: String, @ColumnInfo(name = "file_path") val filePath: String = "",
/** /**
* 顺序 * 顺序
*/ */
@ColumnInfo(name = "index") var index: Int=0, @ColumnInfo(name = "index") val index: Int = 0,
) : Parcelable, BaseTable { ) : Parcelable, BaseTable {

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, operator: String,
) = ) =
Expression( Expression(
RoomAnnotation.getColumnName(T::class.java.name, name), RoomAnnotation.getColumnName(T::class, name),
operator, operator,
value, value,
T::class T::class
) )
inline infix fun <reified T : Any, reified V> KMutableProperty1<T, V>.eq(value: V) = inline infix fun <reified T : Any, reified V> KProperty1<T, V>.eq(value: V) =
baseOperator(value, "==") baseOperator(value, "==")
inline infix fun <reified T : Any, reified V> KMutableProperty1<T, V>.neq(value: V) = inline infix fun <reified T : Any, reified V> KProperty1<T, V>.neq(value: V) =
baseOperator(value, "!=") baseOperator(value, "!=")
inline infix fun <reified T : Any, reified V> KMutableProperty1<T, V>.less(value: V) = inline infix fun <reified T : Any, reified V> KProperty1<T, V>.less(value: V) =
baseOperator(value, "<") baseOperator(value, "<")
inline infix fun <reified T : Any, reified V> KMutableProperty1<T, V>.lessEq(value: V) = inline infix fun <reified T : Any, reified V> KProperty1<T, V>.lessEq(value: V) =
baseOperator(value, "<=") baseOperator(value, "<=")
inline infix fun <reified T : Any, reified V> KMutableProperty1<T, V>.greater(value: V) = inline infix fun <reified T : Any, reified V> KProperty1<T, V>.greater(value: V) =
baseOperator(value, ">") baseOperator(value, ">")
inline infix fun <reified T : Any, reified V> KMutableProperty1<T, V>.greaterEq(value: V) = inline infix fun <reified T : Any, reified V> KProperty1<T, V>.greaterEq(value: V) =
baseOperator(value, ">=") baseOperator(value, ">=")
inline infix fun <reified T : Any, reified V> KMutableProperty1<T, V>.inList(value: List<V>) = inline infix fun <reified T : Any, reified V> KProperty1<T, V>.inList(value: List<V>) =
baseOperator(value, "IN") baseOperator(value, "IN")
inline infix fun <reified T : Any, reified V> KMutableProperty1<T, V>.glob(value: GlobString) = inline infix fun <reified T : Any, reified V> KProperty1<T, V>.glob(value: GlobString) =
baseOperator(value, "GLOB") baseOperator(value, "GLOB")
inline infix fun <reified T : Any, reified V> KMutableProperty1<T, V>.like(value: LikeString) = inline infix fun <reified T : Any, reified V> KProperty1<T, V>.like(value: LikeString) =
baseOperator(value, "LIKE") baseOperator(value, "LIKE")
inline fun <reified T : Any, V, V2> KProperty1<T, V>.baseOperator( inline fun <reified T : Any, V, V2> KProperty1<T, V>.baseOperator(
@ -56,7 +56,7 @@ object Operator {
operator: String, operator: String,
) = ) =
Expression( Expression(
RoomAnnotation.getColumnName(T::class.java.name, name), RoomAnnotation.getColumnName(T::class, name),
operator, operator,
value, value,
T::class T::class

View File

@ -1,42 +1,56 @@
package li.songe.gkd.db.util package li.songe.gkd.db.util
import java.lang.Exception import li.songe.gkd.db.table.SubsConfig
import li.songe.gkd.db.table.SubsItem
import li.songe.gkd.db.table.TriggerLog
import kotlin.reflect.KClass
object RoomAnnotation { object RoomAnnotation {
fun getTableName(className: String): String = when (className) {
"li.songe.gkd.db.table.SubsConfig" -> "subs_config" fun getTableName(cls: KClass<*>): String = when (cls) {
"li.songe.gkd.db.table.SubsItem" -> "subs_item" SubsConfig::class -> "subs_config"
"r-1682430013322" -> "avoid_compile_error" SubsItem::class -> "subs_item"
else -> throw Exception("""not found className : $className""") TriggerLog::class -> "trigger_log"
else -> throw Exception("""not found className : ${cls.qualifiedName}""")
} }
fun getColumnName(className: String, propertyName: String): String = when (className) { fun getColumnName(cls: KClass<*>, propertyName: String): String = when (cls) {
"li.songe.gkd.db.table.SubsConfig" -> when (propertyName) { SubsConfig::class -> when (propertyName) {
"id" -> "id" SubsConfig::id.name -> "id"
"ctime" -> "ctime" SubsConfig::ctime.name -> "ctime"
"mtime" -> "mtime" SubsConfig::mtime.name -> "mtime"
"type" -> "type" SubsConfig::type.name -> "type"
"enable" -> "enable" SubsConfig::enable.name -> "enable"
"subsItemId" -> "subs_item_id" SubsConfig::subsItemId.name -> "subs_item_id"
"appId" -> "app_id" SubsConfig::appId.name -> "app_id"
"groupKey" -> "group_key" SubsConfig::groupKey.name -> "group_key"
"ruleKey" -> "rule_key" SubsConfig::ruleKey.name -> "rule_key"
"r-1682430013322" -> "avoid_compile_error" else -> error("""not found columnName : ${cls.qualifiedName}#$propertyName""")
else -> throw Exception("""not found columnName : $className#$propertyName""")
} }
"li.songe.gkd.db.table.SubsItem" -> when (propertyName) {
"id" -> "id" SubsItem::class -> when (propertyName) {
"ctime" -> "ctime" SubsItem::id.name -> "id"
"mtime" -> "mtime" SubsItem::ctime.name -> "ctime"
"enable" -> "enable" SubsItem::mtime.name -> "mtime"
"name" -> "name" SubsItem::enable.name -> "enable"
"updateUrl" -> "update_url" SubsItem::name.name -> "name"
"filePath" -> "file_path" SubsItem::updateUrl.name -> "update_url"
"index" -> "index" SubsItem::filePath.name -> "file_path"
"r-1682430013322" -> "avoid_compile_error" SubsItem::index.name -> "index"
else -> throw Exception("""not found columnName : $className#$propertyName""") else -> error("""not found columnName : ${cls.qualifiedName}#$propertyName""")
} }
"r-1682430013322" -> "avoid_compile_error"
else -> throw Exception("""not found className : $className""") TriggerLog::class -> when (propertyName) {
TriggerLog::id.name -> "id"
TriggerLog::ctime.name -> "ctime"
TriggerLog::mtime.name -> "mtime"
TriggerLog::appId.name -> "app_id"
TriggerLog::activityId.name -> "activity_id"
TriggerLog::selector.name -> "selector"
else -> error("""not found columnName : ${cls.qualifiedName}#$propertyName""")
} }
else -> error("""not found className : ${cls.qualifiedName}""")
}
} }

View File

@ -3,7 +3,7 @@ package li.songe.gkd.db.util
import androidx.sqlite.db.SimpleSQLiteQuery import androidx.sqlite.db.SimpleSQLiteQuery
import li.songe.gkd.db.AppDatabase.Companion.db import li.songe.gkd.db.AppDatabase.Companion.db
import li.songe.gkd.db.BaseDao import li.songe.gkd.db.BaseDao
import li.songe.gkd.db.BaseTable import li.songe.gkd.db.LogDatabase.Companion.logDb
import li.songe.gkd.db.table.* import li.songe.gkd.db.table.*
import kotlin.reflect.KClass import kotlin.reflect.KClass
@ -13,37 +13,13 @@ object RoomX {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun <T : Any> getBaseDao(cls: KClass<T>) = when (cls) { fun <T : Any> getBaseDao(cls: KClass<T>) = when (cls) {
SubsItem::class -> db.subsItemRoomDao() SubsItem::class -> db.subsItemRoomDao()
// SubsAppItem::class -> db.subsAppItemRoomDao()
// SubsGroupItem::class -> db.subsGroupItemRoomDao()
// SubsRuleItem::class -> db.subsRuleItemRoomDao()
SubsConfig::class -> db.subsConfigRoomDao() SubsConfig::class -> db.subsConfigRoomDao()
else -> throw Exception("not found class dao : ${cls::class.java.name}") TriggerLog::class -> logDb.triggerLogRoomDao()
else -> error("not found class dao : ${cls::class.java.name}")
} as BaseDao<T> } as BaseDao<T>
fun databaseBeforeHook(vararg objects: Any) {
objects.forEach { /**/ when (it) {
is BaseTable -> {
it.mtime = System.currentTimeMillis()
}
else -> throw Exception("not found table class hook : ${it::class.java.name}")
}
}
}
fun databaseInsertAfterHook(objects: Array<out Any>, idList: List<Long>) {
objects.forEachIndexed { index, any -> /**/ when (any) {
is BaseTable -> {
// 插入数据后更新实体类的id
any.id = idList[index]
}
else -> throw Exception("not found table class hook : ${any::class.java.name}")
}
}
}
suspend inline fun <reified T : Any> update(vararg objects: T): Int { suspend inline fun <reified T : Any> update(vararg objects: T): Int {
databaseBeforeHook(*objects)
return getBaseDao(T::class).update(*objects) return getBaseDao(T::class).update(*objects)
} }
@ -51,10 +27,7 @@ object RoomX {
* 插入成功后, 自动改变入参对象的 id * 插入成功后, 自动改变入参对象的 id
*/ */
suspend inline fun <reified T : Any> insert(vararg objects: T): List<Long> { suspend inline fun <reified T : Any> insert(vararg objects: T): List<Long> {
databaseBeforeHook(*objects) return getBaseDao(T::class).insert(*objects)
return getBaseDao(T::class).insert(*objects).apply {
databaseInsertAfterHook(objects, this)
}
} }
suspend inline fun <reified T : Any> delete(vararg objects: T) = suspend inline fun <reified T : Any> delete(vararg objects: T) =
@ -66,7 +39,7 @@ object RoomX {
noinline block: (() -> Expression<*, *, T>)? = null noinline block: (() -> Expression<*, *, T>)? = null
): List<T> { ): List<T> {
val expression = block?.invoke() val expression = block?.invoke()
val tableName = RoomAnnotation.getTableName(T::class.java.name) val tableName = RoomAnnotation.getTableName(T::class)
val sqlString = "SELECT * FROM $tableName" + (if (expression != null) { val sqlString = "SELECT * FROM $tableName" + (if (expression != null) {
" WHERE ${expression.stringify()}" " WHERE ${expression.stringify()}"
} else { } else {
@ -90,7 +63,7 @@ object RoomX {
noinline block: (() -> Expression<*, *, T>)? = null noinline block: (() -> Expression<*, *, T>)? = null
): List<Int> { ): List<Int> {
val expression = block?.invoke() val expression = block?.invoke()
val tableName = RoomAnnotation.getTableName(T::class.java.name) val tableName = RoomAnnotation.getTableName(T::class)
val sqlString = "DELETE FROM $tableName" + (if (expression != null) { val sqlString = "DELETE FROM $tableName" + (if (expression != null) {
" WHERE ${expression.stringify()}" " WHERE ${expression.stringify()}"
} else { } else {
@ -107,42 +80,5 @@ object RoomX {
val baseDao = getBaseDao(T::class) val baseDao = getBaseDao(T::class)
return baseDao.delete(SimpleSQLiteQuery(sqlString)) return baseDao.delete(SimpleSQLiteQuery(sqlString))
} }
// inline fun <reified T : Any> selectFlow(
// limit: Int? = null,
// offset: Int? = null,
// noinline block: (() -> Expression<*, *, T>)? = null
// ): Flow<List<T>> {
// val expression = block?.invoke()
// val tableName = RoomAnnotation.getTableName(T::class.java.name)
// val sqlString = "SELECT * FROM $tableName" + (if (expression != null) {
// " WHERE ${expression.stringify()}"
// } else {
// ""
// }) + (if (limit != null) {
// " LIMIT $limit"
// } else {
// ""
// }) + (if (offset != null) {
// " OFFSET $offset"
// } else {
// ""
// })
// val baseDao = getBaseDao(T::class)
// return baseDao.queryFlow(SimpleSQLiteQuery(sqlString))
// }
// fun testExample() = runBlocking {
// select { SubsItem::filePath like likeString().any(".json") }.forEach {
// LogUtils.d(it)
// }
//
// selectFlow { SubsItem::description like likeString().any(".json") }.distinctUntilChanged()
// .collect {
// LogUtils.d(it.firstOrNull())
// }
// }
} }

View File

@ -9,7 +9,7 @@ import com.torrydo.floatingbubbleview.FloatingBubble
import li.songe.gkd.App import li.songe.gkd.App
import li.songe.gkd.R import li.songe.gkd.R
import li.songe.gkd.composition.CompositionFbService import li.songe.gkd.composition.CompositionFbService
import li.songe.gkd.composition.Hook.useMessage import li.songe.gkd.composition.CompositionExt.useMessage
import li.songe.gkd.composition.InvokeMessage import li.songe.gkd.composition.InvokeMessage
import li.songe.gkd.debug.server.HttpService import li.songe.gkd.debug.server.HttpService

View File

@ -31,13 +31,14 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import li.songe.gkd.App import li.songe.gkd.App
import li.songe.gkd.composition.CompositionService import li.songe.gkd.composition.CompositionService
import li.songe.gkd.composition.Hook.useMessage import li.songe.gkd.composition.CompositionExt.useMessage
import li.songe.gkd.composition.InvokeMessage import li.songe.gkd.composition.InvokeMessage
import li.songe.gkd.debug.Ext.captureSnapshot import li.songe.gkd.debug.Ext.captureSnapshot
import li.songe.gkd.debug.Ext.screenshotDir import li.songe.gkd.debug.Ext.screenshotDir
import li.songe.gkd.debug.Ext.snapshotDir import li.songe.gkd.debug.Ext.snapshotDir
import li.songe.gkd.debug.Ext.windowDir import li.songe.gkd.debug.Ext.windowDir
import li.songe.gkd.debug.FloatingService import li.songe.gkd.debug.FloatingService
import li.songe.gkd.debug.server.api.Device
import li.songe.gkd.util.Ext.getIpAddressInLocalNetwork import li.songe.gkd.util.Ext.getIpAddressInLocalNetwork
import li.songe.gkd.util.Singleton import li.songe.gkd.util.Singleton
import li.songe.gkd.util.Storage import li.songe.gkd.util.Storage
@ -78,24 +79,26 @@ class HttpService : CompositionService({
routing { routing {
route("/api/rpc") { route("/api/rpc") {
get("/device") {
call.respond(Device.singleton)
}
get("/capture") { get("/capture") {
removeBubbles() removeBubbles()
delay(200) delay(200)
try { try {
call.respond(captureSnapshot()) call.respond(captureSnapshot())
} catch (e: Exception) { } catch (e: Exception) {
showBubbles()
throw e throw e
} } finally {
showBubbles() showBubbles()
} }
}
get("/snapshot") { get("/snapshot") {
val id = call.request.queryParameters["id"]?.toLongOrNull() val id = call.request.queryParameters["id"]?.toLongOrNull()
?: throw RpcError("miss id") ?: throw RpcError("miss id")
call.response.cacheControl(CacheControl.MaxAge(3600)) call.response.cacheControl(CacheControl.MaxAge(3600))
call.respondFile(snapshotDir, "/${id}.json") call.respondFile(snapshotDir, "/${id}.json")
} }
get("/window") { get("/window") {
val id = call.request.queryParameters["id"]?.toLongOrNull() val id = call.request.queryParameters["id"]?.toLongOrNull()
?: throw RpcError("miss id") ?: throw RpcError("miss id")
@ -110,7 +113,6 @@ class HttpService : CompositionService({
call.respondFile(screenshotDir, "/${id}.png") call.respondFile(screenshotDir, "/${id}.png")
} }
} }
listOf("/", "/index.html").forEach { p -> listOf("/", "/index.html").forEach { p ->
get(p) { get(p) {
val response = Singleton.client.get("$proxyUrl${call.request.uri}") val response = Singleton.client.get("$proxyUrl${call.request.uri}")

View File

@ -8,7 +8,7 @@ data class RpcError(
val code: Int = 0, val code: Int = 0,
) : Exception(message) { ) : Exception(message) {
companion object { companion object {
const val HeaderKey = "X-Rpc-Result" const val HeaderKey = "X_Rpc_Result"
const val HeaderOkValue = "ok" const val HeaderOkValue = "ok"
const val HeaderErrorValue = "error" const val HeaderErrorValue = "error"
} }

View File

@ -11,23 +11,32 @@ import io.ktor.server.response.respond
val RpcErrorHeaderPlugin = createApplicationPlugin(name = "RpcErrorHeaderPlugin") { val RpcErrorHeaderPlugin = createApplicationPlugin(name = "RpcErrorHeaderPlugin") {
onCall { call -> onCall { call ->
Log.d("Ktor", "Request Path: ${call.request.uri}") Log.d("Ktor", "onCall: ${call.request.uri}")
} }
on(CallFailed) { call, cause -> on(CallFailed) { call, cause ->
if (cause is RpcError) { when (cause) {
is RpcError -> {
// 主动抛出的错误 // 主动抛出的错误
LogUtils.d(call.request.uri, cause.code, cause.message) LogUtils.d(call.request.uri, cause.code, cause.message)
call.response.header(RpcError.HeaderKey, RpcError.HeaderErrorValue) call.response.header(RpcError.HeaderKey, RpcError.HeaderErrorValue)
call.respond(cause) call.respond(cause)
} else if (cause is Exception) { }
is Exception -> {
// 未知错误 // 未知错误
LogUtils.d(call.request.uri, cause.message) LogUtils.d(call.request.uri, cause.message)
cause.printStackTrace() cause.printStackTrace()
call.respond(HttpStatusCode.InternalServerError, cause) call.respond(HttpStatusCode.InternalServerError, cause)
} }
else -> {
cause.printStackTrace()
}
}
} }
onCallRespond { call, _ -> onCallRespond { call, _ ->
if (call.response.status() == HttpStatusCode.OK && val status=call.response.status() ?: HttpStatusCode.OK
if (status == HttpStatusCode.OK &&
!call.response.headers.contains( !call.response.headers.contains(
RpcError.HeaderKey RpcError.HeaderKey
) )

View File

@ -6,12 +6,12 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
data class Attr( data class Attr(
val id: String?, val id: String? = null,
val className: String?, val className: String? = null,
val childCount: Int, val childCount: Int = 0,
val text: String?, val text: String? = null,
val isClickable: Boolean, val isClickable: Boolean = false,
val desc: String?, val desc: String? = null,
val left: Int, val left: Int,
val top: Int, val top: Int,
val right: Int, val right: Int,

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 android.view.accessibility.AccessibilityNodeInfo
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import li.songe.selector.forEach import li.songe.selector_android.forEach
import java.util.ArrayDeque import java.util.ArrayDeque

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.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -44,10 +43,10 @@ val DebugPage = Page {
val launcher = LocalLauncher.current val launcher = LocalLauncher.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var httpServerRunning by usePollState { HttpService.isRunning() } val httpServerRunning by usePollState { HttpService.isRunning() }
var screenshotRunning by usePollState { ScreenshotService.isRunning() } val screenshotRunning by usePollState { ScreenshotService.isRunning() }
var gkdAccessRunning by usePollState { GkdAbService.isRunning() } val gkdAccessRunning by usePollState { GkdAbService.isRunning() }
var floatingRunning by usePollState { val floatingRunning by usePollState {
FloatingService.isRunning() && Settings.canDrawOverlays( FloatingService.isRunning() && Settings.canDrawOverlays(
context context
) )
@ -55,7 +54,7 @@ val DebugPage = Page {
val debugAvailable by remember { val debugAvailable by remember {
derivedStateOf { httpServerRunning && screenshotRunning && gkdAccessRunning } derivedStateOf { httpServerRunning }
} }
val serverUrl by remember { val serverUrl by remember {
@ -132,8 +131,8 @@ val DebugPage = Page {
launcher.launch(intent) { resultCode, _ -> launcher.launch(intent) { resultCode, _ ->
if (resultCode != ComponentActivity.RESULT_OK) return@launch if (resultCode != ComponentActivity.RESULT_OK) return@launch
if (!Settings.canDrawOverlays(context)) return@launch if (!Settings.canDrawOverlays(context)) return@launch
val intent = Intent(context, FloatingService::class.java) val intent1 = Intent(context, FloatingService::class.java)
ContextCompat.startForegroundService(context, intent) ContextCompat.startForegroundService(context, intent1)
} }
} }
} else { } else {

View File

@ -11,6 +11,9 @@ import androidx.compose.foundation.layout.width
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
@ -20,31 +23,34 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import li.songe.gkd.R import li.songe.gkd.R
import li.songe.gkd.db.table.SubsItem import li.songe.gkd.db.table.SubsItem
import li.songe.gkd.util.Singleton
@Composable @Composable
fun SubsItemCard( fun SubsItemCard(
data: SubsItem, subsItem: SubsItem,
onShareClick: (() -> Unit)? = null, onShareClick: (() -> Unit)? = null,
onEditClick: (() -> Unit)? = null, onEditClick: (() -> Unit)? = null,
onDelClick: (() -> Unit)? = null, onDelClick: (() -> Unit)? = null,
onRefreshClick: (() -> Unit)? = null, onRefreshClick: (() -> Unit)? = null,
) { ) {
val dateStr by remember(subsItem) {
derivedStateOf { "更新于:" + Singleton.simpleDateFormat.format(subsItem.mtime) }
}
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier
.padding(8.dp) .padding(8.dp)
.alpha(if (data.enable) 1f else .3f), .alpha(if (subsItem.enable) 1f else .3f),
) { ) {
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text( Text(
text = data.name, text = subsItem.name,
maxLines = 1, maxLines = 1,
softWrap = false, softWrap = false,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
Text( Text(
text = data.updateUrl, text = dateStr,
maxLines = 1, maxLines = 1,
softWrap = false, softWrap = false,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis

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 package li.songe.gkd.ui.home
import android.app.Activity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
@ -10,17 +8,20 @@ import androidx.compose.material.AlertDialog
import androidx.compose.material.Button import androidx.compose.material.Button
import androidx.compose.material.Card import androidx.compose.material.Card
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import com.blankj.utilcode.util.ClipboardUtils
import com.blankj.utilcode.util.PathUtils
import com.blankj.utilcode.util.ToastUtils import com.blankj.utilcode.util.ToastUtils
import com.journeyapps.barcodescanner.ScanContract import com.google.zxing.BarcodeFormat
import com.journeyapps.barcodescanner.ScanOptions
import io.ktor.client.request.* import io.ktor.client.request.*
import io.ktor.client.statement.* import io.ktor.client.statement.*
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
@ -32,8 +33,10 @@ import li.songe.gkd.db.table.SubsConfig
import li.songe.gkd.db.table.SubsItem import li.songe.gkd.db.table.SubsItem
import li.songe.gkd.db.util.Operator.eq import li.songe.gkd.db.util.Operator.eq
import li.songe.gkd.db.util.RoomX import li.songe.gkd.db.util.RoomX
import li.songe.gkd.hooks.useNavigateForQrcodeResult
import li.songe.gkd.ui.SubsPage import li.songe.gkd.ui.SubsPage
import li.songe.gkd.ui.component.SubsItemCard import li.songe.gkd.ui.component.SubsItemCard
import li.songe.gkd.util.Ext.launchTry
import li.songe.gkd.util.Singleton import li.songe.gkd.util.Singleton
import li.songe.gkd.util.ThrottleState import li.songe.gkd.util.ThrottleState
import li.songe.router.LocalRouter import li.songe.router.LocalRouter
@ -42,40 +45,24 @@ import java.io.File
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun SubscriptionManagePage() { fun SubscriptionManagePage() {
// https://medium.com/androiddevelopers/multiple-back-stacks-b714d974f134
// https://medium.com/androiddevelopers/animations-in-navigation-compose-36d48870776b
// https://tigeroakes.com/posts/react-to-compose-dictionary/#storybook--preview
// https://google.github.io/accompanist/
// https://foso.github.io/Jetpack-Compose-Playground/
// https://www.jetpackcompose.net/
// https://jetpackcompose.cn/docs/
// https://developer.android.com/jetpack/compose/performance
val context = LocalContext.current as Activity
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val router = LocalRouter.current val router = LocalRouter.current
var subItemList by remember { mutableStateOf(listOf<SubsItem>()) } var subItemList by remember { mutableStateOf(listOf<SubsItem>()) }
var shareSubItem: SubsItem? by remember { mutableStateOf(null) } var shareSubItem: SubsItem? by remember { mutableStateOf(null) }
var shareQrcode: ImageBitmap? by remember { mutableStateOf(null) }
var deleteSubItem: SubsItem? by remember { mutableStateOf(null) } var deleteSubItem: SubsItem? by remember { mutableStateOf(null) }
val scanLauncher =
rememberLauncherForActivityResult(contract = ScanContract(), onResult = { result ->
if (result.contents != null) {
// scope.launch {
// val newSubsItem = router.navigateForResult(
// SubsItemInsertPage, SubsItem(filePath = "", updateUrl = result.contents)
// ) ?: return@launch
// subItemList = subItemList.toMutableList().apply { add(newSubsItem) }
// }
}
})
var showAddDialog by remember { mutableStateOf(false) } var showAddDialog by remember { mutableStateOf(false) }
var showLinkInputDialog by remember { mutableStateOf(false) }
val viewSubItemThrottle = ThrottleState.use(scope) val viewSubItemThrottle = ThrottleState.use(scope)
val editSubItemThrottle = ThrottleState.use(scope) val editSubItemThrottle = ThrottleState.use(scope)
val refreshSubItemThrottle = ThrottleState.use(scope, 250) val refreshSubItemThrottle = ThrottleState.use(scope, 250)
val navigateForQrcodeResult = useNavigateForQrcodeResult()
var linkText by remember {
mutableStateOf("")
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
subItemList = RoomX.select<SubsItem>().sortedBy { it.index } subItemList = RoomX.select<SubsItem>().sortedBy { it.index }
@ -95,6 +82,7 @@ fun SubscriptionManagePage() {
Text( Text(
text = "共有${subItemList.size}条订阅,激活:${subItemList.count { it.enable }},禁用:${subItemList.count { !it.enable }}", text = "共有${subItemList.size}条订阅,激活:${subItemList.count { it.enable }},禁用:${subItemList.count { !it.enable }}",
) )
Row {
Image(painter = painterResource(R.drawable.ic_add), Image(painter = painterResource(R.drawable.ic_add),
contentDescription = "", contentDescription = "",
modifier = Modifier modifier = Modifier
@ -103,17 +91,55 @@ fun SubscriptionManagePage() {
} }
.padding(4.dp) .padding(4.dp)
.size(25.dp)) .size(25.dp))
Image(painter = painterResource(R.drawable.ic_refresh),
contentDescription = "",
modifier = Modifier
.clickable {
scope.launchTry {
subItemList.mapIndexed { i, oldItem ->
val subscriptionRaw = SubscriptionRaw.parse5(
Singleton.client
.get(oldItem.updateUrl)
.bodyAsText()
)
if (subscriptionRaw.version <= oldItem.version) {
ToastUtils.showShort("暂无更新:${oldItem.name}")
return@mapIndexed
}
val newItem = oldItem.copy(
updateUrl = subscriptionRaw.updateUrl
?: oldItem.updateUrl,
name = subscriptionRaw.name,
mtime = System.currentTimeMillis(),
version = subscriptionRaw.version
)
RoomX.update(newItem)
File(newItem.filePath).writeText(
SubscriptionRaw.stringify(
subscriptionRaw
)
)
ToastUtils.showShort("更新成功:${newItem.name}")
subItemList = subItemList
.toMutableList()
.also {
it[i] = newItem
}
}
}
}
.padding(4.dp)
.size(25.dp))
}
} }
} }
items(subItemList.size, { i -> subItemList[i].hashCode() }) { i -> items(subItemList.size) { i ->
Card( Card(
modifier = Modifier modifier = Modifier
.animateItemPlacement() .animateItemPlacement()
.padding(vertical = 3.dp, horizontal = 8.dp) .padding(vertical = 3.dp, horizontal = 8.dp)
.clickable(onClick = viewSubItemThrottle.invoke { .clickable(onClick = { router.navigate(SubsPage, subItemList[i]) }),
router.navigate(SubsPage, subItemList[i])
}),
elevation = 0.dp, elevation = 0.dp,
border = BorderStroke(1.dp, Color(0xfff6f6f6)), border = BorderStroke(1.dp, Color(0xfff6f6f6)),
shape = RoundedCornerShape(8.dp), shape = RoundedCornerShape(8.dp),
@ -121,29 +147,32 @@ fun SubscriptionManagePage() {
SubsItemCard(subItemList[i], onShareClick = { SubsItemCard(subItemList[i], onShareClick = {
shareSubItem = subItemList[i] shareSubItem = subItemList[i]
}, onEditClick = editSubItemThrottle.invoke { }, onEditClick = editSubItemThrottle.invoke {
// val newSubsItem =
// router.navigateForResult(SubsItemUpdatePage, subItemList[i])
// ?: return@invoke
// subItemList = subItemList.toMutableList().apply {
// set(i, newSubsItem)
// }
}, onDelClick = { }, onDelClick = {
deleteSubItem = subItemList[i] deleteSubItem = subItemList[i]
}, onRefreshClick = refreshSubItemThrottle.invoke { }, onRefreshClick = refreshSubItemThrottle.invoke {
val oldItem = subItemList[i]
val subscriptionRaw = SubscriptionRaw.parse5( val subscriptionRaw = SubscriptionRaw.parse5(
Singleton.client.get(subItemList[i].updateUrl).bodyAsText() Singleton.client.get(oldItem.updateUrl).bodyAsText()
) )
subItemList = subItemList.toMutableList().also { if (subscriptionRaw.version <= oldItem.version) {
it[i] = it[i].copy( ToastUtils.showShort("暂无更新:${oldItem.name}")
updateUrl = subscriptionRaw.updateUrl return@invoke
?: subItemList[i].updateUrl,
name = subscriptionRaw.name
)
RoomX.update(it[i])
val f = File(it[i].filePath)
f.writeText(SubscriptionRaw.stringify(subscriptionRaw))
} }
ToastUtils.showShort("更新成功") val newItem = oldItem.copy(
updateUrl = subscriptionRaw.updateUrl
?: oldItem.updateUrl,
name = subscriptionRaw.name,
mtime = System.currentTimeMillis(),
version = subscriptionRaw.version
)
RoomX.update(newItem)
withContext(IO) {
File(newItem.filePath).writeText(SubscriptionRaw.stringify(subscriptionRaw))
}
subItemList = subItemList.toMutableList().also {
it[i] = newItem
}
ToastUtils.showShort("更新成功:${newItem.name}")
}.catch { }.catch {
if (!it.message.isNullOrEmpty()) { if (!it.message.isNullOrEmpty()) {
ToastUtils.showShort(it.message) ToastUtils.showShort(it.message)
@ -153,7 +182,7 @@ fun SubscriptionManagePage() {
} }
} }
if (shareSubItem != null) { shareSubItem?.let { _shareSubItem ->
Dialog(onDismissRequest = { shareSubItem = null }) { Dialog(onDismissRequest = { shareSubItem = null }) {
Box( Box(
Modifier Modifier
@ -164,12 +193,25 @@ fun SubscriptionManagePage() {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(text = "二维码", Text(text = "二维码",
modifier = Modifier modifier = Modifier
.clickable { } .clickable {
shareQrcode = Singleton.barcodeEncoder
.encodeBitmap(
_shareSubItem.updateUrl,
BarcodeFormat.QR_CODE,
500,
500
)
.asImageBitmap()
shareSubItem = null
}
.fillMaxWidth() .fillMaxWidth()
.padding(8.dp)) .padding(8.dp))
Text(text = "导出至剪切板", Text(text = "导出至剪切板",
modifier = Modifier modifier = Modifier
.clickable { } .clickable {
ClipboardUtils.copyText(_shareSubItem.updateUrl)
shareSubItem = null
}
.fillMaxWidth() .fillMaxWidth()
.padding(8.dp)) .padding(8.dp))
} }
@ -177,6 +219,16 @@ fun SubscriptionManagePage() {
} }
} }
shareQrcode?.let { _shareQrcode ->
Dialog(onDismissRequest = { shareQrcode = null }) {
Image(
bitmap = _shareQrcode,
contentDescription = "qrcode",
modifier = Modifier.size(400.dp)
)
}
}
val delSubItemThrottle = ThrottleState.use(scope) val delSubItemThrottle = ThrottleState.use(scope)
if (deleteSubItem != null) { if (deleteSubItem != null) {
@ -198,7 +250,6 @@ fun SubscriptionManagePage() {
} }
subItemList = subItemList.toMutableList().also { it.remove(deleteSubItem) } subItemList = subItemList.toMutableList().also { it.remove(deleteSubItem) }
deleteSubItem = null deleteSubItem = null
}) { }) {
Text("") Text("")
} }
@ -213,7 +264,7 @@ fun SubscriptionManagePage() {
} }
if (showAddDialog) { if (showAddDialog) {
val clickQrcodeThrottle = ThrottleState.use(scope) val clickQrcodeThrottle = ThrottleState.use(scope)
Dialog(onDismissRequest = { showAddDialog = false },) { Dialog(onDismissRequest = { showAddDialog = false }) {
Box( Box(
Modifier Modifier
.width(250.dp) .width(250.dp)
@ -225,23 +276,20 @@ fun SubscriptionManagePage() {
text = "二维码", modifier = Modifier text = "二维码", modifier = Modifier
.clickable(onClick = clickQrcodeThrottle.invoke { .clickable(onClick = clickQrcodeThrottle.invoke {
showAddDialog = false showAddDialog = false
scanLauncher.launch(ScanOptions().apply { val qrCode = navigateForQrcodeResult()
setOrientationLocked(false) val contents = qrCode.contents
}) if (contents != null) {
showLinkInputDialog = true
linkText = contents
}
}) })
.fillMaxWidth() .fillMaxWidth()
.padding(8.dp) .padding(8.dp)
) )
Text(text = "链接", modifier = Modifier Text(text = "链接", modifier = Modifier
.clickable { .clickable {
showLinkInputDialog = true
showAddDialog = false showAddDialog = false
scope.launch {
// val newSubsItem =
// router.navigateForResult(SubsItemInsertPage) ?: return@launch
// subItemList = subItemList
// .toMutableList()
// .apply { add(newSubsItem) }
}
} }
.fillMaxWidth() .fillMaxWidth()
.padding(8.dp)) .padding(8.dp))
@ -249,4 +297,68 @@ fun SubscriptionManagePage() {
} }
} }
} }
if (showLinkInputDialog) {
Dialog(onDismissRequest = { showLinkInputDialog = false;linkText = "" }) {
Box(
Modifier
.width(250.dp)
.background(Color.White)
.padding(8.dp)
) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(text = "请输入订阅链接")
TextField(
value = linkText,
onValueChange = { linkText = it },
singleLine = true
)
Button(onClick = {
showLinkInputDialog = false
if (subItemList.any { it.updateUrl == linkText }) {
ToastUtils.showShort("该链接已经添加过")
return@Button
}
scope.launch {
try {
val text = Singleton.client.get(linkText).bodyAsText()
val subscriptionRaw = SubscriptionRaw.parse5(text)
File(
PathUtils.getExternalAppFilesPath()
.plus("/subscription/")
).apply {
if (!exists()) {
mkdir()
}
}
val file = File(
PathUtils.getExternalAppFilesPath()
.plus("/subscription/")
.plus(System.currentTimeMillis())
.plus(".json")
)
withContext(IO) {
file.writeText(text)
}
val tempItem = SubsItem(
updateUrl = subscriptionRaw.updateUrl ?: linkText,
filePath = file.absolutePath,
name = subscriptionRaw.name,
version = subscriptionRaw.version
)
val newItem = tempItem.copy(
id = RoomX.insert(tempItem)[0]
)
subItemList = subItemList.toMutableList().apply { add(newItem) }
} catch (e: Exception) {
e.printStackTrace()
ToastUtils.showShort(e.message ?: "")
}
}
}) {
Text(text = "添加")
}
}
}
}
}
} }

View File

@ -16,11 +16,11 @@ import android.os.Looper
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import com.blankj.utilcode.util.ToastUtils
import com.dylanc.activityresult.launcher.StartActivityLauncher import com.dylanc.activityresult.launcher.StartActivityLauncher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -192,13 +192,26 @@ object Ext {
context: CoroutineContext = EmptyCoroutineContext, context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT, start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit, block: suspend CoroutineScope.() -> Unit,
): Job { ) = launch(context, start) {
return this.launch(context, start) {
while (isActive) { while (isActive) {
block() block()
} }
} }
fun CoroutineScope.launchTry(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit,
) = launch(context, start) {
try {
block()
} catch (e: Exception) {
e.printStackTrace()
ToastUtils.showShort(e.message)
} }
}
fun createNotificationChannel(context: Service) { fun createNotificationChannel(context: Service) {
val channelId = "CHANNEL_TEST" val channelId = "CHANNEL_TEST"

View File

@ -4,18 +4,18 @@ import blue.endless.jankson.Jankson
import com.journeyapps.barcodescanner.BarcodeEncoder import com.journeyapps.barcodescanner.BarcodeEncoder
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.http.ContentType import io.ktor.http.ContentType
import io.ktor.serialization.kotlinx.json.json import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.text.SimpleDateFormat
import java.util.Locale
object Singleton { object Singleton {
// @OptIn(ExperimentalSerializationApi::class)
val json by lazy { val json by lazy {
Json { Json {
// prettyPrint = true
// prettyPrintIndent = "\u0020".repeat(2)
isLenient = true isLenient = true
ignoreUnknownKeys = true ignoreUnknownKeys = true
} }
@ -26,21 +26,13 @@ object Singleton {
install(ContentNegotiation) { install(ContentNegotiation) {
json(json, ContentType.Any) json(json, ContentType.Any)
} }
install(HttpTimeout){
connectTimeoutMillis = 3000
} }
} }
}
// inline fun <reified T : Any> produce(data: T, block: (data: T) -> Unit): T { val simpleDateFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
// val proxyData = Proxy.newProxyInstance(
// T::class.java.classLoader,
// arrayOf(),
// InvocationHandler { proxy, method, args ->
//
// }) as T
// block(proxyData)
// return proxyData
// }
val barcodeEncoder by lazy { BarcodeEncoder() } val barcodeEncoder by lazy { BarcodeEncoder() }
} }

View File

@ -1,16 +1,11 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="Theme.Gkd" parent="Theme.MaterialComponents.DayNight.DarkActionBar"> <style name="Theme.Gkd" parent="Theme.AppCompat">
<!-- Primary brand color. --> <!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item> <item name="colorPrimary">#ededed</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. --> <!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item> <item name="android:statusBarColor" tools:targetApi="l">@android:color/transparent</item>
<item name="android:windowLightStatusBar">true</item>
<!-- Customize your theme here. --> <!-- Customize your theme here. -->
</style> </style>
</resources> </resources>

View File

@ -1,16 +1,10 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="Theme.Gkd" parent="Theme.MaterialComponents.DayNight.DarkActionBar"> <style name="Theme.Gkd" parent="Theme.AppCompat">
<!-- Primary brand color. --> <!-- Primary brand color. -->
<item name="colorPrimary">#ededed</item> <item name="colorPrimary">#ededed</item>
<item name="colorPrimaryVariant">#ededed</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. --> <!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item> <item name="android:statusBarColor" tools:targetApi="l">@android:color/transparent</item>
<item name="android:windowLightStatusBar">true</item> <item name="android:windowLightStatusBar">true</item>
<!-- Customize your theme here. --> <!-- Customize your theme here. -->
</style> </style>
@ -23,12 +17,4 @@
</style> </style>
<!-- <style name="Entry" parent="Theme.AppCompat.Light.NoActionBar">-->
<!-- <item name="android:windowFullscreen">true</item>-->
<!-- <item name="android:windowIsTranslucent">true</item>-->
<!-- <item name="android:statusBarColor">@android:color/transparent</item>-->
<!-- <item name="android:windowTranslucentNavigation">true</item>-->
<!-- <item name="background">@android:color/transparent</item>-->
<!-- <item name="android:windowBackground">@android:color/transparent</item>-->
<!-- </style>-->
</resources> </resources>

View File

@ -1,6 +1,12 @@
package li.songe.gkd package li.songe.gkd
import org.junit.Test import kotlinx.serialization.decodeFromString
import li.songe.gkd.debug.server.api.Window
import li.songe.gkd.util.Singleton
import li.songe.selector_core.Node
import li.songe.selector_core.Selector
import java.io.File
import li.songe.gkd.debug.server.api.Node as ApiNode
/** /**
* Example local unit test, which will execute on the development machine (host). * Example local unit test, which will execute on the development machine (host).
@ -8,16 +14,82 @@ import org.junit.Test
* See [testing documentation](http://d.android.com/tools/testing). * See [testing documentation](http://d.android.com/tools/testing).
*/ */
class ExampleUnitTest { class ExampleUnitTest {
@Test // @Test
fun addition_isCorrect() { fun check_selector() {
// assertEquals(4, 4L) // println(Selector.parse("X View >n Text > Button[a=1][b=false][c=null][d!=`hello`] + A - X < Z"))
// println(MatchRule.parse("ImageView[text=hi][id=hi] >> WebView[text=hi] - TextView")) // println(Selector.parse("A[a=1][a!=3][a*=3][a!*=3][a^=null]"))
// val testFile = File("D:/User/Documents/Project/gkd-subscription/subs.json") // println(Selector.parse("@LinearLayout > TextView[id=`com.byted.pangle:id/tt_item_tv`][text=`不感兴趣`]"))
// val subsRaw = SubscriptionRaw.parse(testFile.readText())
// File("D:/User/Documents/Project/gkd-subscription/subs-2.json").writeText( // val s1 = "ImageView < @FrameLayout < LinearLayout < RelativeLayout <n\n" +
// SubscriptionRaw.stringify( // "LinearLayout < RelativeLayout + LinearLayout > RelativeLayout > TextView[text$=`广告`]"
// subsRaw // val selector = Selector.parse(s1)
// ) //// Selector.parse("ImageView < @FrameLayout < LinearLayout < RelativeLayout <n LinearLayout < RelativeLayout + LinearLayout > RelativeLayout > TextView[text$=`广告`]")
//
//
// val nodes =
// Singleton.json.decodeFromString<Window>(File("D:/User/Downloads/gkd/snapshot-1684381133305/window.json").readText()).nodes
// ?: emptyList()
//
// val simpleNodes = nodes.map { n ->
// SimpleNode(
// value = n
// ) // )
// }
// simpleNodes.forEach { simpleNode ->
// simpleNode.parent = simpleNodes.getOrNull(simpleNode.value.pid)?.apply {
// children.add(simpleNode)
// }
// }
// val rootWrapper = simpleNodes.map { SimpleNodeWrapper(it) }[0]
// println(rootWrapper.querySelector(selector))
}
class SimpleNode(
var parent: SimpleNode? = null,
val children: MutableList<SimpleNode> = mutableListOf(),
val value: ApiNode
) {
override fun toString(): String {
return value.toString()
}
}
data class SimpleNodeWrapper(val value: SimpleNode) : Node {
override val parent: Node?
get() = value.parent?.let { SimpleNodeWrapper(it) }
override val children: Sequence<Node?>
get() = sequence {
value.children.forEach { yield(SimpleNodeWrapper(it)) }
}
override val name: CharSequence
get() = value.value.attr.className ?: ""
override fun attr(name: String): Any? {
val attr = value.value.attr
return when (name) {
"id" -> attr.id
"name" -> attr.className
"text" -> attr.text
"textLen" -> attr.text?.length
"desc" -> attr.desc
"descLen" -> attr.desc?.length
"isClickable" -> attr.isClickable
"isChecked" -> null
"index" -> {
val children = value.parent?.children ?: return null
children.forEachIndexed { index, simpleNode ->
if (simpleNode as SimpleNodeWrapper == this) {
return index
}
}
return null
}
"_id" -> value.value.id
"_pid" -> value.value.pid
else -> null
}
}
} }
} }

View File

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

View File

@ -22,3 +22,4 @@ kotlin.code.style=official
#org.gradle.java.home=D\:/User/Documents/lisonge/.jdks/corretto-11.0.13 #org.gradle.java.home=D\:/User/Documents/lisonge/.jdks/corretto-11.0.13
#android.experimental.legacyTransform.forceNonIncremental=true #android.experimental.legacyTransform.forceNonIncremental=true
android.debug.obsoleteApi=true android.debug.obsoleteApi=true
kotlin.js.compiler=ir

View File

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

View File

@ -5,7 +5,7 @@ plugins {
} }
android { android {
namespace = "li.songe.selector" namespace = "li.songe.selector_android"
compileSdk = libs.versions.android.compileSdk.get().toInt() compileSdk = libs.versions.android.compileSdk.get().toInt()
buildToolsVersion = libs.versions.android.buildToolsVersion.get() buildToolsVersion = libs.versions.android.buildToolsVersion.get()

View File

@ -1,4 +1,4 @@
package li.songe.selector package li.songe.selector_android
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="li.songe.selector"> package="li.songe.selector_android">
</manifest> </manifest>

View File

@ -1,12 +1,12 @@
package li.songe.selector package li.songe.selector_android
import android.accessibilityservice.AccessibilityService import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.GestureDescription import android.accessibilityservice.GestureDescription
import android.graphics.Path import android.graphics.Path
import android.graphics.Rect import android.graphics.Rect
import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo
import li.songe.selector.parser.Transform import li.songe.selector_android.parser.Transform
import li.songe.selector.wrapper.PropertySelectorWrapper import li.songe.selector_android.wrapper.PropertySelectorWrapper
data class GkdSelector(val wrapper: PropertySelectorWrapper) { data class GkdSelector(val wrapper: PropertySelectorWrapper) {
@ -19,6 +19,7 @@ data class GkdSelector(val wrapper: PropertySelectorWrapper) {
return trackNodes.findLast { it != null } ?: child return trackNodes.findLast { it != null } ?: child
} }
} }
nodeInfo.getChild(1)
return null return null
} }

View File

@ -1,4 +1,4 @@
package li.songe.selector package li.songe.selector_android
import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo
import java.util.ArrayDeque import java.util.ArrayDeque
@ -25,8 +25,8 @@ inline fun AccessibilityNodeInfo.forEachIndexed(action: (index: Int, childNode:
} }
} }
inline fun AccessibilityNodeInfo.forEachAncestorIndexed(action: (depth: Int, ancestorNode: AccessibilityNodeInfo) -> Unit) { inline fun AccessibilityNodeInfo?.forEachAncestorIndexed(action: (depth: Int, ancestorNode: AccessibilityNodeInfo) -> Unit) {
var p: AccessibilityNodeInfo? = this var p = this
var depth = 0 var depth = 0
while (true) { while (true) {
val p2 = p?.parent val p2 = p?.parent
@ -129,3 +129,5 @@ fun AccessibilityNodeInfo.getBrother(dep: Int, elder: Boolean = true): Accessibi
} }
return null return null
} }

View File

@ -1,7 +1,7 @@
package li.songe.selector.expression package li.songe.selector_android.expression
import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo
import li.songe.selector.operator.Operator import li.songe.selector_android.operator.Operator
data class BinaryExpression(val name: String, val operator: Operator, val value: Any?) { data class BinaryExpression(val name: String, val operator: Operator, val value: Any?) {

View File

@ -1,7 +1,7 @@
package li.songe.selector.operator package li.songe.selector_android.operator
import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo
import li.songe.selector.expression.BinaryExpression import li.songe.selector_android.expression.BinaryExpression
object End : Operator("$=") { object End : Operator("$=") {

View File

@ -1,9 +1,9 @@
package li.songe.selector.operator package li.songe.selector_android.operator
import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo
import li.songe.selector.expression.BinaryExpression import li.songe.selector_android.expression.BinaryExpression
import li.songe.selector.getDepth import li.songe.selector_android.getDepth
import li.songe.selector.getIndex import li.songe.selector_android.getIndex
object Equal : Operator("=") { object Equal : Operator("=") {
override fun match(expression: BinaryExpression): (nodeInfo: AccessibilityNodeInfo) -> Boolean { override fun match(expression: BinaryExpression): (nodeInfo: AccessibilityNodeInfo) -> Boolean {

View File

@ -1,7 +1,7 @@
package li.songe.selector.operator package li.songe.selector_android.operator
import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo
import li.songe.selector.expression.BinaryExpression import li.songe.selector_android.expression.BinaryExpression
object Include : Operator("*=") { object Include : Operator("*=") {

View File

@ -1,9 +1,9 @@
package li.songe.selector.operator package li.songe.selector_android.operator
import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo
import li.songe.selector.expression.BinaryExpression import li.songe.selector_android.expression.BinaryExpression
import li.songe.selector.getDepth import li.songe.selector_android.getDepth
import li.songe.selector.getIndex import li.songe.selector_android.getIndex
object Less : Operator("<") { object Less : Operator("<") {
override fun match(expression: BinaryExpression): (nodeInfo: AccessibilityNodeInfo) -> Boolean { override fun match(expression: BinaryExpression): (nodeInfo: AccessibilityNodeInfo) -> Boolean {

View File

@ -1,9 +1,9 @@
package li.songe.selector.operator package li.songe.selector_android.operator
import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo
import li.songe.selector.expression.BinaryExpression import li.songe.selector_android.expression.BinaryExpression
import li.songe.selector.getDepth import li.songe.selector_android.getDepth
import li.songe.selector.getIndex import li.songe.selector_android.getIndex
object LessEqual : Operator("<=") { object LessEqual : Operator("<=") {

View File

@ -1,9 +1,9 @@
package li.songe.selector.operator package li.songe.selector_android.operator
import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo
import li.songe.selector.expression.BinaryExpression import li.songe.selector_android.expression.BinaryExpression
import li.songe.selector.getDepth import li.songe.selector_android.getDepth
import li.songe.selector.getIndex import li.songe.selector_android.getIndex
object More : Operator(">") { object More : Operator(">") {
override fun match(expression: BinaryExpression): (nodeInfo: AccessibilityNodeInfo) -> Boolean { override fun match(expression: BinaryExpression): (nodeInfo: AccessibilityNodeInfo) -> Boolean {

View File

@ -1,9 +1,9 @@
package li.songe.selector.operator package li.songe.selector_android.operator
import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo
import li.songe.selector.expression.BinaryExpression import li.songe.selector_android.expression.BinaryExpression
import li.songe.selector.getDepth import li.songe.selector_android.getDepth
import li.songe.selector.getIndex import li.songe.selector_android.getIndex
object MoreEqual : Operator(">=") { object MoreEqual : Operator(">=") {

View File

@ -1,9 +1,9 @@
package li.songe.selector.operator package li.songe.selector_android.operator
import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo
import li.songe.selector.expression.BinaryExpression import li.songe.selector_android.expression.BinaryExpression
import li.songe.selector.getDepth import li.songe.selector_android.getDepth
import li.songe.selector.getIndex import li.songe.selector_android.getIndex
object NotEqual : Operator("!=") { object NotEqual : Operator("!=") {

View File

@ -1,7 +1,7 @@
package li.songe.selector.operator package li.songe.selector_android.operator
import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo
import li.songe.selector.expression.BinaryExpression import li.songe.selector_android.expression.BinaryExpression
sealed class Operator(private val key: String) { sealed class Operator(private val key: String) {
override fun toString() = key override fun toString() = key

View File

@ -1,7 +1,7 @@
package li.songe.selector.operator package li.songe.selector_android.operator
import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo
import li.songe.selector.expression.BinaryExpression import li.songe.selector_android.expression.BinaryExpression
object Start : Operator("^=") { object Start : Operator("^=") {

View File

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

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) 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) : data class GkdSyntaxError(val expectedValue: String, val position: Int, val source: String) :
Exception( 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_android.GkdSelector
import li.songe.selector.expression.BinaryExpression import li.songe.selector_android.expression.BinaryExpression
import li.songe.selector.operator.End import li.songe.selector_android.operator.End
import li.songe.selector.operator.Equal import li.songe.selector_android.operator.Equal
import li.songe.selector.operator.Include import li.songe.selector_android.operator.Include
import li.songe.selector.operator.Less import li.songe.selector_android.operator.Less
import li.songe.selector.operator.LessEqual import li.songe.selector_android.operator.LessEqual
import li.songe.selector.operator.More import li.songe.selector_android.operator.More
import li.songe.selector.operator.MoreEqual import li.songe.selector_android.operator.MoreEqual
import li.songe.selector.operator.NotEqual import li.songe.selector_android.operator.NotEqual
import li.songe.selector.operator.Start import li.songe.selector_android.operator.Start
import li.songe.selector.selector.CombinatorSelector import li.songe.selector_android.selector.CombinatorSelector
import li.songe.selector.selector.PropertySelector import li.songe.selector_android.selector.PropertySelector
import li.songe.selector.wrapper.CombinatorSelectorWrapper import li.songe.selector_android.wrapper.CombinatorSelectorWrapper
import li.songe.selector.wrapper.PropertySelectorWrapper import li.songe.selector_android.wrapper.PropertySelectorWrapper
internal object Transform { internal object Transform {
val whiteCharParser = GkdParser("\u0020\t\r\n") { source, offset, prefix -> val whiteCharParser = GkdParser("\u0020\t\r\n") { source, offset, prefix ->

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( 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 android.view.accessibility.AccessibilityNodeInfo
import li.songe.selector.forEachAncestorIndexed import li.songe.selector_android.forEachAncestorIndexed
import li.songe.selector.forEachElderBrotherIndexed import li.songe.selector_android.forEachElderBrotherIndexed
import li.songe.selector.forEachIndexed import li.songe.selector_android.forEachIndexed
import li.songe.selector.forEachYoungerBrotherIndexed import li.songe.selector_android.forEachYoungerBrotherIndexed
import li.songe.selector.getAncestor import li.songe.selector_android.getAncestor
import li.songe.selector.getBrother import li.songe.selector_android.getBrother
import li.songe.selector.getDepth import li.songe.selector_android.getDepth
import li.songe.selector.getIndex import li.songe.selector_android.getIndex
import li.songe.selector.selector.CombinatorSelector import li.songe.selector_android.selector.CombinatorSelector
data class CombinatorSelectorWrapper( data class CombinatorSelectorWrapper(
private val combinatorSelector: CombinatorSelector, private val combinatorSelector: CombinatorSelector,

View File

@ -1,7 +1,7 @@
package li.songe.selector.wrapper package li.songe.selector_android.wrapper
import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo
import li.songe.selector.selector.PropertySelector import li.songe.selector_android.selector.PropertySelector
data class PropertySelectorWrapper( data class PropertySelectorWrapper(
private val propertySelector: PropertySelector, private val propertySelector: PropertySelector,

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.json.JSONObject
import org.junit.Test 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" rootProject.name = "gkd"
include(":app") include(":app")
include(":selector")
include(":router") include(":router")
include(":selector_core")
include(":selector_android")
pluginManagement {
repositories {
maven("https://plugins.gradle.org/m2/")
}
}
dependencyResolutionManagement { dependencyResolutionManagement {
// https://youtrack.jetbrains.com/issue/KT-55620
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories { repositories {
mavenLocal() mavenLocal()
@ -13,7 +21,6 @@ dependencyResolutionManagement {
} }
versionCatalogs { versionCatalogs {
create("libs") { create("libs") {
// 当前 android 项目 kotlin 的版本
library("android.gradle", "com.android.tools.build:gradle:7.3.1") library("android.gradle", "com.android.tools.build:gradle:7.3.1")
version("android.compileSdk", "33") version("android.compileSdk", "33")
@ -21,6 +28,7 @@ dependencyResolutionManagement {
version("android.targetSdk", "33") version("android.targetSdk", "33")
version("android.buildToolsVersion", "33.0.0") version("android.buildToolsVersion", "33.0.0")
// 当前 android 项目 kotlin 的版本
library("kotlin.gradle.plugin", "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.20") library("kotlin.gradle.plugin", "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.20")
library("kotlin.serialization", "org.jetbrains.kotlin:kotlin-serialization:1.8.20") library("kotlin.serialization", "org.jetbrains.kotlin:kotlin-serialization:1.8.20")
// library("kotlin.stdlib", "org.jetbrains.kotlin:kotlin-stdlib:1.8.10") // library("kotlin.stdlib", "org.jetbrains.kotlin:kotlin-stdlib:1.8.10")
@ -58,6 +66,8 @@ dependencyResolutionManagement {
library("others.zxing.android.embedded", "com.journeyapps:zxing-android-embedded:4.3.0") library("others.zxing.android.embedded", "com.journeyapps:zxing-android-embedded:4.3.0")
library("others.floating.bubble.view", "io.github.torrydo:floating-bubble-view:0.5.2") library("others.floating.bubble.view", "io.github.torrydo:floating-bubble-view:0.5.2")
library("androidx.localbroadcastmanager", "androidx.localbroadcastmanager:localbroadcastmanager:1.1.0")
library("androidx.appcompat", "androidx.appcompat:appcompat:1.6.1") library("androidx.appcompat", "androidx.appcompat:appcompat:1.6.1")
library("androidx.core.ktx", "androidx.core:core-ktx:1.10.0") library("androidx.core.ktx", "androidx.core:core-ktx:1.10.0")
library( library(
@ -72,7 +82,6 @@ dependencyResolutionManagement {
library("androidx.room.compiler", "androidx.room:room-compiler:2.5.1") library("androidx.room.compiler", "androidx.room:room-compiler:2.5.1")
library("androidx.room.ktx", "androidx.room:room-ktx:2.5.1") library("androidx.room.ktx", "androidx.room:room-ktx:2.5.1")
library("google.material", "com.google.android.material:material:1.8.0")
library( library(
"google.accompanist.drawablepainter", "google.accompanist.drawablepainter",
"com.google.accompanist:accompanist-drawablepainter:0.23.1" "com.google.accompanist:accompanist-drawablepainter:0.23.1"
@ -113,13 +122,9 @@ dependencyResolutionManagement {
"org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5" "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5"
) )
// https://developer.android.com/reference/kotlin/org/json/package-summary
library("org.json", "org.json:json:20210307") library("org.json", "org.json:json:20210307")
} }
} }
} }
pluginManagement {
repositories {
maven("https://plugins.gradle.org/m2/")
}
}