6.7.0 - Alpha15 - 插件中心 M2 - 主页增加 "插件" 标签页; 插件中心支持搜索/筛选/排序; 版本忽略功能框架 (未完成)

This commit is contained in:
SuperMonster003
2026-01-17 15:40:08 +08:00
parent b6df1acf92
commit bbf7356ad4
41 changed files with 1413 additions and 395 deletions

View File

@@ -1,9 +1,9 @@
{ {
"$data": { "$data": {
"v6.7.0": { "v6.7.0": {
"released_date": "2026/01/14", "released_date": "2026/01/17",
"feature": [ "feature": [
"插件中心功能, 支持插件的安装/卸载/更新等操作 (入口: 主页抽屉按钮)", "插件中心功能, 支持插件的安装/卸载/更新等操作 (入口: 主页抽屉按钮/主页标签页)",
"cvt 模块, 用于数据单位转换 (参阅 项目文档 > [单位转换](https://docs.autojs6.com/#/cvt))", "cvt 模块, 用于数据单位转换 (参阅 项目文档 > [单位转换](https://docs.autojs6.com/#/cvt))",
"fmt 模块, 用于数据格式化 (参阅 项目文档 > [格式化](https://docs.autojs6.com/#/fmt))", "fmt 模块, 用于数据格式化 (参阅 项目文档 > [格式化](https://docs.autojs6.com/#/fmt))",
"zip 模块, 用于文件压缩与解压缩操作 (Ref to [Auto.js Pro](https://g.pro.autojs.org/)) (参阅 项目文档 > [Zip](https://docs.autojs6.com/#/zip))", "zip 模块, 用于文件压缩与解压缩操作 (Ref to [Auto.js Pro](https://g.pro.autojs.org/)) (参阅 项目文档 > [Zip](https://docs.autojs6.com/#/zip))",

View File

@@ -7,15 +7,17 @@ import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.SearchView
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.afollestad.materialdialogs.DialogAction
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.autojs.autojs.extension.MaterialDialogExtensions.widgetThemeColor
import org.autojs.autojs.ui.BaseActivity import org.autojs.autojs.ui.BaseActivity
import org.autojs.autojs.ui.widget.SearchViewItem
import org.autojs.autojs.util.ViewUtils import org.autojs.autojs.util.ViewUtils
import org.autojs.autojs.util.ViewUtils.onceGlobalLayout
import org.autojs.autojs.util.ViewUtils.setMenuIconsColorByThemeColorLuminance import org.autojs.autojs.util.ViewUtils.setMenuIconsColorByThemeColorLuminance
import org.autojs.autojs.util.ViewUtils.setNavigationIconColorByThemeColorLuminance import org.autojs.autojs.util.ViewUtils.setNavigationIconColorByThemeColorLuminance
import org.autojs.autojs.util.ViewUtils.setTitlesTextColorByThemeColorLuminance
import org.autojs.autojs6.R import org.autojs.autojs6.R
import org.autojs.autojs6.databinding.ActivityPluginCenterBinding import org.autojs.autojs6.databinding.ActivityPluginCenterBinding
@@ -24,6 +26,8 @@ class PluginCenterActivity : BaseActivity() {
private lateinit var binding: ActivityPluginCenterBinding private lateinit var binding: ActivityPluginCenterBinding
private var mSearchViewItem: SearchViewItem? = null
private val pickApkLauncher = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> private val pickApkLauncher = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
uri ?: return@registerForActivityResult uri ?: return@registerForActivityResult
lifecycleScope.launch { lifecycleScope.launch {
@@ -48,73 +52,34 @@ class PluginCenterActivity : BaseActivity() {
override fun onCreateOptionsMenu(menu: Menu?): Boolean { override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu_plugin_center, menu) menuInflater.inflate(R.menu.menu_plugin_center, menu)
setUpSearchMenuItem(menu)
setUpToolbarColors() setUpToolbarColors()
return true return true
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
val center = supportFragmentManager.findFragmentById(R.id.fragment_plugin_center) as? PluginCenterFragment
return when (item.itemId) { return when (item.itemId) {
R.id.action_install_from_local_file -> { R.id.action_install_from_local_file -> {
pickApkLauncher.launch(arrayOf("application/vnd.android.package-archive")) PluginInstallActions.installFromLocalFile(pickApkLauncher)
true true
} }
R.id.action_install_from_url -> { R.id.action_install_from_url -> {
MaterialDialog.Builder(this) PluginInstallActions.showInstallFromUrlDialog(this, lifecycleScope)
.title(R.string.text_install_plugin_from_url)
.content(R.string.instruction_install_plugin_from_url)
.input(null, null) { d, input ->
val positiveButton = d.getActionButton(DialogAction.POSITIVE)
when {
input.isNullOrBlank() -> {
positiveButton.setOnClickListener(null)
positiveButton.setTextColor(d.context.getColor(R.color.dialog_button_unavailable))
}
else -> {
positiveButton.setOnClickListener {
d.dismiss()
val url = input.trim().toString()
val context = this@PluginCenterActivity
lifecycleScope.launch {
runCatching {
PluginInstaller.installFromUrlWithPrompt(context, url)
}.onFailure { e ->
MaterialDialog.Builder(context)
.title(R.string.text_failed_to_retrieve)
.content(e.message ?: e.toString())
.positiveText(R.string.dialog_button_dismiss)
.show()
}
}
}
positiveButton.setTextColor(d.context.getColor(R.color.dialog_button_attraction))
}
}
}
.alwaysCallInputCallback()
.widgetThemeColor()
.negativeText(R.string.text_cancel)
.negativeColorRes(R.color.dialog_button_default)
.onNegative { d, _ -> d.dismiss() }
.positiveText(R.string.dialog_button_retrieve)
.positiveColorRes(R.color.dialog_button_unavailable)
.autoDismiss(false)
.cancelable(false)
.show()
true true
} }
R.id.action_search -> { R.id.action_search -> {
// TODO action_search // Handled by SearchViewItem.
ViewUtils.showToast(this, R.string.text_under_development) // zh-CN: 由 SearchViewItem 处理.
true super.onOptionsItemSelected(item)
} }
R.id.action_sort -> { R.id.action_sort -> {
// TODO action_sort showSortDialog(center)
ViewUtils.showToast(this, R.string.text_under_development)
true true
} }
R.id.action_filter -> { R.id.action_filter -> {
// TODO action_filter showFilterDialog(center)
ViewUtils.showToast(this, R.string.text_under_development)
true true
} }
R.id.action_global_settings -> { R.id.action_global_settings -> {
@@ -126,9 +91,96 @@ class PluginCenterActivity : BaseActivity() {
} }
} }
private fun setUpSearchMenuItem(menu: Menu?) {
val m = menu ?: return
val searchMenuItem = m.findItem(R.id.action_search) ?: return
mSearchViewItem = object : SearchViewItem(this, searchMenuItem) {
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
binding.toolbar.onceGlobalLayout { setUpToolbarColors() }
return super.onMenuItemActionExpand(item)
}
}.apply {
setQueryCallback(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?) = true.also { submitQueryToFragment(query) }
override fun onQueryTextChange(newText: String?) = true.also { submitQueryToFragment(newText) }
})
}
}
private fun submitQueryToFragment(query: String?) {
val center = supportFragmentManager.findFragmentById(R.id.fragment_plugin_center) as? PluginCenterFragment ?: return
center.setQuery(query)
}
private fun showSortDialog(center: PluginCenterFragment?) {
if (center == null) return
MaterialDialog.Builder(this)
.title(R.string.text_sort)
.items(
listOf(
getString(R.string.text_sort_by_name),
getString(R.string.text_sort_by_last_update_time),
getString(R.string.text_sort_by_package_size),
)
)
.itemsCallback { d, _, which, _ ->
d.dismiss()
when (which) {
0 -> center.setSort(PluginCenterFragment.Sort.TITLE_ASC)
1 -> center.setSort(PluginCenterFragment.Sort.LAST_UPDATE_DESC)
2 -> center.setSort(PluginCenterFragment.Sort.PACKAGE_SIZE_DESC)
else -> Unit
}
}
.negativeText(R.string.text_cancel)
.negativeColorRes(R.color.dialog_button_default)
.show()
}
private fun showFilterDialog(center: PluginCenterFragment?) {
if (center == null) return
MaterialDialog.Builder(this)
.title(R.string.text_filter)
.items(
listOf(
getString(R.string.text_all),
getString(R.string.text_installed),
getString(R.string.text_not_installed),
getString(R.string.text_enabled),
getString(R.string.text_disabled),
getString(R.string.text_updatable),
)
)
.itemsCallback { d, _, which, _ ->
d.dismiss()
when (which) {
0 -> center.setFilter(PluginCenterFragment.Filter.ALL)
1 -> center.setFilter(PluginCenterFragment.Filter.INSTALLED)
2 -> center.setFilter(PluginCenterFragment.Filter.NOT_INSTALLED)
3 -> center.setFilter(PluginCenterFragment.Filter.ENABLED)
4 -> center.setFilter(PluginCenterFragment.Filter.DISABLED)
5 -> center.setFilter(PluginCenterFragment.Filter.UPDATABLE)
else -> Unit
}
}
.negativeText(R.string.text_cancel)
.negativeColorRes(R.color.dialog_button_default)
.show()
}
private fun setUpToolbarColors() { private fun setUpToolbarColors() {
binding.toolbar.setMenuIconsColorByThemeColorLuminance(this) binding.toolbar.setMenuIconsColorByThemeColorLuminance(this)
binding.toolbar.setNavigationIconColorByThemeColorLuminance(this) binding.toolbar.setNavigationIconColorByThemeColorLuminance(this)
binding.toolbar.setTitlesTextColorByThemeColorLuminance(this)
mSearchViewItem?.setColorsByThemeColorLuminance()
}
override fun onDestroy() {
super.onDestroy()
mSearchViewItem = null
} }
companion object { companion object {

View File

@@ -39,6 +39,17 @@ class PluginCenterFragment : Fragment(R.layout.fragment_plugin_center) {
private var isFirstEnter: Boolean = true private var isFirstEnter: Boolean = true
// Latest full list from ViewModel (unfiltered).
// zh-CN: 来自 ViewModel 的最新完整列表 (未过滤).
private var latestFullItems: List<PluginCenterItem> = emptyList()
// Current query used by UI filtering, null means "no filtering".
// zh-CN: 当前用于 UI 过滤的查询串, null 表示 "不做过滤".
private var currentQuery: String? = null
private var currentSort: Sort = Sort.TITLE_ASC
private var currentFilter: Filter = Filter.ALL
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
_binding = FragmentPluginCenterBinding.bind(view) _binding = FragmentPluginCenterBinding.bind(view)
@@ -72,21 +83,7 @@ class PluginCenterFragment : Fragment(R.layout.fragment_plugin_center) {
} }
override fun onUpdate(item: PluginCenterItem) { override fun onUpdate(item: PluginCenterItem) {
val url = item.installableApkUrl PluginInfoDialogManager.showUpdatablePluginInfoDialog(contextRef, PluginInfoDialogManager.PluginInfoUpdatable(item))
when {
url.isNullOrBlank() -> MaterialDialog.Builder(contextRef)
.title(R.string.text_failed_to_update)
.content(R.string.error_no_available_url_provided_for_current_plugin)
.positiveText(R.string.dialog_button_dismiss)
.show()
else -> viewLifecycleOwner.lifecycleScope.launch {
PluginInstaller.installFromUrlWithPrompt(
context = contextRef,
url = url,
expectedSha256 = item.installableApkSha256,
)
}
}
} }
}) })
@@ -111,8 +108,8 @@ class PluginCenterFragment : Fragment(R.layout.fragment_plugin_center) {
// zh-CN: 订阅列表数据. // zh-CN: 订阅列表数据.
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
vm.items.collectLatest { list -> vm.items.collectLatest { list ->
adapter.updateData(list) latestFullItems = list
updateEmptyHint(list, vm.indexLoaded.value) renderList()
} }
} }
@@ -121,7 +118,12 @@ class PluginCenterFragment : Fragment(R.layout.fragment_plugin_center) {
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
vm.indexLoaded.collectLatest { loaded -> vm.indexLoaded.collectLatest { loaded ->
binding.pluginCenterSwipeRefresh.isRefreshing = false binding.pluginCenterSwipeRefresh.isRefreshing = false
updateEmptyHint(adapter.items(), loaded) updateEmptyHint(
filteredItems = adapter.items(),
indexLoaded = loaded,
fullItems = latestFullItems,
query = currentQuery,
)
} }
} }
@@ -140,11 +142,95 @@ class PluginCenterFragment : Fragment(R.layout.fragment_plugin_center) {
} }
} }
private fun updateEmptyHint(items: List<PluginCenterItem>, indexLoaded: Boolean) { /**
* Update query for filtering current list.
* zh-CN: 更新用于过滤当前列表的查询串.
*/
fun setQuery(query: String?) {
currentQuery = query?.takeIf { it.isNotBlank() }
renderList()
}
/**
* Update sort strategy for current list rendering.
* zh-CN: 更新当前列表渲染的排序策略.
*/
fun setSort(sort: Sort) {
currentSort = sort
renderList()
}
/**
* Update filter strategy for current list rendering.
* zh-CN: 更新当前列表渲染的筛选策略.
*/
fun setFilter(filter: Filter) {
currentFilter = filter
renderList()
}
private fun renderList() {
val q = currentQuery
val filteredByQuery = if (q.isNullOrBlank()) {
latestFullItems
} else {
// @formatter:off
latestFullItems.filter { item ->
item.title.contains(q, ignoreCase = true) ||
item.packageName.contains(q, ignoreCase = true) ||
item.author?.contains(q, ignoreCase = true) == true ||
item.description?.contains(q, ignoreCase = true) == true
}
// @formatter:on
}
val filtered = filteredByQuery.filter { item ->
when (currentFilter) {
Filter.ALL -> true
Filter.INSTALLED -> item.isInstalled
Filter.NOT_INSTALLED -> !item.isInstalled
Filter.ENABLED -> item.isEnabled
Filter.DISABLED -> !item.isEnabled
Filter.UPDATABLE -> item.updatableVersionCode != null
}
}
val sorted = when (currentSort) {
Sort.TITLE_ASC -> filtered.sortedBy { it.title.lowercase() }
Sort.LAST_UPDATE_DESC -> filtered.sortedWith(
compareByDescending<PluginCenterItem> { it.isInstalled }
.thenByDescending { it.lastUpdateTime ?: 0L }
.thenBy { it.title.lowercase() }
)
Sort.PACKAGE_SIZE_DESC -> filtered.sortedWith(
compareByDescending<PluginCenterItem> { it.isInstalled }
.thenByDescending { it.packageSize }
.thenBy { it.title.lowercase() }
)
}
adapter.updateData(sorted)
updateEmptyHint(
filteredItems = sorted,
indexLoaded = vm.indexLoaded.value,
fullItems = latestFullItems,
query = currentQuery,
)
}
private fun updateEmptyHint(
filteredItems: List<PluginCenterItem>,
indexLoaded: Boolean,
fullItems: List<PluginCenterItem>,
query: String?,
) {
val hintView = binding.pluginCenterEmptyHint val hintView = binding.pluginCenterEmptyHint
val hasQuery = !query.isNullOrBlank()
when { when {
items.isNotEmpty() -> { filteredItems.isNotEmpty() -> {
// Has data: hide hint immediately and cancel any waiting tasks. // Has data: hide hint immediately and cancel any waiting tasks.
// zh-CN: 有数据: 立即隐藏提示, 并取消任何等待任务. // zh-CN: 有数据: 立即隐藏提示, 并取消任何等待任务.
emptyHintJob?.cancel() emptyHintJob?.cancel()
@@ -152,9 +238,17 @@ class PluginCenterFragment : Fragment(R.layout.fragment_plugin_center) {
hintView.visibility = View.GONE hintView.visibility = View.GONE
isFirstEnter = false isFirstEnter = false
} }
hasQuery && fullItems.isNotEmpty() -> {
// Search filtering produced empty results, do not show misleading "no plugins" hint.
// zh-CN: 搜索过滤导致结果为空时, 不显示可能误导的 "没有插件" 提示.
emptyHintJob?.cancel()
emptyHintJob = null
hintView.visibility = View.GONE
isFirstEnter = false
}
indexLoaded -> { indexLoaded -> {
// Local and index stages have ended, list is still empty, immediately show "no plugins" hint. // Local and index stages have ended, list is still empty, immediately show "no plugins" hint.
// zh-CN: 本地与索引阶段已结束, 列表仍为空, 立即显示"没有插件"提示. // zh-CN: 本地与索引阶段已结束, 列表仍为空, 立即显示 "没有插件" 提示.
emptyHintJob?.cancel() emptyHintJob?.cancel()
emptyHintJob = null emptyHintJob = null
hintView.visibility = View.VISIBLE hintView.visibility = View.VISIBLE
@@ -249,4 +343,23 @@ class PluginCenterFragment : Fragment(R.layout.fragment_plugin_center) {
_binding = null _binding = null
} }
// Sort strategy for rendering list.
// zh-CN: 用于渲染列表的排序策略.
enum class Sort {
TITLE_ASC,
LAST_UPDATE_DESC,
PACKAGE_SIZE_DESC,
}
// Filter strategy for rendering list.
// zh-CN: 用于渲染列表的筛选策略.
enum class Filter {
ALL,
INSTALLED,
NOT_INSTALLED,
ENABLED,
DISABLED,
UPDATABLE,
}
} }

View File

@@ -15,9 +15,17 @@ data class PluginCenterItem(
val versionName: String, val versionName: String,
val versionCode: Long? = null, val versionCode: Long? = null,
val versionDate: String? = null, val versionDate: String? = null,
var updatableVersionName: String? = null, var updatableVersionName: String? = null,
var updatableVersionCode: Long? = null, var updatableVersionCode: Long? = null,
var updatableVersionDate: String? = null, var updatableVersionDate: String? = null,
var updatableApkUrl: String? = null,
var updatableApkSha256: String? = null,
var updatableApkSizeBytes: Long? = null,
var updatableChangelogUrl: String? = null,
var updatableChangelogText: String? = null,
val author: String? = null, val author: String? = null,
val collaborators: List<String> = emptyList(), val collaborators: List<String> = emptyList(),
val description: String? = null, val description: String? = null,
@@ -53,7 +61,7 @@ data class PluginCenterItem(
} }
val isUpdatable: Boolean val isUpdatable: Boolean
get() = updatableVersionName != null get() = updatableVersionCode != null
var lastInstallTime: Long? var lastInstallTime: Long?
get() = PluginRecentStore.getLastInstalled(packageName) get() = PluginRecentStore.getLastInstalled(packageName)

View File

@@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.autojs.autojs.network.UpdateIgnoreStore
import org.autojs.autojs6.R import org.autojs.autojs6.R
/** /**
@@ -28,12 +29,17 @@ import org.autojs.autojs6.R
* - 先加载本地插件并立即推送列表 (本地优先). * - 先加载本地插件并立即推送列表 (本地优先).
* - 紧接着在后台加载索引, 成功后与本地合并再推送一次. * - 紧接着在后台加载索引, 成功后与本地合并再推送一次.
* - 若本地发现失败, 通过 fatalError 通知 UI 弹窗并退出 Activity. * - 若本地发现失败, 通过 fatalError 通知 UI 弹窗并退出 Activity.
*
* Created by JetBrains AI Assistant (GPT-5.2) on Nov 26, 2025.
* Modified by SuperMonster003 as of Jan 17, 2026.
*/ */
class PluginCenterViewModel : ViewModel() { class PluginCenterViewModel : ViewModel() {
private val indexRepo = PluginIndexRepository() // TODO by SuperMonster003 on Jan 17, 2026.
// private val indexRepo = PluginIndexRepository()
private val installedRepo = InstalledPluginRepository() private val installedRepo = InstalledPluginRepository()
private val enableStore = PluginEnableStore() private val enableStore = PluginEnableStore
private val _items = MutableStateFlow<List<PluginCenterItem>>(emptyList()) private val _items = MutableStateFlow<List<PluginCenterItem>>(emptyList())
val items: StateFlow<List<PluginCenterItem>> = _items val items: StateFlow<List<PluginCenterItem>> = _items
@@ -96,7 +102,9 @@ class PluginCenterViewModel : ViewModel() {
// Asynchronously load index and merge. // Asynchronously load index and merge.
// zh-CN: 异步加载索引并合并. // zh-CN: 异步加载索引并合并.
val indexEntries = runCatching { val indexEntries = runCatching {
indexRepo.fetchOfficialIndex(context, forceRefresh = forceRefreshIndex) // TODO by SuperMonster003 on Jan 17, 2026.
// indexRepo.fetchOfficialIndex(context, forceRefresh = forceRefreshIndex)
emptyList<PluginIndexEntry>()
}.onFailure { }.onFailure {
// Index fetch exception is not fatal, just log it. // Index fetch exception is not fatal, just log it.
// zh-CN: 索引获取异常不算致命, 使用日志记录即可. // zh-CN: 索引获取异常不算致命, 使用日志记录即可.
@@ -135,6 +143,11 @@ class PluginCenterViewModel : ViewModel() {
PluginInfoDialogManager.refreshIfShowing(context, _items.value) PluginInfoDialogManager.refreshIfShowing(context, _items.value)
} }
fun ignoreUpdatableVersion(item: PluginCenterItem) {
val v = item.updatableVersionCode ?: return
UpdateIgnoreStore.ignoreVersion(item.packageName, v)
}
private fun toPluginCenterItem(context: Context, index: PluginIndexEntry?, local: InstalledPluginRepository.InstalledPlugin?): PluginCenterItem? { private fun toPluginCenterItem(context: Context, index: PluginIndexEntry?, local: InstalledPluginRepository.InstalledPlugin?): PluginCenterItem? {
val packageName = local?.packageName ?: index?.packageName val packageName = local?.packageName ?: index?.packageName
if (packageName.isNullOrBlank()) return null if (packageName.isNullOrBlank()) return null
@@ -144,18 +157,21 @@ class PluginCenterViewModel : ViewModel() {
val author = local?.author ?: index?.author val author = local?.author ?: index?.author
val collaborators = index?.collaborators ?: emptyList() val collaborators = index?.collaborators ?: emptyList()
val versionNameLocal = local?.versionName ?: index?.versionName ?: context.getString(R.string.text_unknown) val versionNameLocal = local?.versionName ?: index?.releases?.firstOrNull()?.versionName ?: context.getString(R.string.text_unknown)
val versionCodeLocal = local?.versionCode val versionCodeLocal = local?.versionCode
val isInstalled = local != null val isInstalled = local != null
// Only mark as updatable when "installed and index version is higher", and fill in updatable target information. // 未安装时: installable 取 releases 最新 (第一个).
// zh-CN: 仅当 "已安装且索引版本更高" 时, 标记可更新, 并填充可更新目标信息. val latestRelease = index?.releases?.maxByOrNull { it.versionCode }
val (updatableName, updatableCode, updatableDate) = run {
val defaultVersionInfo = Triple<String?, Long?, String?>(null, null, null) // 已安装时: updatable 取 "大于 installed 且未忽略" 的最高版本.
versionCodeLocal ?: return@run defaultVersionInfo val targetUpdate = run {
val versionCodeIndex = index?.versionCode ?: return@run defaultVersionInfo if (!isInstalled || versionCodeLocal == null) return@run null
if (versionCodeIndex <= versionCodeLocal) return@run defaultVersionInfo val candidates = index?.releases
Triple(index.versionName, index.versionCode, index.versionDate) ?.filter { it.versionCode > versionCodeLocal }
?.sortedByDescending { it.versionCode }
?: emptyList()
candidates.firstOrNull { !UpdateIgnoreStore.isIgnored(packageName, it.versionCode) }
} }
val enabled = enableStore.isEnabled(context, packageName, defaultEnabled = isInstalled) val enabled = enableStore.isEnabled(context, packageName, defaultEnabled = isInstalled)
@@ -164,18 +180,28 @@ class PluginCenterViewModel : ViewModel() {
title = title, title = title,
packageName = packageName, packageName = packageName,
versionName = versionNameLocal, versionName = versionNameLocal,
versionCode = versionCodeLocal ?: index?.versionCode, versionCode = versionCodeLocal ?: latestRelease?.versionCode,
versionDate = index?.versionDate, versionDate = latestRelease?.versionDate,
updatableVersionName = updatableName,
updatableVersionCode = updatableCode, updatableVersionName = targetUpdate?.versionName,
updatableVersionDate = updatableDate, updatableVersionCode = targetUpdate?.versionCode,
updatableVersionDate = targetUpdate?.versionDate,
updatableApkUrl = targetUpdate?.apkUrl,
updatableApkSha256 = targetUpdate?.apkSha256,
updatableApkSizeBytes = targetUpdate?.apkSizeBytes,
updatableChangelogUrl = targetUpdate?.changelogUrl,
updatableChangelogText = targetUpdate?.changelogText,
author = author, author = author,
collaborators = collaborators, collaborators = collaborators,
description = description, description = description,
packageSize = local?.packageSize ?: 0,
installableApkUrl = index?.apkUrl, packageSize = local?.packageSize ?: 0L,
installableApkSha256 = index?.apkSha256,
installableApkSizeBytes = index?.apkSizeBytes, installableApkUrl = latestRelease?.apkUrl,
installableApkSha256 = latestRelease?.apkSha256,
installableApkSizeBytes = latestRelease?.apkSizeBytes,
// TODO 已安装优先用应用图标; 未安装走默认占位图. // TODO 已安装优先用应用图标; 未安装走默认占位图.
icon = local?.icon, icon = local?.icon,
isEnabled = enabled, isEnabled = enabled,

View File

@@ -3,17 +3,17 @@ package org.autojs.autojs.core.plugin.center
import android.content.Context import android.content.Context
import androidx.core.content.edit import androidx.core.content.edit
class PluginEnableStore { object PluginEnableStore {
private val spName = "plugin_center_enable_state" private const val SP_NAME = "plugin_center_enable_state"
fun isEnabled(context: Context, packageName: String, defaultEnabled: Boolean = true): Boolean { fun isEnabled(context: Context, packageName: String, defaultEnabled: Boolean = true): Boolean {
val sp = context.getSharedPreferences(spName, Context.MODE_PRIVATE) val sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
return sp.getBoolean(key(packageName), defaultEnabled) return sp.getBoolean(key(packageName), defaultEnabled)
} }
fun setEnabled(context: Context, packageName: String, enabled: Boolean) { fun setEnabled(context: Context, packageName: String, enabled: Boolean) {
val sp = context.getSharedPreferences(spName, Context.MODE_PRIVATE) val sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
sp.edit { putBoolean(key(packageName), enabled) } sp.edit { putBoolean(key(packageName), enabled) }
} }

View File

@@ -19,13 +19,7 @@ data class PluginIndexEntry(
/** @sample "paddle-ocr-pp-ocrv5" */ /** @sample "paddle-ocr-pp-ocrv5" */
val engineId: String? = null, val engineId: String? = null,
val versionName: String, val releases: List<PluginIndexRelease> = emptyList(),
val versionCode: Long? = null,
val versionDate: String? = null,
val apkUrl: String? = null,
val apkSha256: String? = null,
val apkSizeBytes: Long? = null,
val tags: List<String> = emptyList(), val tags: List<String> = emptyList(),
) )

View File

@@ -0,0 +1,14 @@
package org.autojs.autojs.core.plugin.center
data class PluginIndexRelease(
val versionName: String,
val versionCode: Long = 0L,
val versionDate: String? = null,
val apkUrl: String? = null,
val apkSha256: String? = null,
val apkSizeBytes: Long? = null,
val changelogUrl: String? = null,
val changelogText: String? = null,
)

View File

@@ -30,7 +30,7 @@ class PluginIndexRepository {
private const val KEY_LAST_FAILURE_TS = "last_failure_ts" private const val KEY_LAST_FAILURE_TS = "last_failure_ts"
private const val KEY_RETRY_ATTEMPTS = "retry_attempts" private const val KEY_RETRY_ATTEMPTS = "retry_attempts"
private const val CACHE_FILE_NAME = "plugin_center_index_cache.json" private const val CACHE_FILE_NAME = "autojs6_plugin_index.json"
private const val MIN_RETRY_INTERVAL_MS = 30_000L // 30 sec private const val MIN_RETRY_INTERVAL_MS = 30_000L // 30 sec
private const val MAX_RETRY_INTERVAL_MS = 10 * 60_000L // 10 min private const val MAX_RETRY_INTERVAL_MS = 10 * 60_000L // 10 min
@@ -266,12 +266,16 @@ class PluginIndexRepository {
engine = engine, engine = engine,
variant = variant, variant = variant,
engineId = engineId, engineId = engineId,
versionName = versionName, releases = listOf(
versionCode = versionCode, PluginIndexRelease(
versionDate = versionDate, versionName = versionName,
apkUrl = apkUrl, versionCode = versionCode ?: 0L,
apkSha256 = apkSha256, versionDate = versionDate,
apkSizeBytes = apkSize, apkUrl = apkUrl,
apkSha256 = apkSha256,
apkSizeBytes = apkSize,
),
),
tags = emptyList(), tags = emptyList(),
) )
} }

View File

@@ -3,6 +3,7 @@ package org.autojs.autojs.core.plugin.center
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.graphics.PorterDuff import android.graphics.PorterDuff
import android.graphics.drawable.Drawable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View.MeasureSpec.UNSPECIFIED import android.view.View.MeasureSpec.UNSPECIFIED
import android.widget.TextView import android.widget.TextView
@@ -24,6 +25,7 @@ import org.autojs.autojs.theme.ThemeColorManager
import org.autojs.autojs.util.ColorUtils import org.autojs.autojs.util.ColorUtils
import org.autojs.autojs.util.DisplayUtils import org.autojs.autojs.util.DisplayUtils
import org.autojs.autojs.util.TimeUtils import org.autojs.autojs.util.TimeUtils
import org.autojs.autojs.util.ViewUtils
import org.autojs.autojs.util.ViewUtils.colorFilterWithDesaturateOrNull import org.autojs.autojs.util.ViewUtils.colorFilterWithDesaturateOrNull
import org.autojs.autojs.util.ViewUtils.toCircular import org.autojs.autojs.util.ViewUtils.toCircular
import org.autojs.autojs6.R import org.autojs.autojs6.R
@@ -59,20 +61,22 @@ object PluginInfoDialogManager {
private fun showInstallablePluginInfoDialog(context: Context, item: PluginCenterItem) { private fun showInstallablePluginInfoDialog(context: Context, item: PluginCenterItem) {
val states = listOf(context.getString(R.string.text_installable)) val states = listOf(context.getString(R.string.text_installable))
val info = PluginInfoInstallable( val info = PluginInfoInstallable(
title = item.title, item = item,
states = states, states = states,
packageName = item.packageName,
version = item.versionSummary,
author = item.author,
collaborators = item.collaborators,
description = item.description,
packageSize = item.installableApkSizeBytes ?: 0L,
lastInstallTime = item.lastInstallTime, lastInstallTime = item.lastInstallTime,
lastUninstallTime = item.lastUninstallTime, lastUninstallTime = item.lastUninstallTime,
apkUrl = item.installableApkUrl,
sha256 = item.installableApkSha256,
) )
showPluginInfoDialogInternal(context, item, info) showPluginInfoDialogInternal(context, info) {
positiveText(R.string.text_install)
positiveColorRes(R.color.dialog_button_attraction)
onPositive { d, _ ->
d.dismiss()
val url = info.validateApkUrlAndPrompt(context, d) ?: return@onPositive
CoroutineScope(Dispatchers.Main).launch {
PluginInstaller.installFromUrlWithPrompt(context, url, info.sha256)
}
}
}
} }
private fun showInstalledPluginInfoDialog(context: Context, item: PluginCenterItem) { private fun showInstalledPluginInfoDialog(context: Context, item: PluginCenterItem) {
@@ -81,107 +85,63 @@ object PluginInfoDialogManager {
if (item.isUpdatable) add(context.getString(R.string.text_updatable)) if (item.isUpdatable) add(context.getString(R.string.text_updatable))
} }
val info = PluginInfoInstalled( val info = PluginInfoInstalled(
title = item.title, item = item,
states = states, states = states,
packageName = item.packageName,
version = item.versionSummary,
author = item.author,
collaborators = item.collaborators,
description = item.description,
packageSize = item.packageSize,
updatableVersion = item.updatableVersionSummary, updatableVersion = item.updatableVersionSummary,
firstInstallTime = item.firstInstallTime, firstInstallTime = item.firstInstallTime,
lastUpdateTime = item.lastUpdateTime, lastUpdateTime = item.lastUpdateTime,
apkUrl = item.installableApkUrl,
sha256 = item.installableApkSha256,
) )
showPluginInfoDialogInternal(context, item, info) showPluginInfoDialogInternal(context, info) {
positiveText(R.string.text_uninstall)
positiveColorRes(R.color.dialog_button_warn)
onPositive { d, _ -> item.uninstallWithPrompt(context, d) }
if (item.isUpdatable) {
neutralText(R.string.dialog_button_view_update)
neutralColorRes(R.color.dialog_button_attraction)
onNeutral { d, _ ->
showUpdatablePluginInfoDialog(context, PluginInfoUpdatable(item), d)
}
}
}.apply {
makeSettingsLaunchable({ it.iconView }, info.packageName)
}
} }
private fun showPluginInfoDialogInternal(context: Context, item: PluginCenterItem, info: PluginInfoBase) { private fun showPluginInfoDialogInternal(context: Context, info: PluginInfoBase, builderApplier: MaterialDialog.Builder.() -> Unit = {}): MaterialDialog {
val binding = PluginInfoDialogItemsBinding.inflate(LayoutInflater.from(context)) val binding = PluginInfoDialogItemsBinding.inflate(LayoutInflater.from(context))
val dialog = MaterialDialog.Builder(context) val dialog = MaterialDialog.Builder(context)
.title(info.title)
.customView(binding.root, false)
.autoDismiss(false)
.iconRes(R.drawable.ic_three_dots_outline_small) .iconRes(R.drawable.ic_three_dots_outline_small)
.limitIconToDefaultSize() .limitIconToDefaultSize()
.title(info.title)
.customView(binding.root, false)
.negativeText(R.string.dialog_button_dismiss) .negativeText(R.string.dialog_button_dismiss)
.onNegative { d, _ -> d.dismiss() } .onNegative { d, _ -> d.dismiss() }
.apply { .autoDismiss(false)
when (info) { .apply(builderApplier)
is PluginInfoInstallable -> {
positiveText(R.string.text_install)
positiveColorRes(R.color.dialog_button_attraction)
onPositive { d, _ ->
val url = info.apkUrl
when {
url.isNullOrBlank() -> {
MaterialDialog.Builder(context)
.title(R.string.text_failed_to_install)
.content(R.string.error_no_available_url_provided_for_current_plugin)
.positiveText(R.string.dialog_button_dismiss)
.show()
val positiveButton = d.getActionButton(DialogAction.POSITIVE)
positiveButton.setTextColor(d.context.getColor(R.color.dialog_button_unavailable))
}
else -> {
d.dismiss()
CoroutineScope(Dispatchers.Main).launch {
PluginInstaller.installFromUrlWithPrompt(context, url, info.sha256)
}
}
}
}
}
is PluginInfoInstalled -> {
positiveText(R.string.text_uninstall)
positiveColorRes(R.color.dialog_button_warn)
onPositive { d, _ -> item.uninstallWithPrompt(context, d) }
if (item.isUpdatable) {
neutralText(R.string.text_update)
neutralColorRes(R.color.dialog_button_attraction)
onNeutral { d, _ ->
val url = info.apkUrl
when {
url.isNullOrBlank() -> {
MaterialDialog.Builder(context)
.title(R.string.text_failed_to_update)
.content(R.string.error_no_available_url_provided_for_current_plugin)
.positiveText(R.string.dialog_button_dismiss)
.show()
}
else -> {
d.dismiss()
CoroutineScope(Dispatchers.Main).launch {
PluginInstaller.installFromUrlWithPrompt(context, url, info.sha256)
}
}
}
}
}
}
}
}
.show() .show()
.apply { .apply { makeTextCopyable { titleView } }
makeTextCopyable { titleView }
}
// Hold the current dialog and package name for refreshing on onResume. // Hold the current dialog and package name for refreshing on onResume.
// zh-CN: 记录 "当前对话框" 与包名, 便于 onResume 刷新. // zh-CN: 记录 "当前对话框" 与包名, 便于 onResume 刷新.
currentDialog = WeakReference(dialog) currentDialog = WeakReference(dialog)
currentPackageName = item.packageName currentPackageName = info.packageName
restoreEssentialViews(binding, context, info) restoreEssentialViews(binding, context, info)
updateGuidelines(binding) updateGuidelines(binding)
binding.stateValueFirst.text = info.states.getOrNull(0) when (info.states.size) {
if (info.states.size > 1) { 0 -> {
binding.stateValueSecond.text = info.states[1] binding.stateParent.isVisible = false
binding.stateSpliterFirstSecond.isVisible = true }
binding.stateValueSecond.isVisible = true 1 -> {
binding.stateValueFirst.text = info.states[0]
}
else -> {
binding.stateValueSecond.text = info.states[1]
binding.stateSpliterFirstSecond.isVisible = true
binding.stateValueSecond.isVisible = true
}
} }
dialog.setCopyableTextIfAbsent(binding.packageNameValue, info.packageName) dialog.setCopyableTextIfAbsent(binding.packageNameValue, info.packageName)
@@ -190,7 +150,7 @@ object PluginInfoDialogManager {
dialog.setCopyableTextIfAbsent(binding.descriptionValue, info.description) dialog.setCopyableTextIfAbsent(binding.descriptionValue, info.description)
dialog.setCopyableTextIfAbsent(binding.pluginItemInfoPackageSizeValue, info.packageSize.takeIf { it > 0 }?.let { formatSize(it) }) dialog.setCopyableTextIfAbsent(binding.pluginItemInfoPackageSizeValue, info.packageSize.takeIf { it > 0 }?.let { formatSize(it) })
val dialogIcon = item.icon ?: AppCompatResources.getDrawable(context, R.drawable.ic_plugin_center_default)?.mutate()?.also { d -> val dialogIcon = info.icon ?: AppCompatResources.getDrawable(context, R.drawable.ic_plugin_center_default)?.mutate()?.also { d ->
val adjustedImageContrastColor = ColorUtils.adjustColorForContrast(context.getColor(R.color.window_background), ThemeColorManager.colorPrimary, 2.3) val adjustedImageContrastColor = ColorUtils.adjustColorForContrast(context.getColor(R.color.window_background), ThemeColorManager.colorPrimary, 2.3)
DrawableCompat.setTint(d, adjustedImageContrastColor) DrawableCompat.setTint(d, adjustedImageContrastColor)
DrawableCompat.setTintMode(d, PorterDuff.Mode.SRC_IN) DrawableCompat.setTintMode(d, PorterDuff.Mode.SRC_IN)
@@ -205,10 +165,7 @@ object PluginInfoDialogManager {
borderColor = context.getColor(R.color.plugin_center_item_icon_border), borderColor = context.getColor(R.color.plugin_center_item_icon_border),
) )
) )
dialog.iconView.colorFilterWithDesaturateOrNull(item.isEnabled, 0.5F) dialog.iconView.colorFilterWithDesaturateOrNull(info.isEnabled, 0.5F)
if (info is PluginInfoInstalled) {
dialog.makeSettingsLaunchable({ it.iconView }, info.packageName)
}
} }
// If the index does not provide size, try to HEAD request to get it, update display after success. // If the index does not provide size, try to HEAD request to get it, update display after success.
@@ -226,6 +183,75 @@ object PluginInfoDialogManager {
} }
} }
} }
return dialog
}
internal fun showUpdatablePluginInfoDialog(context: Context, info: PluginInfoUpdatable, parentDialog: MaterialDialog? = null) {
info.validateApkUrlAndPrompt(context, parentDialog) ?: return
parentDialog?.dismiss()
// TODO 更新详情参考 org.autojs.autojs.network.UpdateChecker.Dialog.Builder.Update.
// showPluginInfoDialogInternal(context, info) {
// positiveText(R.string.dialog_button_update_now)
// positiveColorRes(R.color.dialog_button_attraction)
// onPositive { d, _ ->
// d.dismiss()
// CoroutineScope(Dispatchers.Main).launch {
// PluginInstaller.installFromUrlWithPrompt(context, url, info.sha256)
// }
// }
// }
val ignoreUpdateOption = MaterialDialog.OptionMenuItemSpec(context.getString(R.string.dialog_button_ignore_current_update)) { parentDialog ->
MaterialDialog.Builder(context)
.title(R.string.text_prompt)
.content(R.string.prompt_add_ignored_version)
.negativeText(R.string.dialog_button_cancel)
.positiveText(R.string.dialog_button_confirm)
.positiveColorRes(R.color.dialog_button_caution)
.onPositive { _, _ ->
// UpdateUtils.addIgnoredVersion(versionInfo)
ViewUtils.showToast(context, R.string.text_done)
parentDialog.dismiss()
}
.show()
}
MaterialDialog.Builder(context)
.title(info.version ?: info.title)
.options(listOf(ignoreUpdateOption))
.content(R.string.text_retrieving_release_notes)
.neutralText(R.string.dialog_button_version_histories)
.neutralColor(context.getColor(R.color.dialog_button_hint))
.onNeutral { _, _ ->
// DisplayVersionHistoriesActivity.launch(context)
}
.negativeText(R.string.dialog_button_cancel)
.negativeColor(context.getColor(R.color.dialog_button_default))
.onNegative { d, _ -> d.dismiss() }
.positiveText(R.string.dialog_button_update_now)
.positiveColor(context.getColor(R.color.dialog_button_unavailable))
.autoDismiss(false)
.cancelable(false)
}
private fun PluginInfoBase.validateApkUrlAndPrompt(context: Context, parentDialog: MaterialDialog?): String? {
val url = this.apkUrl
return when {
url.isNullOrBlank() -> {
MaterialDialog.Builder(context)
.title(R.string.text_prompt)
.content(R.string.error_no_available_url_provided_for_current_plugin)
.positiveText(R.string.dialog_button_dismiss)
.show()
parentDialog
?.getActionButton(DialogAction.POSITIVE)
?.setTextColor(context.getColor(R.color.dialog_button_unavailable))
null
}
else -> url
}
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
@@ -246,6 +272,9 @@ object PluginInfoDialogManager {
binding.pluginItemInfoCollaboratorsThirdParent.isVisible = true binding.pluginItemInfoCollaboratorsThirdParent.isVisible = true
} }
when (info) { when (info) {
is PluginInfoUpdatable -> {
/* No additional operations needed. */
}
is PluginInfoInstalled -> { is PluginInfoInstalled -> {
info.updatableVersion?.let { info.updatableVersion?.let {
binding.versionLabel.text = context.getString(R.string.plugin_item_info_installed_version) binding.versionLabel.text = context.getString(R.string.plugin_item_info_installed_version)
@@ -309,44 +338,39 @@ object PluginInfoDialogManager {
) )
private sealed interface PluginInfoBase { private sealed interface PluginInfoBase {
val title: String val item: PluginCenterItem
val states: List<String> val states: List<String> get() = emptyList()
val packageName: String val isEnabled: Boolean get() = item.isEnabled
val version: String val title: String get() = item.title
val author: String? val icon: Drawable? get() = item.icon
val collaborators: List<String> val packageName: String get() = item.packageName
val description: String? val version: String? get() = item.versionSummary
val author: String? get() = item.author
val collaborators: List<String> get() = item.collaborators
val description: String? get() = item.description
val packageSize: Long val packageSize: Long
val apkUrl: String? val apkUrl: String? get() = item.installableApkUrl
val sha256: String? val sha256: String? get() = item.installableApkSha256
} }
internal class PluginInfoUpdatable(
override val item: PluginCenterItem,
override val packageSize: Long = item.installableApkSizeBytes ?: 0L,
override val version: String? = item.updatableVersionSummary,
) : PluginInfoBase
private data class PluginInfoInstallable( private data class PluginInfoInstallable(
override val title: String, override val item: PluginCenterItem,
override val states: List<String>, override val states: List<String>,
override val packageName: String, override val packageSize: Long = item.installableApkSizeBytes ?: 0L,
override val version: String,
override val author: String?,
override val collaborators: List<String>,
override val description: String?,
override val packageSize: Long,
override val apkUrl: String?,
override val sha256: String?,
val lastInstallTime: Long?, val lastInstallTime: Long?,
val lastUninstallTime: Long?, val lastUninstallTime: Long?,
) : PluginInfoBase ) : PluginInfoBase
private data class PluginInfoInstalled( private data class PluginInfoInstalled(
override val title: String, override val item: PluginCenterItem,
override val states: List<String>, override val states: List<String>,
override val packageName: String, override val packageSize: Long = item.packageSize,
override val version: String,
override val author: String?,
override val collaborators: List<String>,
override val description: String?,
override val packageSize: Long,
override val apkUrl: String?,
override val sha256: String?,
val updatableVersion: String? = null, val updatableVersion: String? = null,
val firstInstallTime: Long?, val firstInstallTime: Long?,
val lastUpdateTime: Long?, val lastUpdateTime: Long?,

View File

@@ -0,0 +1,61 @@
package org.autojs.autojs.core.plugin.center
import androidx.activity.result.ActivityResultLauncher
import androidx.lifecycle.LifecycleCoroutineScope
import com.afollestad.materialdialogs.DialogAction
import com.afollestad.materialdialogs.MaterialDialog
import kotlinx.coroutines.launch
import org.autojs.autojs.extension.MaterialDialogExtensions.widgetThemeColor
import org.autojs.autojs6.R
object PluginInstallActions {
private val apkMimeTypes = arrayOf("application/vnd.android.package-archive")
fun installFromLocalFile(pickApkLauncher: ActivityResultLauncher<Array<String>>) {
pickApkLauncher.launch(apkMimeTypes)
}
fun showInstallFromUrlDialog(context: android.content.Context, scope: LifecycleCoroutineScope) {
MaterialDialog.Builder(context)
.title(R.string.text_install_plugin_from_url)
.content(R.string.instruction_install_plugin_from_url)
.input(null, null) { d, input ->
val positiveButton = d.getActionButton(DialogAction.POSITIVE)
when {
input.isNullOrBlank() -> {
positiveButton.setOnClickListener(null)
positiveButton.setTextColor(d.context.getColor(R.color.dialog_button_unavailable))
}
else -> {
positiveButton.setOnClickListener {
d.dismiss()
val url = input.trim().toString()
scope.launch {
runCatching {
PluginInstaller.installFromUrlWithPrompt(context, url)
}.onFailure { e ->
MaterialDialog.Builder(context)
.title(R.string.text_failed_to_retrieve)
.content(e.message ?: e.toString())
.positiveText(R.string.dialog_button_dismiss)
.show()
}
}
}
positiveButton.setTextColor(d.context.getColor(R.color.dialog_button_attraction))
}
}
}
.alwaysCallInputCallback()
.widgetThemeColor()
.negativeText(R.string.text_cancel)
.negativeColorRes(R.color.dialog_button_default)
.onNegative { d, _ -> d.dismiss() }
.positiveText(R.string.dialog_button_retrieve)
.positiveColorRes(R.color.dialog_button_unavailable)
.autoDismiss(false)
.cancelable(false)
.show()
}
}

View File

@@ -17,7 +17,8 @@ import org.autojs.autojs.ui.main.scripts.ApkInfoDialogManager
import org.autojs.autojs.util.ClipboardUtils import org.autojs.autojs.util.ClipboardUtils
import org.autojs.autojs.util.FileUtils import org.autojs.autojs.util.FileUtils
import org.autojs.autojs.util.FileUtils.toCacheFile import org.autojs.autojs.util.FileUtils.toCacheFile
import org.autojs.autojs.util.UpdateUtils import org.autojs.autojs.util.IntentUtils
import org.autojs.autojs.util.IntentUtils.SnackExceptionHolder
import org.autojs.autojs.util.ViewUtils import org.autojs.autojs.util.ViewUtils
import org.autojs.autojs6.R import org.autojs.autojs6.R
import java.io.EOFException import java.io.EOFException
@@ -54,7 +55,7 @@ object PluginInstaller {
MaterialDialog.Builder(context) MaterialDialog.Builder(context)
.title(R.string.text_prompt) .title(R.string.text_prompt)
.content(context.getString(R.string.prompt_file_may_not_be_a_valid_plugin_package_with_uri, "$uri")) .content(context.getString(R.string.prompt_file_may_not_be_a_valid_plugin_package_with_uri, "$uri"))
.negativeText(R.string.dialog_button_quit) .negativeText(R.string.dialog_button_abandon)
.negativeColorRes(R.color.dialog_button_default) .negativeColorRes(R.color.dialog_button_default)
.onNegative { d, _ -> d.dismiss() } .onNegative { d, _ -> d.dismiss() }
.positiveText(R.string.dialog_button_continue) .positiveText(R.string.dialog_button_continue)
@@ -166,7 +167,7 @@ object PluginInstaller {
.positiveColorRes(R.color.dialog_button_caution) .positiveColorRes(R.color.dialog_button_caution)
.onPositive { _, _ -> .onPositive { _, _ ->
d.getActionButton(DialogAction.POSITIVE).performClick() d.getActionButton(DialogAction.POSITIVE).performClick()
UpdateUtils.openUrl(context, url) IntentUtils.browse(context, url, SnackExceptionHolder(d.view))
} }
.cancelable(false) .cancelable(false)
.build() .build()
@@ -317,7 +318,7 @@ object PluginInstaller {
ClipboardUtils.setClip(context, result.message) ClipboardUtils.setClip(context, result.message)
ViewUtils.showSnack(d.view, R.string.text_already_copied_to_clip, false) ViewUtils.showSnack(d.view, R.string.text_already_copied_to_clip, false)
} }
.negativeText(R.string.dialog_button_quit) .negativeText(R.string.dialog_button_abandon)
.negativeColorRes(R.color.dialog_button_default) .negativeColorRes(R.color.dialog_button_default)
.onNegative { d, _ -> .onNegative { d, _ ->
d.dismiss() d.dismiss()

View File

@@ -20,6 +20,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.autojs.autojs.core.plugin.center.PluginEnableStore
import org.autojs.plugin.paddle.ocr.IOcrPlugin import org.autojs.plugin.paddle.ocr.IOcrPlugin
import org.autojs.plugin.paddle.ocr.OcrOptions import org.autojs.plugin.paddle.ocr.OcrOptions
import org.autojs.plugin.paddle.ocr.OcrResult import org.autojs.plugin.paddle.ocr.OcrResult
@@ -106,7 +107,9 @@ object PaddleOcrPluginHost {
// e.g. "v5" // e.g. "v5"
variant: String? = null, variant: String? = null,
): Discovered? { ): Discovered? {
val list = discover(context).filter { it.pluginInfo != null } val list = discover(context)
.filter { it.pluginInfo != null }
.filter { PluginEnableStore.isEnabled(context, it.serviceInfo.packageName, false) }
if (list.isEmpty()) return null if (list.isEmpty()) return null
if (engineId != null) { if (engineId != null) {
list.firstOrNull { d -> d.pluginInfo?.id == engineId }?.let { return it } list.firstOrNull { d -> d.pluginInfo?.id == engineId }?.let { return it }
@@ -117,7 +120,10 @@ object PaddleOcrPluginHost {
if (engine != null) { if (engine != null) {
list.firstOrNull { d -> d.pluginInfo?.engine == engine }?.let { return it } list.firstOrNull { d -> d.pluginInfo?.engine == engine }?.let { return it }
} }
return list.first() return list.maxBy {
val variant = it.pluginInfo?.variant ?: return@maxBy 0
variant.replace(Regex("\\D"), "").toIntOrNull() ?: 0
}
} }
// Convert temporary file to read-only FD. // Convert temporary file to read-only FD.

View File

@@ -32,6 +32,7 @@ import org.autojs.autojs.tool.SimpleObserver;
import org.autojs.autojs.ui.settings.DisplayVersionHistoriesActivity; import org.autojs.autojs.ui.settings.DisplayVersionHistoriesActivity;
import org.autojs.autojs.util.AndroidUtils; import org.autojs.autojs.util.AndroidUtils;
import org.autojs.autojs.util.IntentUtils; import org.autojs.autojs.util.IntentUtils;
import org.autojs.autojs.util.IntentUtils.SnackExceptionHolder;
import org.autojs.autojs.util.IntentUtils.ToastExceptionHolder; import org.autojs.autojs.util.IntentUtils.ToastExceptionHolder;
import org.autojs.autojs.util.TextUtils; import org.autojs.autojs.util.TextUtils;
import org.autojs.autojs.util.UpdateUtils; import org.autojs.autojs.util.UpdateUtils;
@@ -55,6 +56,7 @@ import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.File; import java.io.File;
import java.io.Reader; import java.io.Reader;
import java.net.URL;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@@ -77,31 +79,67 @@ import java.util.stream.Collectors;
@SuppressLint("CheckResult") @SuppressLint("CheckResult")
public class UpdateChecker { public class UpdateChecker {
private static final String TAG = UpdateChecker.class.getSimpleName();
private MaterialDialog mUpdateDialog;
private MaterialDialog mPendingDialog;
public enum PromptMode {DIALOG, SNACKBAR}
public static final String URL_BASE_GITHUB_RAW = "https://raw.githubusercontent.com/"; public static final String URL_BASE_GITHUB_RAW = "https://raw.githubusercontent.com/";
public static final String URL_BASE_GITHUB_HOME = "https://github.com/"; public static final String URL_BASE_GITHUB_HOME = "https://github.com/";
public static final String URL_VERSION_PROPS_RAW = URL_BASE_GITHUB_RAW + "SuperMonster003/AutoJs6/master/version.properties"; private static final String TAG = UpdateChecker.class.getSimpleName();
public static final String URL_VERSION_PROPS_BLOB = URL_BASE_GITHUB_HOME + "SuperMonster003/AutoJs6/blob/master/version.properties";
private final Handler mHandler = new Handler(Looper.getMainLooper()); private final Handler mHandler = new Handler(Looper.getMainLooper());
private final Context mContext; private final Context mContext;
private final View mView; private final View mView;
private final PromptMode mPromptMode;
private final SimpleObserver<ResponseBody> mCallback;
private final Executor mGitHubExecutor = Executors.newSingleThreadExecutor(); private final Executor mGitHubExecutor = Executors.newSingleThreadExecutor();
private UpdateChecker(Context context, View view, PromptMode promptMode, SimpleObserver<ResponseBody> callback) { private final PromptMode mPromptMode;
private final SimpleObserver<ResponseBody> mCallback;
private final String mGitHubUser;
private final String mGitHubRepo;
private final String mGitHubMainBranch;
private final String mGitHubVersionProperties;
private final String mUrlVersionPropsRaw;
private final String mRrlVersionPropsBlob;
private final GitHubChangeLogProvider mGitHubChangelogFileProvider;
private MaterialDialog mUpdateDialog;
private MaterialDialog mPendingDialog;
private UpdateChecker(Context context,
View view,
String gitHubUser,
String gitHubRepo,
String gitHubMainBranch,
String gitHubVersionProperties,
GitHubChangeLogProvider gitHubChangelogFileProvider,
PromptMode promptMode,
SimpleObserver<ResponseBody> callback
) {
mContext = context; mContext = context;
mView = view; mView = view;
mPromptMode = promptMode; mPromptMode = promptMode;
mCallback = callback; mCallback = callback;
mGitHubUser = gitHubUser;
mGitHubRepo = gitHubRepo;
mGitHubMainBranch = gitHubMainBranch;
mGitHubVersionProperties = gitHubVersionProperties;
mGitHubChangelogFileProvider = gitHubChangelogFileProvider;
mUrlVersionPropsRaw = URL_BASE_GITHUB_RAW + gitHubUser + "/" + gitHubRepo + "/" + gitHubMainBranch + "/" + gitHubVersionProperties;
mRrlVersionPropsBlob = URL_BASE_GITHUB_HOME + gitHubUser + "/" + gitHubRepo + "/" + "blob" + "/" + gitHubMainBranch + "/" + gitHubVersionProperties;
}
private static @Nullable Spanned getLatestReleaseFromGitHubRelease(GHRepository repo, GHRelease release) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
return GitHubRepoUtils.getReleaseHtml(repo, release);
});
try {
String rawHtmlContent = future.get(30, TimeUnit.SECONDS);
return Html.fromHtml(rawHtmlContent, Html.FROM_HTML_MODE_COMPACT);
} catch (Exception e) {
e.printStackTrace();
return null;
}
} }
public void checkNow() { public void checkNow() {
@@ -119,20 +157,20 @@ public class UpdateChecker {
AtomicReference<String> errorRaw = new AtomicReference<>(); AtomicReference<String> errorRaw = new AtomicReference<>();
Observable<ResponseBody> obsBlobSafe = getStreamingApi() Observable<ResponseBody> obsBlobSafe = getStreamingApi()
.streamingUrl(URL_VERSION_PROPS_BLOB) .streamingUrl(mRrlVersionPropsBlob)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.onErrorResumeNext(e -> { .onErrorResumeNext(e -> {
Log.d(TAG, "Error from obsBlobSafe while parsing version.properties"); Log.d(TAG, "Error from obsBlobSafe while parsing " + mGitHubVersionProperties);
errorBlob.set(e.getMessage()); errorBlob.set(e.getMessage());
e.printStackTrace(); e.printStackTrace();
return Observable.empty(); return Observable.empty();
}); });
Observable<ResponseBody> obsRawSafe = getStreamingApi() Observable<ResponseBody> obsRawSafe = getStreamingApi()
.streamingUrl(URL_VERSION_PROPS_RAW) .streamingUrl(mUrlVersionPropsRaw)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.onErrorResumeNext(e -> { .onErrorResumeNext(e -> {
Log.d(TAG, "Error from obsRawSafe while parsing version.properties"); Log.d(TAG, "Error from obsRawSafe while parsing " + mGitHubVersionProperties);
errorRaw.set(e.getMessage()); errorRaw.set(e.getMessage());
e.printStackTrace(); e.printStackTrace();
return Observable.empty(); return Observable.empty();
@@ -276,7 +314,7 @@ public class UpdateChecker {
} }
if (mUpdateDialog != null) { if (mUpdateDialog != null) {
d.getActionButton(DialogAction.NEGATIVE).setText(R.string.dialog_button_quit); d.getActionButton(DialogAction.NEGATIVE).setText(R.string.dialog_button_abandon);
d.getActionButton(DialogAction.NEGATIVE).setTextColor(context.getColor(R.color.dialog_button_caution)); d.getActionButton(DialogAction.NEGATIVE).setTextColor(context.getColor(R.color.dialog_button_caution));
d.getActionButton(DialogAction.POSITIVE).setText(R.string.dialog_button_retry); d.getActionButton(DialogAction.POSITIVE).setText(R.string.dialog_button_retry);
d.getActionButton(DialogAction.POSITIVE).setOnClickListener(v -> { d.getActionButton(DialogAction.POSITIVE).setOnClickListener(v -> {
@@ -290,7 +328,7 @@ public class UpdateChecker {
d.getActionButton(DialogAction.NEUTRAL).setTextColor(context.getColor(R.color.dialog_button_hint)); d.getActionButton(DialogAction.NEUTRAL).setTextColor(context.getColor(R.color.dialog_button_hint));
d.getActionButton(DialogAction.NEUTRAL).setOnClickListener(v -> { d.getActionButton(DialogAction.NEUTRAL).setOnClickListener(v -> {
d.dismiss(); d.dismiss();
UpdateUtils.openUrl(context, versionInfo.getDownloadUrl()); IntentUtils.browse(context, versionInfo.getDownloadUrl(), new SnackExceptionHolder(d.getView()));
}); });
} }
@@ -342,6 +380,9 @@ public class UpdateChecker {
mUpdateDialog = new Dialog.Builder.Update(context, versionInfo).build(); mUpdateDialog = new Dialog.Builder.Update(context, versionInfo).build();
mPendingDialog = new Dialog.Builder.Pending(context, R.string.text_preparing).build(); mPendingDialog = new Dialog.Builder.Pending(context, R.string.text_preparing).build();
MDButton neutralButton = mUpdateDialog.getActionButton(DialogAction.NEUTRAL);
neutralButton.setOnClickListener(null);
MDButton negativeButton = mUpdateDialog.getActionButton(DialogAction.NEGATIVE); MDButton negativeButton = mUpdateDialog.getActionButton(DialogAction.NEGATIVE);
negativeButton.setOnClickListener(v -> mUpdateDialog.dismiss()); negativeButton.setOnClickListener(v -> mUpdateDialog.dismiss());
@@ -351,56 +392,73 @@ public class UpdateChecker {
mUpdateDialog.show(); mUpdateDialog.show();
mGitHubExecutor.execute(() -> { mGitHubExecutor.execute(() -> {
GitHub github = GHub.getConnection(); GitHub github = GitHubRepoUtils.getConnection();
if (github == null) { if (github == null) {
Dialog.setDialogContent(mUpdateDialog, R.string.error_cannot_connect_to_github); Dialog.setDialogContent(mUpdateDialog, R.string.error_cannot_connect_to_github);
return; return;
} }
String userName = context.getString(R.string.developer_full_name); GHRepository repo = GitHubRepoUtils.getRepo(github, mGitHubUser, mGitHubRepo);
String repoName = context.getString(R.string.app_name);
GHRepository repo = GHub.getRepo(github, userName, repoName);
if (repo == null) { if (repo == null) {
Dialog.setDialogContent(mUpdateDialog, context.getString(R.string.error_invalid_github_repo, repoName)); Dialog.setDialogContent(mUpdateDialog, context.getString(R.string.error_invalid_github_repo, mGitHubRepo));
return; return;
} }
GHRelease release = GHub.getRelease(repo); GHRelease release = GitHubRepoUtils.getRelease(repo);
if (release == null) { if (release == null) {
Dialog.setDialogContent(mUpdateDialog, R.string.error_get_github_latest_release); Dialog.setDialogContent(mUpdateDialog, R.string.error_get_github_latest_release);
return; return;
} }
mHandler.post(() -> setUpdateDialogButtonNeutral(context, release.getHtmlUrl()));
String releaseTag = release.getTagName(); String releaseTag = release.getTagName();
if (!GHub.isTagMatches(releaseTag, propVersion)) { if (!GitHubRepoUtils.isTagMatches(releaseTag, propVersion)) {
Dialog.setDialogContent(mUpdateDialog, R.string.error_corresponding_github_release_may_not_published); Dialog.setDialogContent(mUpdateDialog, R.string.error_corresponding_github_release_may_not_published);
return; return;
} }
fetchLatestReleaseNotes(context, versionInfo, repo, release, releaseTag); fetchLatestChangelog(context, versionInfo, repo, release, releaseTag);
PagedIterable<GHAsset> assets = GHub.getAssets(release); PagedIterable<GHAsset> assets = GitHubRepoUtils.getAssets(release);
if (assets == null) { if (assets == null) {
mHandler.post(() -> new Dialog.Builder mHandler.post(() -> new Dialog.Builder
.Prompt(context, R.string.error_empty_github_release_assets) .Prompt(context, R.string.error_empty_github_release_assets)
.build().show()); .build().show());
return; return;
} }
mHandler.post(() -> setDialogUpdateButton(context, assets, versionInfo)); mHandler.post(() -> setUpdateDialogButtonPositive(context, assets, versionInfo));
}); });
} }
private void fetchLatestReleaseNotes(Context context, VersionInfo versionInfo, GHRepository repo, GHRelease release, String releaseTag) { private void fetchLatestChangelog(Context context, VersionInfo versionInfo, GHRepository repo, GHRelease release, String releaseTag) {
Language language = Objects.requireNonNullElse(Language.getPrefLanguageOrNull(), Language.EN); Language language = Objects.requireNonNullElse(Language.getPrefLanguageOrNull(), Language.EN);
String languageTag = language.getLocalCompatibleLanguageTag(); String languageTag = language.getLocalCompatibleLanguageTag();
String urlSuffix = "app/src/main/assets-app/doc/CHANGELOG-" + languageTag + ".md"; String urlSuffix = mGitHubChangelogFileProvider.with(languageTag);
String urlBlob = "https://github.com/SuperMonster003/AutoJs6/blob/master/" + urlSuffix;
String urlRaw = "https://raw.githubusercontent.com/SuperMonster003/AutoJs6/master/" + urlSuffix; if (urlSuffix == null) {
Spanned fallbackLatestReleaseNotes = getFallbackLatestReleaseNotes(repo, release);
if (fallbackLatestReleaseNotes == null) {
Dialog.setDialogContent(mUpdateDialog, R.string.error_failed_to_retrieve_release_notes);
} else {
Dialog.setDialogContent(mUpdateDialog, fallbackLatestReleaseNotes);
}
return;
}
if (urlSuffix.startsWith("/")) {
urlSuffix = urlSuffix.substring(1);
}
if (urlSuffix.startsWith("http://") || urlSuffix.startsWith("https://")) {
throw new IllegalArgumentException("Field \"mGitHubChangelogFileProvider\" for UpdataChecker should provide a relative path instead of a full url");
}
String urlBlob = "https://github.com/" + mGitHubUser + "/" + mGitHubRepo + "/blob/" + mGitHubMainBranch + "/" + urlSuffix;
String urlRaw = "https://raw.githubusercontent.com/" + mGitHubUser + "/" + mGitHubRepo + "/" + mGitHubMainBranch + "/" + urlSuffix;
Observable<Spanned> obsBlob = getStreamingApi() Observable<Spanned> obsBlob = getStreamingApi()
.streamingUrl(urlBlob) .streamingUrl(urlBlob)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.onErrorResumeNext(e -> { .onErrorResumeNext(e -> {
Log.d(TAG, "Error from obsBlob while parsing latest release notes from " + urlBlob); Log.d(TAG, "Error from obsBlob while parsing latest changelog from " + urlBlob);
e.printStackTrace(); e.printStackTrace();
return Observable.never(); return Observable.never();
}) })
@@ -408,7 +466,7 @@ public class UpdateChecker {
try { try {
String content = responseBody.string().trim(); String content = responseBody.string().trim();
Log.d(TAG, "Respond body string (first 500) got from github: " + content.substring(0, Math.min(content.length(), 500))); Log.d(TAG, "Respond body string (first 500) got from github: " + content.substring(0, Math.min(content.length(), 500)));
String html = parseLatestReleaseNotesFromHtml(content); String html = parseLatestChangelogFromHtml(content);
if (html != null && !html.isBlank()) { if (html != null && !html.isBlank()) {
String assembledHtml = assembleBlobDependenciesForSingleVersion(html, versionInfo.getVersionName(), urlBlob); String assembledHtml = assembleBlobDependenciesForSingleVersion(html, versionInfo.getVersionName(), urlBlob);
return Observable.just(Html.fromHtml(assembledHtml, Html.FROM_HTML_MODE_COMPACT)); return Observable.just(Html.fromHtml(assembledHtml, Html.FROM_HTML_MODE_COMPACT));
@@ -423,7 +481,7 @@ public class UpdateChecker {
.streamingUrl(urlRaw) .streamingUrl(urlRaw)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.onErrorResumeNext(e -> { .onErrorResumeNext(e -> {
Log.d(TAG, "Error from obsRawSafe while parsing latest release notes from " + urlRaw); Log.d(TAG, "Error from obsRawSafe while parsing latest changelog from " + urlRaw);
e.printStackTrace(); e.printStackTrace();
return Observable.never(); return Observable.never();
}) })
@@ -431,7 +489,7 @@ public class UpdateChecker {
try { try {
String content = responseBody.string().trim(); String content = responseBody.string().trim();
Log.d(TAG, "Respond body string got from github: " + content); Log.d(TAG, "Respond body string got from github: " + content);
String markdown = parseLatestReleaseNotesFromMarkdown(content, releaseTag); String markdown = parseLatestChangelogFromMarkdown(content, releaseTag);
if (markdown != null && !markdown.isBlank()) { if (markdown != null && !markdown.isBlank()) {
String assembledMarkdown = assembleRawDependenciesForSingleVersion(markdown, versionInfo.getVersionName(), urlRaw); String assembledMarkdown = assembleRawDependenciesForSingleVersion(markdown, versionInfo.getVersionName(), urlRaw);
String assembledHtml = TextUtils.markdownToHtml(assembledMarkdown); String assembledHtml = TextUtils.markdownToHtml(assembledMarkdown);
@@ -448,41 +506,40 @@ public class UpdateChecker {
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.timeout(23, TimeUnit.SECONDS) .timeout(23, TimeUnit.SECONDS)
.subscribe( .subscribe(
releaseNotesSpannedForSingleVersion -> { changelogSpannedForSingleVersion -> {
Dialog.setDialogContent(mUpdateDialog, releaseNotesSpannedForSingleVersion); Dialog.setDialogContent(mUpdateDialog, changelogSpannedForSingleVersion);
}, },
e -> { e -> {
e.printStackTrace(); e.printStackTrace();
Spanned fallbackLatestReleaseNotes = getFallbackLatestReleaseNotes(repo, release); Spanned fallbackLatestReleaseNotes = getFallbackLatestReleaseNotes(repo, release);
if (fallbackLatestReleaseNotes == null) { if (fallbackLatestReleaseNotes == null) {
Dialog.setDialogContent(mUpdateDialog, R.string.error_failed_to_retrieve_released_notes); Dialog.setDialogContent(mUpdateDialog, R.string.error_failed_to_retrieve_release_notes);
return; return;
} }
Dialog.setDialogContent(mUpdateDialog, fallbackLatestReleaseNotes); Dialog.setDialogContent(mUpdateDialog, fallbackLatestReleaseNotes);
if (language != Language.ZH_HANS) { if (language != Language.ZH_HANS) {
mHandler.post(() -> new Dialog.Builder mHandler.post(() -> new Dialog.Builder
.Prompt(context, R.string.text_prompt, R.string.content_failed_to_retrieve_released_notes_of_current_language_with_zh_hans_fallback) .Prompt(context, R.string.text_prompt, R.string.content_failed_to_retrieve_changelog_of_current_language_with_zh_hans_release_notes_fallback)
.build().show()); .build().show());
} }
} }
); );
} }
private @NotNull String assembleBlobDependenciesForSingleVersion(@NotNull String releaseNotes, String versionName, String markdownUrl) { private @NotNull String assembleBlobDependenciesForSingleVersion(@NotNull String changelog, String versionName, String markdownUrl) {
String labelImprovement = mContext.getString(R.string.changelog_label_improvement); String labelImprovement = mContext.getString(R.string.changelog_label_improvement);
String labelDependency = mContext.getString(R.string.changelog_label_dependency); String labelDependency = mContext.getString(R.string.changelog_label_dependency);
boolean isFiltered = false; boolean isFiltered = false;
List<String> filteredReleaseNotes = new ArrayList<>(); List<String> filteredChangelog = new ArrayList<>();
String[] items = releaseNotes.split("\n"); String[] items = changelog.split("\n");
for (String item : items) { for (String item : items) {
Log.d(TAG, "item: " + item); Log.d(TAG, "item: " + item);
if (item.matches(".*\\b" + labelDependency + "\\b.*")) { if (item.matches(".*\\b" + labelDependency + "\\b.*")) {
isFiltered = true; isFiltered = true;
} else { } else {
filteredReleaseNotes.add(item); filteredChangelog.add(item);
} }
} }
@@ -490,8 +547,8 @@ public class UpdateChecker {
String anchor = "v" + String.join("", versionName.split("\\.")); String anchor = "v" + String.join("", versionName.split("\\."));
String dependenciesSummary = mContext.getString(R.string.text_changelog_item_dependency); String dependenciesSummary = mContext.getString(R.string.text_changelog_item_dependency);
for (int i = filteredReleaseNotes.size() - 1; i >= 0; i--) { for (int i = filteredChangelog.size() - 1; i >= 0; i--) {
String item = filteredReleaseNotes.get(i); String item = filteredChangelog.get(i);
if (!item.isBlank()) { if (!item.isBlank()) {
String assembledHtml = "<li><code>" + String assembledHtml = "<li><code>" +
labelImprovement + labelImprovement +
@@ -502,28 +559,28 @@ public class UpdateChecker {
"\" rel=\"nofollow\"><code>" + "\" rel=\"nofollow\"><code>" +
"CHANGELOG.md" + "CHANGELOG.md" +
"</code></a></em></li>"; "</code></a></em></li>";
filteredReleaseNotes.add(i + 1, assembledHtml); filteredChangelog.add(i + 1, assembledHtml);
break; break;
} }
} }
return String.join("\n", filteredReleaseNotes); return String.join("\n", filteredChangelog);
} }
return releaseNotes; return changelog;
} }
private @NotNull String assembleRawDependenciesForSingleVersion(@NotNull String releaseNotes, String versionName, String markdownUrl) { private @NotNull String assembleRawDependenciesForSingleVersion(@NotNull String changelog, String versionName, String markdownUrl) {
String labelImprovement = mContext.getString(R.string.changelog_label_improvement); String labelImprovement = mContext.getString(R.string.changelog_label_improvement);
String labelDependency = mContext.getString(R.string.changelog_label_dependency); String labelDependency = mContext.getString(R.string.changelog_label_dependency);
boolean isFiltered = false; boolean isFiltered = false;
List<String> filteredReleaseNotes = new ArrayList<>(); List<String> filteredChangelog = new ArrayList<>();
String[] items = releaseNotes.split("\n"); String[] items = changelog.split("\n");
for (String item : items) { for (String item : items) {
if (item.contains("`" + labelDependency + "`")) { if (item.contains("`" + labelDependency + "`")) {
isFiltered = true; isFiltered = true;
} else { } else {
filteredReleaseNotes.add(item); filteredChangelog.add(item);
} }
} }
@@ -531,28 +588,21 @@ public class UpdateChecker {
String anchor = "v" + String.join("", versionName.split("\\.")); String anchor = "v" + String.join("", versionName.split("\\."));
String dependenciesSummary = mContext.getString(R.string.text_changelog_item_dependency); String dependenciesSummary = mContext.getString(R.string.text_changelog_item_dependency);
for (int i = filteredReleaseNotes.size() - 1; i >= 0; i--) { for (int i = filteredChangelog.size() - 1; i >= 0; i--) {
String item = filteredReleaseNotes.get(i); String item = filteredChangelog.get(i);
if (!item.isBlank()) { if (!item.isBlank()) {
String assembledMarkdown = "* `" + labelImprovement + "` " + dependenciesSummary + " _[`CHANGELOG.md`](" + markdownUrl + "#" + anchor + ")_"; String assembledMarkdown = "* `" + labelImprovement + "` " + dependenciesSummary + " _[`CHANGELOG.md`](" + markdownUrl + "#" + anchor + ")_";
filteredReleaseNotes.add(i + 1, assembledMarkdown); filteredChangelog.add(i + 1, assembledMarkdown);
break; break;
} }
} }
return String.join("\n", filteredReleaseNotes); return String.join("\n", filteredChangelog);
} }
return releaseNotes; return changelog;
}
private String assembleDependenciesInReleaseNotesListMarkdown(String fullReleaseNotes) {
// TODO by SuperMonster003 on Apr 24, 2025.
// ! Remove all dependency items and append a summary item as improvement.
// ! Reference to `generate_markdown.py`.
return fullReleaseNotes;
} }
@Nullable @Nullable
private String parseLatestReleaseNotesFromHtml(String htmlContent) { private String parseLatestChangelogFromHtml(String htmlContent) {
Document document = Jsoup.parse(htmlContent); Document document = Jsoup.parse(htmlContent);
Element releaseDateHeading = document.selectFirst("div.markdown-heading"); Element releaseDateHeading = document.selectFirst("div.markdown-heading");
if (releaseDateHeading != null) { if (releaseDateHeading != null) {
@@ -572,7 +622,7 @@ public class UpdateChecker {
} }
@Nullable @Nullable
private String parseLatestReleaseNotesFromMarkdown(String markdown, String releaseTag) { private String parseLatestChangelogFromMarkdown(String markdown, String releaseTag) {
Pattern pattern = Pattern.compile("#+\\s*" + releaseTag + "([\\s\\S]*?)(?=\\n#+\\s*v\\d+\\.\\d+|\\z)"); Pattern pattern = Pattern.compile("#+\\s*" + releaseTag + "([\\s\\S]*?)(?=\\n#+\\s*v\\d+\\.\\d+|\\z)");
Matcher matcher = pattern.matcher(markdown); Matcher matcher = pattern.matcher(markdown);
if (matcher.find()) { if (matcher.find()) {
@@ -590,27 +640,25 @@ public class UpdateChecker {
private Spanned getFallbackLatestReleaseNotes(GHRepository repo, GHRelease release) { private Spanned getFallbackLatestReleaseNotes(GHRepository repo, GHRelease release) {
Spanned htmlSpannedContent = getLatestReleaseFromGitHubRelease(repo, release); Spanned htmlSpannedContent = getLatestReleaseFromGitHubRelease(repo, release);
if (htmlSpannedContent == null || htmlSpannedContent.toString().isBlank()) { if (htmlSpannedContent == null || htmlSpannedContent.toString().isBlank()) {
Log.d(TAG, "Release note got nothing from the latest release (fallback)"); Log.d(TAG, "Failed to fetch the latest release notes (fallback)");
return null; return null;
} }
Log.d(TAG, "Release note got from latest release (fallback)"); Log.d(TAG, "Fetch the latest release notes (fallback) successfully");
return htmlSpannedContent; return htmlSpannedContent;
} }
private static @Nullable Spanned getLatestReleaseFromGitHubRelease(GHRepository repo, GHRelease release) { private void setUpdateDialogButtonNeutral(Context context, URL htmlUrl) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { String url = htmlUrl == null ? "" : htmlUrl.toString();
return GHub.getReleaseHtml(repo, release); if (url.isEmpty()) return;
mUpdateDialog.getActionButton(DialogAction.NEUTRAL).setText(R.string.dialog_button_view_with_browser);
mUpdateDialog.getActionButton(DialogAction.NEUTRAL).setTextColor(context.getColor(R.color.dialog_button_hint));
mUpdateDialog.getActionButton(DialogAction.NEUTRAL).setOnClickListener(v -> {
IntentUtils.browse(context, url, new SnackExceptionHolder(mUpdateDialog.getView()));
}); });
try {
String rawHtmlContent = future.get(30, TimeUnit.SECONDS);
return Html.fromHtml(rawHtmlContent, Html.FROM_HTML_MODE_COMPACT);
} catch (Exception e) {
e.printStackTrace();
return null;
}
} }
private void setDialogUpdateButton(@NonNull Context ctx, PagedIterable<GHAsset> ghAssets, VersionInfo versionInfo) { private void setUpdateDialogButtonPositive(@NonNull Context ctx, PagedIterable<GHAsset> ghAssets, VersionInfo versionInfo) {
MDButton positiveButton = mUpdateDialog.getActionButton(DialogAction.POSITIVE); MDButton positiveButton = mUpdateDialog.getActionButton(DialogAction.POSITIVE);
positiveButton.setTextColor(ctx.getColor(R.color.dialog_button_attraction)); positiveButton.setTextColor(ctx.getColor(R.color.dialog_button_attraction));
positiveButton.setOnClickListener(v -> { positiveButton.setOnClickListener(v -> {
@@ -623,7 +671,7 @@ public class UpdateChecker {
mGitHubExecutor.execute(() -> { mGitHubExecutor.execute(() -> {
List<String> abiList = AndroidUtils.getDeviceFilteredAbiList(); List<String> abiList = AndroidUtils.getDeviceFilteredAbiList();
abiList.add("universal"); abiList.add("universal");
GHub.Asset targetAsset = GHub.pickAssetIntelligently(ghAssets, abiList); GitHubRepoUtils.Asset targetAsset = GitHubRepoUtils.pickAssetIntelligently(ghAssets, abiList);
mPendingDialog.dismiss(); mPendingDialog.dismiss();
@@ -648,6 +696,15 @@ public class UpdateChecker {
}); });
} }
public enum PromptMode {DIALOG, SNACKBAR}
public interface GitHubChangeLogProvider {
@Nullable
String with(String languageTag);
}
public static class Builder { public static class Builder {
private final Context mContext; private final Context mContext;
@@ -655,8 +712,16 @@ public class UpdateChecker {
private PromptMode mPromptMode; private PromptMode mPromptMode;
private SimpleObserver<ResponseBody> mCallback; private SimpleObserver<ResponseBody> mCallback;
private String mGitHubUser;
private String mGitHubRepo;
private String mGitHubMainBranch = "main";
private String mGitHubVersionProperties = "version.properties";
private GitHubChangeLogProvider mGitHubChangelogFileProvider = languageTag -> "app/src/main/assets-app/doc/CHANGELOG-" + languageTag + ".md";
public Builder(Context context) { public Builder(Context context) {
mContext = context; mContext = context;
mGitHubUser = context.getString(R.string.developer_full_name);
mGitHubRepo = context.getString(R.string.app_name);
} }
public Builder(@NonNull View view) { public Builder(@NonNull View view) {
@@ -664,18 +729,52 @@ public class UpdateChecker {
mView = view; mView = view;
} }
public Builder setGitHubUser(String user) {
mGitHubUser = user;
return this;
}
public Builder setGitHubRepo(String repo) {
mGitHubRepo = repo;
return this;
}
public Builder setGitHubMainBranch(String branch) {
mGitHubMainBranch = branch;
return this;
}
public Builder setGitHubVersionProperties(String versionProperties) {
mGitHubVersionProperties = versionProperties;
return this;
}
public Builder setGitHubChangelogFileProvider(GitHubChangeLogProvider provider) {
mGitHubChangelogFileProvider = provider;
return this;
}
public Builder setPromptMode(PromptMode promptMode) { public Builder setPromptMode(PromptMode promptMode) {
this.mPromptMode = promptMode; mPromptMode = promptMode;
return this; return this;
} }
public Builder setCallback(SimpleObserver<ResponseBody> callback) { public Builder setCallback(SimpleObserver<ResponseBody> callback) {
this.mCallback = callback; mCallback = callback;
return this; return this;
} }
public UpdateChecker build() { public UpdateChecker build() {
return new UpdateChecker(mContext, mView, mPromptMode, mCallback); return new UpdateChecker(mContext,
mView,
mGitHubUser,
mGitHubRepo,
mGitHubMainBranch,
mGitHubVersionProperties,
mGitHubChangelogFileProvider,
mPromptMode,
mCallback
);
} }
} }
@@ -743,26 +842,28 @@ public class UpdateChecker {
super(context); super(context);
this this
.title(versionInfo.getVersionName()) .title(versionInfo.getVersionName())
.options(List.of(new MaterialDialog.OptionMenuItemSpec(context.getString(R.string.dialog_button_ignore_current_update), parentDialog -> { .options(List.of(
new MaterialDialog.Builder(context) new MaterialDialog.OptionMenuItemSpec(context.getString(R.string.dialog_button_ignore_current_update), parentDialog -> {
.title(R.string.text_prompt) new MaterialDialog.Builder(context)
.content(R.string.prompt_add_ignored_version) .title(R.string.text_prompt)
.negativeText(R.string.dialog_button_cancel) .content(R.string.prompt_add_ignored_version)
.positiveText(R.string.dialog_button_confirm) .negativeText(R.string.dialog_button_cancel)
.positiveColorRes(R.color.dialog_button_warn) .positiveText(R.string.dialog_button_confirm)
.onPositive((tmpDialog, which) -> { .positiveColorRes(R.color.dialog_button_caution)
UpdateUtils.addIgnoredVersion(versionInfo); .onPositive((tmpDialog, which) -> {
ViewUtils.showToast(context, R.string.text_done); UpdateUtils.addIgnoredVersion(versionInfo);
parentDialog.dismiss(); ViewUtils.showToast(context, R.string.text_done);
}) parentDialog.dismiss();
.show(); })
}))) .show();
.content(R.string.text_getting_release_notes) }),
.neutralText(R.string.dialog_button_version_histories) new MaterialDialog.OptionMenuItemSpec(context.getString(R.string.dialog_button_version_histories), parentDialog -> {
.neutralColor(context.getColor(R.color.dialog_button_hint)) DisplayVersionHistoriesActivity.launch(context);
.onNeutral((dialog, which) -> { })
DisplayVersionHistoriesActivity.launch(context); ))
}) .content(R.string.text_retrieving_changelog)
.neutralText(R.string.dialog_button_view_with_browser)
.neutralColor(context.getColor(R.color.dialog_button_unavailable))
.negativeText(R.string.dialog_button_cancel) .negativeText(R.string.dialog_button_cancel)
.negativeColor(context.getColor(R.color.dialog_button_default)) .negativeColor(context.getColor(R.color.dialog_button_default))
.positiveText(R.string.dialog_button_update_now) .positiveText(R.string.dialog_button_update_now)
@@ -777,7 +878,7 @@ public class UpdateChecker {
} }
private static class GHub { private static class GitHubRepoUtils {
private static GitHub mGitHubConnection; private static GitHub mGitHubConnection;
@@ -897,15 +998,15 @@ public class UpdateChecker {
mGitHubAsset = gitHubAsset; mGitHubAsset = gitHubAsset;
} }
public void setAbi(String abi) {
mAbi = abi;
}
@Override @Override
public String getAbi() { public String getAbi() {
return mAbi; return mAbi;
} }
public void setAbi(String abi) {
mAbi = abi;
}
@Override @Override
public String getFileName() { public String getFileName() {
return mGitHubAsset.getName(); return mGitHubAsset.getName();
@@ -928,5 +1029,4 @@ public class UpdateChecker {
private static class ObservableEmptyException extends RuntimeException { private static class ObservableEmptyException extends RuntimeException {
/* Empty body. */ /* Empty body. */
} }
} }

View File

@@ -0,0 +1,48 @@
package org.autojs.autojs.network
import android.content.Context
import androidx.core.content.edit
import org.autojs.autojs.app.GlobalAppContext
import org.autojs.autojs6.R
object UpdateIgnoreStore {
private val context by lazy { GlobalAppContext.get() }
private val sp by lazy {
context.getSharedPreferences(context.getString(R.string.key_ignored_updates), Context.MODE_PRIVATE)
}
fun ignoreVersion(packageName: String, versionCode: Long) {
val set = getMutableStringSet(packageName).apply {
add(versionCode.toString())
}
sp.edit { putStringSet(key(packageName), set) }
}
fun unignoreVersion(packageName: String, versionCode: Long) {
val set = getMutableStringSet(packageName).apply {
remove(versionCode.toString())
}
sp.edit { putStringSet(key(packageName), set) }
}
fun isIgnored(packageName: String, versionCode: Long): Boolean {
val set = getStringSet(packageName)
return versionCode.toString() in set
}
fun ignoredVersionCodes(packageName: String): Set<Long> {
val set = getStringSet(packageName)
return set.mapNotNull { it.toLongOrNull() }.toSet()
}
private fun key(packageName: String) = "key_\$_ignored_plugin_\$_$packageName"
private fun getStringSet(packageName: String) =
sp.getStringSet(key(packageName), emptySet()) ?: emptySet()
private fun getMutableStringSet(packageName: String) =
sp.getStringSet(key(packageName), emptySet())?.toMutableSet() ?: mutableSetOf()
}

View File

@@ -7,33 +7,11 @@ import android.os.Environment;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.afollestad.materialdialogs.DialogAction; import com.afollestad.materialdialogs.DialogAction;
import com.afollestad.materialdialogs.MaterialDialog; import com.afollestad.materialdialogs.MaterialDialog;
import com.afollestad.materialdialogs.internal.MDButton; import com.afollestad.materialdialogs.internal.MDButton;
import org.autojs.autojs.concurrent.VolatileBox;
import org.autojs.autojs.network.UpdateChecker;
import org.autojs.autojs.network.api.DownloadApi;
import org.autojs.autojs.network.entity.VersionInfo;
import org.autojs.autojs.pio.PFiles;
import org.autojs.autojs.core.pref.Language;
import org.autojs.autojs.tool.SimpleObserver;
import org.autojs.autojs.util.StreamUtils;
import org.autojs.autojs.util.UpdateUtils;
import org.autojs.autojs.util.ViewUtils;
import org.autojs.autojs6.R;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.concurrent.ConcurrentHashMap;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable; import io.reactivex.disposables.Disposable;
@@ -44,9 +22,28 @@ import okhttp3.OkHttpClient;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
import okhttp3.ResponseBody; import okhttp3.ResponseBody;
import org.autojs.autojs.concurrent.VolatileBox;
import org.autojs.autojs.core.pref.Language;
import org.autojs.autojs.network.UpdateChecker;
import org.autojs.autojs.network.api.DownloadApi;
import org.autojs.autojs.network.entity.VersionInfo;
import org.autojs.autojs.pio.PFiles;
import org.autojs.autojs.tool.SimpleObserver;
import org.autojs.autojs.util.IntentUtils;
import org.autojs.autojs.util.IntentUtils.ToastExceptionHolder;
import org.autojs.autojs.util.StreamUtils;
import org.autojs.autojs.util.ViewUtils;
import org.autojs.autojs6.R;
import retrofit2.Retrofit; import retrofit2.Retrofit;
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.concurrent.ConcurrentHashMap;
/** /**
* Created by Stardust on Oct 20, 2017. * Created by Stardust on Oct 20, 2017.
*/ */
@@ -164,7 +161,7 @@ public class DownloadManager {
.positiveColorRes(R.color.dialog_button_caution) .positiveColorRes(R.color.dialog_button_caution)
.onPositive((d2, which2) -> { .onPositive((d2, which2) -> {
dialog.getActionButton(DialogAction.POSITIVE).performClick(); dialog.getActionButton(DialogAction.POSITIVE).performClick();
UpdateUtils.openUrl(context, url); IntentUtils.browse(context, url, new ToastExceptionHolder(context));
}) })
.cancelable(false) .cancelable(false)
.build() .build()

View File

@@ -123,7 +123,7 @@ class Ocr(private val scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime)
// @Overload // @Overload
// funcName(img: ImageWrapper, options?: DetectOptionsMLKit | DetectOptionsPaddle): org.autojs.autojs.runtime.api.OcrResult[]; // funcName(img: ImageWrapper, options?: DetectOptionsMLKit | DetectOptionsPaddle): org.autojs.autojs.runtime.api.OcrResult[];
// funcName(img: ImageWrapper, region: OmniRegion): org.autojs.autojs.runtime.api.OcrResult[]; // funcName(img: ImageWrapper, region: OmniRegion): org.autojs.autojs.runtime.api.OcrResult[];
dispatchOcrWith(scriptRuntime, funcName, arrayOf(img.oneShot(), arg1, arg2)) dispatchOcrWith(scriptRuntime, funcName, arrayOf(img.oneShot(), arg1, arg2), overrideMode, resultsHandler)
} }
arg0 !is ImageWrapper -> { arg0 !is ImageWrapper -> {
// @Signature // @Signature
@@ -135,7 +135,7 @@ class Ocr(private val scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime)
// funcName(img: ImageWrapper, region: OmniRegion): org.autojs.autojs.runtime.api.OcrResult[]; // funcName(img: ImageWrapper, region: OmniRegion): org.autojs.autojs.runtime.api.OcrResult[];
val capt = AugmentableImages.captureScreen(scriptRuntime, emptyArray()) val capt = AugmentableImages.captureScreen(scriptRuntime, emptyArray())
dispatchOcrWith(scriptRuntime, funcName, arrayOf(capt, arg0, arg1)) dispatchOcrWith(scriptRuntime, funcName, arrayOf(capt, arg0, arg1), overrideMode, resultsHandler)
} }
shouldTakenAsRegion(arg1) -> { shouldTakenAsRegion(arg1) -> {
@@ -147,7 +147,7 @@ class Ocr(private val scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime)
} }
// @Overload funcName(img: ImageWrapper, options: DetectOptionsMLKit | DetectOptionsPaddle): org.autojs.autojs.runtime.api.OcrResult[]; // @Overload funcName(img: ImageWrapper, options: DetectOptionsMLKit | DetectOptionsPaddle): org.autojs.autojs.runtime.api.OcrResult[];
dispatchOcrWith(scriptRuntime, funcName, arrayOf(arg0, options)) dispatchOcrWith(scriptRuntime, funcName, arrayOf(arg0, options), overrideMode, resultsHandler)
} }
else -> { else -> {

View File

@@ -14,6 +14,7 @@ import org.autojs.autojs.runtime.api.augment.ocr.Ocr.Companion.OcrMode
import org.autojs.autojs.runtime.exception.WrappedIllegalArgumentException import org.autojs.autojs.runtime.exception.WrappedIllegalArgumentException
import org.autojs.autojs.util.RhinoUtils.coerceBoolean import org.autojs.autojs.util.RhinoUtils.coerceBoolean
import org.autojs.autojs.util.RhinoUtils.coerceIntNumber import org.autojs.autojs.util.RhinoUtils.coerceIntNumber
import org.autojs.autojs6.R
import org.autojs.plugin.paddle.ocr.OcrOptions import org.autojs.plugin.paddle.ocr.OcrOptions
import org.mozilla.javascript.NativeArray import org.mozilla.javascript.NativeArray
import org.mozilla.javascript.NativeObject import org.mozilla.javascript.NativeObject
@@ -58,7 +59,7 @@ class OcrPaddle(private val scriptRuntime: ScriptRuntime) : Augmentable(scriptRu
} }
return runBlocking(scriptRuntime.coroutineContext) { return runBlocking(scriptRuntime.coroutineContext) {
val target = PaddleOcrPluginHost.select(globalContext) val target = PaddleOcrPluginHost.select(globalContext)
?: throw WrappedIllegalArgumentException("No Paddle OCR plugin matched") ?: throw WrappedIllegalArgumentException(globalContext.getString(R.string.error_no_paddle_ocr_plugins_available))
PaddleOcrPluginHost.recognizeText(globalContext, target, image.bitmap, ocrOptions) PaddleOcrPluginHost.recognizeText(globalContext, target, image.bitmap, ocrOptions)
} }
} }
@@ -73,7 +74,7 @@ class OcrPaddle(private val scriptRuntime: ScriptRuntime) : Augmentable(scriptRu
} }
return runBlocking(scriptRuntime.coroutineContext) { return runBlocking(scriptRuntime.coroutineContext) {
val target = PaddleOcrPluginHost.select(globalContext) val target = PaddleOcrPluginHost.select(globalContext)
?: throw WrappedIllegalArgumentException("No Paddle OCR plugin matched") ?: throw WrappedIllegalArgumentException(globalContext.getString(R.string.error_no_paddle_ocr_plugins_available))
PaddleOcrPluginHost.detect(globalContext, target, image.bitmap, ocrOptions) PaddleOcrPluginHost.detect(globalContext, target, image.bitmap, ocrOptions)
}.map { OcrResult(it.text, it.confidence, it.bounds) } }.map { OcrResult(it.text, it.confidence, it.bounds) }
} }

View File

@@ -32,8 +32,8 @@ import org.autojs.autojs.event.BackPressedHandler.DoublePressExit
import org.autojs.autojs.event.BackPressedHandler.HostActivity import org.autojs.autojs.event.BackPressedHandler.HostActivity
import org.autojs.autojs.extension.ViewExtensions.setOnTitleViewLongClickListener import org.autojs.autojs.extension.ViewExtensions.setOnTitleViewLongClickListener
import org.autojs.autojs.model.explorer.Explorers import org.autojs.autojs.model.explorer.Explorers
import org.autojs.autojs.permission.DisplayOverOtherAppsPermission
import org.autojs.autojs.permission.AllFilesAccessPermission import org.autojs.autojs.permission.AllFilesAccessPermission
import org.autojs.autojs.permission.DisplayOverOtherAppsPermission
import org.autojs.autojs.permission.PostNotificationsPermission import org.autojs.autojs.permission.PostNotificationsPermission
import org.autojs.autojs.service.ForegroundService import org.autojs.autojs.service.ForegroundService
import org.autojs.autojs.theme.ThemeColorManager import org.autojs.autojs.theme.ThemeColorManager
@@ -47,6 +47,7 @@ import org.autojs.autojs.ui.floating.FloatyWindowManger
import org.autojs.autojs.ui.log.LogActivity import org.autojs.autojs.ui.log.LogActivity
import org.autojs.autojs.ui.main.drawer.DrawerFragment.Companion.Event.OnDrawerClosed import org.autojs.autojs.ui.main.drawer.DrawerFragment.Companion.Event.OnDrawerClosed
import org.autojs.autojs.ui.main.drawer.DrawerFragment.Companion.Event.OnDrawerOpened import org.autojs.autojs.ui.main.drawer.DrawerFragment.Companion.Event.OnDrawerOpened
import org.autojs.autojs.ui.main.plugin.PluginFragment
import org.autojs.autojs.ui.main.scripts.ExplorerFragment import org.autojs.autojs.ui.main.scripts.ExplorerFragment
import org.autojs.autojs.ui.main.task.TaskManagerFragment import org.autojs.autojs.ui.main.task.TaskManagerFragment
import org.autojs.autojs.ui.settings.PreferencesActivity import org.autojs.autojs.ui.settings.PreferencesActivity
@@ -101,6 +102,9 @@ class MainActivity : BaseActivity(), DelegateHost, HostActivity {
val docsItemIndex: Int val docsItemIndex: Int
get() = findPageIndexByTitle(R.string.text_documentation) get() = findPageIndexByTitle(R.string.text_documentation)
val pluginsIndex: Int
get() = findPageIndexByTitle(R.string.text_plugins)
private fun findPageIndexByTitle(titleRes: Int): Int { private fun findPageIndexByTitle(titleRes: Int): Int {
var i = 0 var i = 0
while (i < mPagerAdapter.count) { while (i < mPagerAdapter.count) {
@@ -226,6 +230,7 @@ class MainActivity : BaseActivity(), DelegateHost, HostActivity {
mPagerAdapter = FragmentPagerAdapterBuilder(this) mPagerAdapter = FragmentPagerAdapterBuilder(this)
.add(ExplorerFragment(), R.string.text_file) .add(ExplorerFragment(), R.string.text_file)
.add(DocumentationFragment(), R.string.text_documentation) .add(DocumentationFragment(), R.string.text_documentation)
.add(PluginFragment(), R.string.text_plugins)
.add(TaskManagerFragment(), R.string.text_task) .add(TaskManagerFragment(), R.string.text_task)
.build() .build()
.apply { .apply {

View File

@@ -0,0 +1,263 @@
package org.autojs.autojs.ui.main.plugin;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import android.widget.TextView;
import androidx.annotation.AttrRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import io.reactivex.subjects.PublishSubject;
import org.autojs.autojs6.R;
/**
* Created by SuperMonster003 on Jan 17, 2026.
*/
public class PluginFloatingActionMenu extends FrameLayout implements View.OnClickListener {
private static final int[] ICONS = {
R.drawable.ic_add_black_48dp,
R.drawable.ic_add_black_48dp};
private static final int[] LABELS = {
R.string.text_install_from_local_file,
R.string.text_install_from_url};
private static final int ANIMATION_INTERVAL = 30;
private static final int ANIMATION_DURATION = 250;
private final Interpolator mInterpolator = new FastOutSlowInInterpolator();
private final PublishSubject<Boolean> mState = PublishSubject.create();
private View mOverlay = null;
private FloatingActionButton[] mFabs;
private View[] mFabContainers;
private boolean mExpanded = false;
private OnFloatingActionButtonClickListener mOnFloatingActionButtonClickListener;
private View mToggleFab;
public PluginFloatingActionMenu(@NonNull Context context) {
super(context);
init();
}
public PluginFloatingActionMenu(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public PluginFloatingActionMenu(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public boolean isExpanded() {
return mExpanded;
}
public PublishSubject<Boolean> getState() {
return mState;
}
public void expand() {
showOverlay();
setVisibility(VISIBLE);
int h = mFabs[0].getHeight();
for (int i = 0; i < mFabContainers.length; i++) {
animateY(mFabContainers[i], -(h + ANIMATION_INTERVAL) * (i + 1), null);
rotate(mFabs[i]);
}
mExpanded = true;
mState.onNext(true);
}
public void collapse() {
hideOverlay();
animateY(mFabContainers[0], 0, new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
setVisibility(INVISIBLE);
}
});
for (int i = 1; i < mFabContainers.length; i++) {
animateY(mFabContainers[i], 0, null);
rotate(mFabs[i]);
}
mExpanded = false;
mState.onNext(false);
}
public void setOnFloatingActionButtonClickListener(OnFloatingActionButtonClickListener listener) {
mOnFloatingActionButtonClickListener = listener;
}
public void setToggleFab(@Nullable View toggleFab) {
mToggleFab = toggleFab;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (heightMode == MeasureSpec.EXACTLY) {
return;
}
int h = mFabContainers[0].getMeasuredHeight();
setMeasuredDimension(getMeasuredWidth(), (h + ANIMATION_INTERVAL) * mFabs.length + h);
}
@Override
public void onClick(View v) {
collapse();
if (mOnFloatingActionButtonClickListener != null) {
mOnFloatingActionButtonClickListener.onClick((FloatingActionButton) v, (int) v.getTag());
}
}
private void init() {
buildFabs(ICONS, LABELS);
}
private void rotate(FloatingActionButton fab) {
fab.setRotation(0);
fab.animate()
.rotation(360)
.setDuration(ANIMATION_DURATION)
.setInterpolator(mInterpolator)
.start();
}
private void animateY(View view, float y, Animator.AnimatorListener l) {
view.animate()
.translationY(y)
.setDuration(ANIMATION_DURATION)
.setInterpolator(mInterpolator)
.setListener(l)
.start();
}
@SuppressWarnings("SameParameterValue")
private void buildFabs(int[] icons, int[] labels) {
if (icons.length != labels.length) {
throw new IllegalArgumentException("icons.length = " + icons.length + " is not equal to labels.length = " + labels.length);
}
mFabs = new FloatingActionButton[icons.length];
TextView[] mLabels = new TextView[icons.length];
mFabContainers = new View[icons.length];
LayoutInflater inflater = LayoutInflater.from(getContext());
for (int i = 0; i < icons.length; i++) {
mFabContainers[i] = inflater.inflate(R.layout.item_floating_action_menu, this, false);
mFabs[i] = mFabContainers[i].findViewById(R.id.floating_action_button);
mFabs[i].setImageResource(icons[i]);
mFabs[i].setOnClickListener(this);
mFabs[i].setTag(i);
mLabels[i] = mFabContainers[i].findViewById(R.id.label);
mLabels[i].setText(labels[i]);
addView(mFabContainers[i]);
}
}
private void showOverlay() {
if (mOverlay != null) {
if (mOverlay.getVisibility() != VISIBLE) {
mOverlay.setVisibility(View.VISIBLE);
}
return;
}
Context context = getContext();
if (!(context instanceof Activity activity)) return;
if (!(activity.findViewById(android.R.id.content) instanceof ViewGroup root)) return;
PluginFloatingActionMenu thisMenu = this;
mOverlay = new View(context) {
private final int[] loc = new int[2];
private final Rect menuRectOnScreen = new Rect();
private final Rect toggleFabRectOnScreen = new Rect();
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
thisMenu.getLocationOnScreen(loc);
menuRectOnScreen.set(
loc[0],
loc[1],
loc[0] + thisMenu.getWidth(),
loc[1] + thisMenu.getHeight()
);
boolean hasToggleFab = (mToggleFab != null) && mToggleFab.isShown();
if (hasToggleFab) {
mToggleFab.getLocationOnScreen(loc);
toggleFabRectOnScreen.set(
loc[0],
loc[1],
loc[0] + mToggleFab.getWidth(),
loc[1] + mToggleFab.getHeight()
);
} else {
toggleFabRectOnScreen.setEmpty();
}
int rawX = (int) event.getRawX();
int rawY = (int) event.getRawY();
boolean inToggleFab = hasToggleFab && toggleFabRectOnScreen.contains(rawX, rawY);
if (inToggleFab) {
MotionEvent forwarded = MotionEvent.obtain(event);
forwarded.offsetLocation(-toggleFabRectOnScreen.left, -toggleFabRectOnScreen.top);
mToggleFab.dispatchTouchEvent(forwarded);
forwarded.recycle();
return true;
}
boolean inMenu = menuRectOnScreen.contains(rawX, rawY);
if (inMenu) {
MotionEvent forwarded = MotionEvent.obtain(event);
forwarded.offsetLocation(-menuRectOnScreen.left, -menuRectOnScreen.top);
thisMenu.dispatchTouchEvent(forwarded);
forwarded.recycle();
return true;
}
// Collapse for external touches, avoid breaking button click chains.
// zh-CN: 对外部触摸进行收起, 避免破坏按钮点击链路.
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
collapse();
}
// Do not consume external events, pass them to lower layers such as lists for continued processing.
// zh-CN: 不消耗外部事件, 交给下层列表等继续处理.
return false;
}
};
LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
mOverlay.setLayoutParams(params);
mOverlay.setBackgroundColor(Color.TRANSPARENT);
root.addView(mOverlay);
}
private void hideOverlay() {
if (mOverlay != null) mOverlay.setVisibility(View.GONE);
}
public interface OnFloatingActionButtonClickListener {
void onClick(FloatingActionButton button, int pos);
}
}

View File

@@ -0,0 +1,186 @@
package org.autojs.autojs.ui.main.plugin
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.app.Activity
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.marginBottom
import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.tabs.TabLayout
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.coroutines.launch
import org.autojs.autojs.core.plugin.center.PluginCenterFragment
import org.autojs.autojs.core.plugin.center.PluginInstallActions
import org.autojs.autojs.core.plugin.center.PluginInstaller
import org.autojs.autojs.tool.SimpleObserver
import org.autojs.autojs.ui.main.MainActivity
import org.autojs.autojs.ui.main.QueryEvent
import org.autojs.autojs.ui.main.ViewPagerFragment
import org.autojs.autojs.ui.main.plugin.PluginFloatingActionMenu.OnFloatingActionButtonClickListener
import org.autojs.autojs.ui.widget.ScrollAwareFABBehavior
import org.autojs.autojs6.R
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
/**
* Created by SuperMonster003 on Jan 17, 2026.
*/
class PluginFragment : ViewPagerFragment(0), OnFloatingActionButtonClickListener {
private val mPickApkLauncher = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
uri ?: return@registerForActivityResult
lifecycleScope.launch {
PluginInstaller.installFromFileUriWithPrompt(requireContext(), uri)
}
}
private var mFloatingActionMenu: PluginFloatingActionMenu? = null
private var mIsCurrentPagePlugins = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
EventBus.getDefault().register(this)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.fragment_plugin, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (childFragmentManager.findFragmentByTag(TAG_PLUGIN_CENTER) == null) {
childFragmentManager.commit {
replace(R.id.plugin_center_container, PluginCenterFragment(), TAG_PLUGIN_CENTER)
}
}
(activity as? MainActivity)?.apply {
val tabLayout: TabLayout = findViewById(R.id.tab)
val pluginsTag = tabLayout.getTabAt(pluginsIndex)
pluginsTag?.view?.let { setTabViewClickListeners(it) }
}
}
private fun setTabViewClickListeners(tabView: TabLayout.TabView) {
tabView.setOnLongClickListener { if (mIsCurrentPagePlugins) true.also { toggleFabVisibility() } else false }
}
private fun toggleFabVisibility() {
val behavior = (fab.layoutParams as? CoordinatorLayout.LayoutParams)?.behavior as? ScrollAwareFABBehavior
when {
behavior == null || fab.translationY == 0f -> {
if (fab.isShown) fab.hide() else fab.show()
}
else -> fab.animate()
.translationY(0f)
.setDuration(ScrollAwareFABBehavior.DURATION)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
behavior.setHidden(false)
}
})
.start()
}
}
override fun onFabClick(fab: FloatingActionButton) {
initFloatingActionMenuIfNeeded(fab).run { if (isExpanded) collapse() else expand() }
}
override fun onBackPressed(activity: Activity) = false
private fun initFloatingActionMenuIfNeeded(fab: FloatingActionButton): PluginFloatingActionMenu {
return mFloatingActionMenu ?: requireActivity().findViewById<PluginFloatingActionMenu>(R.id.plugin_floating_action_menu).also { menu ->
menu.state
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : SimpleObserver<Boolean?>() {
override fun onNext(expanding: Boolean) {
fab.animate()
.rotation((if (expanding) 45 else 0).toFloat())
.setDuration(300)
.start()
}
})
menu.setOnFloatingActionButtonClickListener(this)
menu.layoutParams.runCatching {
javaClass.getField("bottomMargin").setInt(this, fab.marginBottom)
}
menu.setToggleFab(fab)
mFloatingActionMenu = menu
}
}
override fun onPageShow() {
super.onPageShow()
mIsCurrentPagePlugins = true
}
override fun onPageHide() {
super.onPageHide()
mFloatingActionMenu?.let { if (it.isExpanded) it.collapse() }
mIsCurrentPagePlugins = false
}
@Subscribe
fun onQuerySummit(event: QueryEvent) {
if (!isShown) {
return
}
val child = childFragmentManager.findFragmentByTag(TAG_PLUGIN_CENTER) as? PluginCenterFragment ?: return
if (event === QueryEvent.CLEAR) {
child.setQuery(null)
return
}
if (event === QueryEvent.FIND_FORWARD) {
return
}
if (event === QueryEvent.FIND_BACKWARD) {
return
}
child.setQuery(event.query)
}
override fun onDestroy() {
super.onDestroy()
EventBus.getDefault().unregister(this)
}
override fun onDestroyView() {
super.onDestroyView()
mFloatingActionMenu = null
}
override fun onDetach() {
super.onDetach()
mFloatingActionMenu?.setOnFloatingActionButtonClickListener(null)
}
override fun onClick(button: FloatingActionButton, pos: Int) {
when (pos) {
1 -> {
PluginInstallActions.showInstallFromUrlDialog(requireContext(), lifecycleScope)
}
0 -> {
PluginInstallActions.installFromLocalFile(mPickApkLauncher)
}
else -> Unit
}
}
companion object {
private const val TAG_PLUGIN_CENTER = "plugin_center"
}
}

View File

@@ -77,8 +77,8 @@ class ExplorerFragment : ViewPagerFragment(0), OnFloatingActionButtonClickListen
} }
(activity as? MainActivity)?.apply { (activity as? MainActivity)?.apply {
val tabLayout: TabLayout = findViewById(R.id.tab) val tabLayout: TabLayout = findViewById(R.id.tab)
val docsTab = tabLayout.getTabAt(filesItemIndex) val filesTab = tabLayout.getTabAt(filesItemIndex)
docsTab?.view?.let { setTabViewClickListeners(it) } filesTab?.view?.let { setTabViewClickListeners(it) }
} }
restoreViewStates() restoreViewStates()
} }
@@ -216,19 +216,19 @@ class ExplorerFragment : ViewPagerFragment(0), OnFloatingActionButtonClickListen
override fun onClick(button: FloatingActionButton, pos: Int) { override fun onClick(button: FloatingActionButton, pos: Int) {
mExplorerView?.let { view -> mExplorerView?.let { view ->
when (pos) { when (pos) {
0 -> ScriptOperations(context, view, view.currentPage)
.newDirectory()
1 -> ScriptOperations(context, view, view.currentPage)
.newFile()
2 -> ScriptOperations(context, view, view.currentPage)
.importFile()
3 -> context?.startActivity( 3 -> context?.startActivity(
Intent(context, ProjectConfigActivity::class.java) Intent(context, ProjectConfigActivity::class.java)
.putExtra(ProjectConfigActivity.EXTRA_PARENT_DIRECTORY, view.currentPage.path) .putExtra(ProjectConfigActivity.EXTRA_PARENT_DIRECTORY, view.currentPage.path)
.putExtra(ProjectConfigActivity.EXTRA_NEW_PROJECT, true) .putExtra(ProjectConfigActivity.EXTRA_NEW_PROJECT, true)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
) )
else -> {} 2 -> ScriptOperations(context, view, view.currentPage)
.importFile()
1 -> ScriptOperations(context, view, view.currentPage)
.newFile()
0 -> ScriptOperations(context, view, view.currentPage)
.newDirectory()
else -> Unit
} }
} }
} }

View File

@@ -163,6 +163,7 @@ open class AboutActivity : BaseActivity() {
private fun checkForUpdates() { private fun checkForUpdates() {
UpdateChecker.Builder(this) UpdateChecker.Builder(this)
.setPromptMode(PromptMode.DIALOG) .setPromptMode(PromptMode.DIALOG)
.setGitHubMainBranch("master")
.build().checkNow() .build().checkNow()
} }

View File

@@ -33,6 +33,7 @@ class CheckForUpdatesPreference : MaterialPreference, OnSharedPreferenceChangeLi
override fun onClick() { override fun onClick() {
UpdateChecker.Builder(prefContext) UpdateChecker.Builder(prefContext)
.setPromptMode(PromptMode.DIALOG) .setPromptMode(PromptMode.DIALOG)
.setGitHubMainBranch("master")
.build().checkNow() .build().checkNow()
super.onClick() super.onClick()
} }

View File

@@ -22,6 +22,7 @@ class CheckForUpdatesWithLocalVersionIgnoredPreference : MaterialPreference {
override fun onClick() { override fun onClick() {
UpdateChecker.Builder(context) UpdateChecker.Builder(context)
.setPromptMode(PromptMode.DIALOG) .setPromptMode(PromptMode.DIALOG)
.setGitHubMainBranch("master")
.build() .build()
.checkNow(true) .checkNow(true)
super.onClick() super.onClick()

View File

@@ -1006,6 +1006,10 @@ object ViewUtils {
toolbar.setNavigationIconColorByColorLuminance(context, aimColor) toolbar.setNavigationIconColorByColorLuminance(context, aimColor)
} }
fun Toolbar.setTitlesTextColorByThemeColorLuminance(context: Context) {
this.setTitlesTextColorByColorLuminance(context, ThemeColorManager.colorPrimary)
}
fun Toolbar.setTitlesTextColorByColorLuminance(context: Context, aimColor: Int) { fun Toolbar.setTitlesTextColorByColorLuminance(context: Context, aimColor: Int) {
val color = getDayOrNightColorByLuminance(context, aimColor) val color = getDayOrNightColorByLuminance(context, aimColor)
setTitleTextColor(color) setTitleTextColor(color)

View File

@@ -69,7 +69,19 @@
android:clipToPadding="false" android:clipToPadding="false"
android:visibility="invisible" android:visibility="invisible"
app:layout_anchor="@id/viewpager" app:layout_anchor="@id/viewpager"
app:layout_anchorGravity="bottom|end" /> app:layout_anchorGravity="bottom|end"/>
<org.autojs.autojs.ui.main.plugin.PluginFloatingActionMenu
android:id="@+id/plugin_floating_action_menu"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="16dp"
android:clipChildren="false"
android:clipToPadding="false"
android:visibility="invisible"
app:layout_anchor="@id/viewpager"
app:layout_anchorGravity="bottom|end"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<org.autojs.autojs.theme.widget.ThemeColorSwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/plugin_swipe_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:enabled="false"
tools:visibility="visible">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/plugin_center_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</org.autojs.autojs.theme.widget.ThemeColorSwipeRefreshLayout>

View File

@@ -27,7 +27,10 @@
android:id="@+id/action_search" android:id="@+id/action_search"
android:icon="@drawable/ic_search_smaller_black_48dp" android:icon="@drawable/ic_search_smaller_black_48dp"
android:title="@string/text_search" android:title="@string/text_search"
app:showAsAction="always" /> android:imeOptions="actionSearch"
android:inputType="text"
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="always|collapseActionView" />
<item <item
android:id="@+id/action_sort" android:id="@+id/action_sort"

View File

@@ -1174,5 +1174,12 @@
<string name="text_write_secure_settings">اكتب إعدادات الأمان</string> <string name="text_write_secure_settings">اكتب إعدادات الأمان</string>
<string name="text_write_system_settings">كتابة إعدادات النظام</string> <string name="text_write_system_settings">كتابة إعدادات النظام</string>
<string name="text_xiaomi_background_popup_permission">النوافذ المنبثقة في الخلفية</string> <string name="text_xiaomi_background_popup_permission">النوافذ المنبثقة في الخلفية</string>
<string name="error_no_paddle_ocr_plugins_available">لم يتم العثور على أي مكونات Paddle OCR إضافية متاحة</string>
<string name="text_installed">مثبّت</string>
<string name="text_not_installed">غير مثبّت</string>
<string name="text_all">الكل</string>
<string name="text_sort_by_name">فرز حسب الاسم</string>
<string name="text_sort_by_last_update_time">فرز حسب آخر تحديث</string>
<string name="text_sort_by_package_size">فرز حسب حجم الحزمة</string>
</resources> </resources>

View File

@@ -1169,5 +1169,12 @@
<string name="text_write_secure_settings">Write security settings</string> <string name="text_write_secure_settings">Write security settings</string>
<string name="text_write_system_settings">Write system settings</string> <string name="text_write_system_settings">Write system settings</string>
<string name="text_xiaomi_background_popup_permission">Display pop-up windows while running in the background</string> <string name="text_xiaomi_background_popup_permission">Display pop-up windows while running in the background</string>
<string name="error_no_paddle_ocr_plugins_available">No Paddle OCR plugins available</string>
<string name="text_installed">Installed</string>
<string name="text_not_installed">Not installed</string>
<string name="text_all">All</string>
<string name="text_sort_by_name">Sort by name</string>
<string name="text_sort_by_last_update_time">Sort by last update time</string>
<string name="text_sort_by_package_size">Sort by package size</string>
</resources> </resources>

View File

@@ -1172,5 +1172,12 @@
<string name="text_write_secure_settings">Escribir la configuración de seguridad</string> <string name="text_write_secure_settings">Escribir la configuración de seguridad</string>
<string name="text_write_system_settings">Escribir la configuración del sistema</string> <string name="text_write_system_settings">Escribir la configuración del sistema</string>
<string name="text_xiaomi_background_popup_permission">Ventanas emergentes en segundo plano</string> <string name="text_xiaomi_background_popup_permission">Ventanas emergentes en segundo plano</string>
<string name="error_no_paddle_ocr_plugins_available">No se encontraron plugins de Paddle OCR disponibles</string>
<string name="text_installed">Instalado</string>
<string name="text_not_installed">No instalado</string>
<string name="text_all">Todos</string>
<string name="text_sort_by_name">Ordenar por nombre</string>
<string name="text_sort_by_last_update_time">Ordenar por última actualización</string>
<string name="text_sort_by_package_size">Ordenar por tamaño del paquete</string>
</resources> </resources>

View File

@@ -1172,5 +1172,12 @@
<string name="text_write_secure_settings">Écrire les paramètres de sécurité</string>. <string name="text_write_secure_settings">Écrire les paramètres de sécurité</string>.
<string name="text_write_system_settings">Écrire les paramètres système</string> <string name="text_write_system_settings">Écrire les paramètres système</string>
<string name="text_xiaomi_background_popup_permission">Fenêtres contextuelles en arrière-plan</string> <string name="text_xiaomi_background_popup_permission">Fenêtres contextuelles en arrière-plan</string>
<string name="error_no_paddle_ocr_plugins_available">Aucun plugin Paddle OCR disponible</string>
<string name="text_installed">Installé</string>
<string name="text_not_installed">Non installé</string>
<string name="text_all">Tous</string>
<string name="text_sort_by_name">Trier par nom</string>
<string name="text_sort_by_last_update_time">Trier par dernière mise à jour</string>
<string name="text_sort_by_package_size">Trier par taille du paquet</string>
</resources> </resources>

View File

@@ -1173,5 +1173,12 @@
<string name="text_write_secure_settings">セキュリティ設定の書き込み</string> <string name="text_write_secure_settings">セキュリティ設定の書き込み</string>
<string name="text_write_system_settings">システム設定の書き込み</string> <string name="text_write_system_settings">システム設定の書き込み</string>
<string name="text_xiaomi_background_popup_permission">バックグラウンドでのポップアップ表示</string> <string name="text_xiaomi_background_popup_permission">バックグラウンドでのポップアップ表示</string>
<string name="error_no_paddle_ocr_plugins_available">利用可能な Paddle OCR プラグインが見つかりません</string>
<string name="text_installed">インストール済み</string>
<string name="text_not_installed">未インストール</string>
<string name="text_all">すべて</string>
<string name="text_sort_by_name">名前で並べ替え</string>
<string name="text_sort_by_last_update_time">最終更新日で並べ替え</string>
<string name="text_sort_by_package_size">パッケージサイズで並べ替え</string>
</resources> </resources>

View File

@@ -1174,5 +1174,12 @@
<string name="text_write_secure_settings">보안 설정을 작성하십시오</string> <string name="text_write_secure_settings">보안 설정을 작성하십시오</string>
<string name="text_write_system_settings">시스템 설정을 작성하십시오</string> <string name="text_write_system_settings">시스템 설정을 작성하십시오</string>
<string name="text_xiaomi_background_popup_permission">백그라운드 팝업</string> <string name="text_xiaomi_background_popup_permission">백그라운드 팝업</string>
<string name="error_no_paddle_ocr_plugins_available">사용 가능한 Paddle OCR 플러그인을 찾을 수 없습니다</string>
<string name="text_installed">설치됨</string>
<string name="text_not_installed">설치되지 않음</string>
<string name="text_all">전체</string>
<string name="text_sort_by_name">이름순 정렬</string>
<string name="text_sort_by_last_update_time">최근 업데이트순 정렬</string>
<string name="text_sort_by_package_size">패키지 크기순 정렬</string>
</resources> </resources>

View File

@@ -1172,5 +1172,12 @@
<string name="text_write_secure_settings">Параметры безопасности записи</string> <string name="text_write_secure_settings">Параметры безопасности записи</string>
<string name="text_write_system_settings">Запись системных настроек</string> <string name="text_write_system_settings">Запись системных настроек</string>
<string name="text_xiaomi_background_popup_permission">Всплывающие окна в фоне</string> <string name="text_xiaomi_background_popup_permission">Всплывающие окна в фоне</string>
<string name="error_no_paddle_ocr_plugins_available">Доступные плагины Paddle OCR не найдены</string>
<string name="text_installed">Установлено</string>
<string name="text_not_installed">Не установлено</string>
<string name="text_all">Все</string>
<string name="text_sort_by_name">Сортировать по имени</string>
<string name="text_sort_by_last_update_time">Сортировать по времени последнего обновления</string>
<string name="text_sort_by_package_size">Сортировать по размеру пакета</string>
</resources> </resources>

View File

@@ -1170,5 +1170,12 @@
<string name="text_write_secure_settings">修改安全設置</string> <string name="text_write_secure_settings">修改安全設置</string>
<string name="text_write_system_settings">修改系統設置</string> <string name="text_write_system_settings">修改系統設置</string>
<string name="text_xiaomi_background_popup_permission">後台彈出界面</string> <string name="text_xiaomi_background_popup_permission">後台彈出界面</string>
<string name="error_no_paddle_ocr_plugins_available">未找到可用的 Paddle OCR 插件</string>
<string name="text_installed">已安裝</string>
<string name="text_not_installed">未安裝</string>
<string name="text_all">全部</string>
<string name="text_sort_by_name">按名稱排序</string>
<string name="text_sort_by_last_update_time">按最近更新排序</string>
<string name="text_sort_by_package_size">按安裝包大小排序</string>
</resources> </resources>

View File

@@ -1170,5 +1170,12 @@
<string name="text_write_secure_settings">修改安全設定</string> <string name="text_write_secure_settings">修改安全設定</string>
<string name="text_write_system_settings">修改系統設定</string> <string name="text_write_system_settings">修改系統設定</string>
<string name="text_xiaomi_background_popup_permission">後臺彈出介面</string> <string name="text_xiaomi_background_popup_permission">後臺彈出介面</string>
<string name="error_no_paddle_ocr_plugins_available">未找到可用的 Paddle OCR 外掛</string>
<string name="text_installed">已安裝</string>
<string name="text_not_installed">未安裝</string>
<string name="text_all">全部</string>
<string name="text_sort_by_name">按名稱排序</string>
<string name="text_sort_by_last_update_time">按最近更新排序</string>
<string name="text_sort_by_package_size">按安裝包大小排序</string>
</resources> </resources>

View File

@@ -1170,5 +1170,12 @@
<string name="text_write_secure_settings">修改安全设置</string> <string name="text_write_secure_settings">修改安全设置</string>
<string name="text_write_system_settings">修改系统设置</string> <string name="text_write_system_settings">修改系统设置</string>
<string name="text_xiaomi_background_popup_permission">后台弹出界面</string> <string name="text_xiaomi_background_popup_permission">后台弹出界面</string>
<string name="error_no_paddle_ocr_plugins_available">未找到可用的 Paddle OCR 插件</string>
<string name="text_installed">已安装</string>
<string name="text_not_installed">未安装</string>
<string name="text_all">全部</string>
<string name="text_sort_by_name">按名称排序</string>
<string name="text_sort_by_last_update_time">按最近更新排序</string>
<string name="text_sort_by_package_size">按安装包大小排序</string>
</resources> </resources>

View File

@@ -1427,5 +1427,12 @@
<string name="text_write_secure_settings">Write security settings</string> <string name="text_write_secure_settings">Write security settings</string>
<string name="text_write_system_settings">Write system settings</string> <string name="text_write_system_settings">Write system settings</string>
<string name="text_xiaomi_background_popup_permission">Display pop-up windows while running in the background</string> <string name="text_xiaomi_background_popup_permission">Display pop-up windows while running in the background</string>
<string name="error_no_paddle_ocr_plugins_available">No Paddle OCR plugins available</string>
<string name="text_installed">Installed</string>
<string name="text_not_installed">Not installed</string>
<string name="text_all">All</string>
<string name="text_sort_by_name">Sort by name</string>
<string name="text_sort_by_last_update_time">Sort by last update time</string>
<string name="text_sort_by_package_size">Sort by package size</string>
</resources> </resources>

View File

@@ -1,5 +1,5 @@
#Fri Jan 16 23:14:30 CST 2026 #Sat Jan 17 15:22:28 CST 2026
BUILD_TIME=1768576470633 BUILD_TIME=1768634548657
COMPILE_SDK_VERSION=36 COMPILE_SDK_VERSION=36
IMAGE_QUANT_CMAKE_VERSION=3.22.1 IMAGE_QUANT_CMAKE_VERSION=3.22.1
IMAGE_QUANT_NDK_VERSION=26.1.10909125 IMAGE_QUANT_NDK_VERSION=26.1.10909125
@@ -27,6 +27,6 @@ RAPID_OCR_OPENCV_MOBILE_LABEL_VERSION=13
RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3 RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3
TARGET_SDK_VERSION=36 TARGET_SDK_VERSION=36
TARGET_SDK_VERSION_INRT=29 TARGET_SDK_VERSION_INRT=29
VERSION_BUILD=3622 VERSION_BUILD=3623
VERSION_NAME=6.7.0 Alpha14 VERSION_NAME=6.7.0 Alpha15
VSCODE_EXT_REQUIRED_VERSION=1.0.13 VSCODE_EXT_REQUIRED_VERSION=1.0.13