6.7.0 - Alpha15 - 插件中心 M2 - 主页增加 "插件" 标签页; 插件中心支持搜索/筛选/排序; 版本忽略功能框架 (未完成)
This commit is contained in:
@@ -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))",
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)
|
||||||
@@ -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(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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?,
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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. */
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 -> {
|
||||||
|
|
||||||
|
|||||||
@@ -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) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
16
app/src/main/res/layout/fragment_plugin.xml
Normal file
16
app/src/main/res/layout/fragment_plugin.xml
Normal 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>
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user