diff --git a/.changelog/lang_zh-Hans.json b/.changelog/lang_zh-Hans.json index c0c077ad..49265740 100644 --- a/.changelog/lang_zh-Hans.json +++ b/.changelog/lang_zh-Hans.json @@ -1,9 +1,9 @@ { "$data": { "v6.7.0": { - "released_date": "2026/01/14", + "released_date": "2026/01/17", "feature": [ - "插件中心功能, 支持插件的安装/卸载/更新等操作 (入口: 主页抽屉按钮)", + "插件中心功能, 支持插件的安装/卸载/更新等操作 (入口: 主页抽屉按钮/主页标签页)", "cvt 模块, 用于数据单位转换 (参阅 项目文档 > [单位转换](https://docs.autojs6.com/#/cvt))", "fmt 模块, 用于数据格式化 (参阅 项目文档 > [格式化](https://docs.autojs6.com/#/fmt))", "zip 模块, 用于文件压缩与解压缩操作 (Ref to [Auto.js Pro](https://g.pro.autojs.org/)) (参阅 项目文档 > [Zip](https://docs.autojs6.com/#/zip))", diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterActivity.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterActivity.kt index 290eea50..d3a18120 100644 --- a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterActivity.kt +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterActivity.kt @@ -7,15 +7,17 @@ import android.os.Bundle import android.view.Menu import android.view.MenuItem import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.widget.SearchView import androidx.lifecycle.lifecycleScope -import com.afollestad.materialdialogs.DialogAction import com.afollestad.materialdialogs.MaterialDialog import kotlinx.coroutines.launch -import org.autojs.autojs.extension.MaterialDialogExtensions.widgetThemeColor 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.onceGlobalLayout import org.autojs.autojs.util.ViewUtils.setMenuIconsColorByThemeColorLuminance import org.autojs.autojs.util.ViewUtils.setNavigationIconColorByThemeColorLuminance +import org.autojs.autojs.util.ViewUtils.setTitlesTextColorByThemeColorLuminance import org.autojs.autojs6.R import org.autojs.autojs6.databinding.ActivityPluginCenterBinding @@ -24,6 +26,8 @@ class PluginCenterActivity : BaseActivity() { private lateinit var binding: ActivityPluginCenterBinding + private var mSearchViewItem: SearchViewItem? = null + private val pickApkLauncher = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> uri ?: return@registerForActivityResult lifecycleScope.launch { @@ -48,73 +52,34 @@ class PluginCenterActivity : BaseActivity() { override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.menu_plugin_center, menu) + setUpSearchMenuItem(menu) setUpToolbarColors() return true } override fun onOptionsItemSelected(item: MenuItem): Boolean { + val center = supportFragmentManager.findFragmentById(R.id.fragment_plugin_center) as? PluginCenterFragment + return when (item.itemId) { R.id.action_install_from_local_file -> { - pickApkLauncher.launch(arrayOf("application/vnd.android.package-archive")) + PluginInstallActions.installFromLocalFile(pickApkLauncher) true } R.id.action_install_from_url -> { - MaterialDialog.Builder(this) - .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() + PluginInstallActions.showInstallFromUrlDialog(this, lifecycleScope) true } R.id.action_search -> { - // TODO action_search - ViewUtils.showToast(this, R.string.text_under_development) - true + // Handled by SearchViewItem. + // zh-CN: 由 SearchViewItem 处理. + super.onOptionsItemSelected(item) } R.id.action_sort -> { - // TODO action_sort - ViewUtils.showToast(this, R.string.text_under_development) + showSortDialog(center) true } R.id.action_filter -> { - // TODO action_filter - ViewUtils.showToast(this, R.string.text_under_development) + showFilterDialog(center) true } 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() { binding.toolbar.setMenuIconsColorByThemeColorLuminance(this) binding.toolbar.setNavigationIconColorByThemeColorLuminance(this) + binding.toolbar.setTitlesTextColorByThemeColorLuminance(this) + mSearchViewItem?.setColorsByThemeColorLuminance() + } + + override fun onDestroy() { + super.onDestroy() + mSearchViewItem = null } companion object { diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterFragment.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterFragment.kt index d6af9151..2a9d5850 100644 --- a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterFragment.kt +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterFragment.kt @@ -39,6 +39,17 @@ class PluginCenterFragment : Fragment(R.layout.fragment_plugin_center) { private var isFirstEnter: Boolean = true + // Latest full list from ViewModel (unfiltered). + // zh-CN: 来自 ViewModel 的最新完整列表 (未过滤). + private var latestFullItems: List = 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?) { super.onViewCreated(view, savedInstanceState) _binding = FragmentPluginCenterBinding.bind(view) @@ -72,21 +83,7 @@ class PluginCenterFragment : Fragment(R.layout.fragment_plugin_center) { } override fun onUpdate(item: PluginCenterItem) { - val url = item.installableApkUrl - 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, - ) - } - } + PluginInfoDialogManager.showUpdatablePluginInfoDialog(contextRef, PluginInfoDialogManager.PluginInfoUpdatable(item)) } }) @@ -111,8 +108,8 @@ class PluginCenterFragment : Fragment(R.layout.fragment_plugin_center) { // zh-CN: 订阅列表数据. viewLifecycleOwner.lifecycleScope.launch { vm.items.collectLatest { list -> - adapter.updateData(list) - updateEmptyHint(list, vm.indexLoaded.value) + latestFullItems = list + renderList() } } @@ -121,7 +118,12 @@ class PluginCenterFragment : Fragment(R.layout.fragment_plugin_center) { viewLifecycleOwner.lifecycleScope.launch { vm.indexLoaded.collectLatest { loaded -> 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, 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 { it.isInstalled } + .thenByDescending { it.lastUpdateTime ?: 0L } + .thenBy { it.title.lowercase() } + ) + Sort.PACKAGE_SIZE_DESC -> filtered.sortedWith( + compareByDescending { 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, + indexLoaded: Boolean, + fullItems: List, + query: String?, + ) { val hintView = binding.pluginCenterEmptyHint + val hasQuery = !query.isNullOrBlank() + when { - items.isNotEmpty() -> { + filteredItems.isNotEmpty() -> { // Has data: hide hint immediately and cancel any waiting tasks. // zh-CN: 有数据: 立即隐藏提示, 并取消任何等待任务. emptyHintJob?.cancel() @@ -152,9 +238,17 @@ class PluginCenterFragment : Fragment(R.layout.fragment_plugin_center) { hintView.visibility = View.GONE 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 -> { // Local and index stages have ended, list is still empty, immediately show "no plugins" hint. - // zh-CN: 本地与索引阶段已结束, 列表仍为空, 立即显示"没有插件"提示. + // zh-CN: 本地与索引阶段已结束, 列表仍为空, 立即显示 "没有插件" 提示. emptyHintJob?.cancel() emptyHintJob = null hintView.visibility = View.VISIBLE @@ -249,4 +343,23 @@ class PluginCenterFragment : Fragment(R.layout.fragment_plugin_center) { _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, + } + } diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterItem.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterItem.kt index 927784be..5c2cda4d 100644 --- a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterItem.kt +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterItem.kt @@ -15,9 +15,17 @@ data class PluginCenterItem( val versionName: String, val versionCode: Long? = null, val versionDate: String? = null, + var updatableVersionName: String? = null, var updatableVersionCode: Long? = 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 collaborators: List = emptyList(), val description: String? = null, @@ -53,7 +61,7 @@ data class PluginCenterItem( } val isUpdatable: Boolean - get() = updatableVersionName != null + get() = updatableVersionCode != null var lastInstallTime: Long? get() = PluginRecentStore.getLastInstalled(packageName) diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterViewModel.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterViewModel.kt index cc5b67fa..24e6da78 100644 --- a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterViewModel.kt +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterViewModel.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch +import org.autojs.autojs.network.UpdateIgnoreStore import org.autojs.autojs6.R /** @@ -28,12 +29,17 @@ import org.autojs.autojs6.R * - 先加载本地插件并立即推送列表 (本地优先). * - 紧接着在后台加载索引, 成功后与本地合并再推送一次. * - 若本地发现失败, 通过 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() { - private val indexRepo = PluginIndexRepository() + // TODO by SuperMonster003 on Jan 17, 2026. + // private val indexRepo = PluginIndexRepository() + private val installedRepo = InstalledPluginRepository() - private val enableStore = PluginEnableStore() + private val enableStore = PluginEnableStore private val _items = MutableStateFlow>(emptyList()) val items: StateFlow> = _items @@ -96,7 +102,9 @@ class PluginCenterViewModel : ViewModel() { // Asynchronously load index and merge. // zh-CN: 异步加载索引并合并. val indexEntries = runCatching { - indexRepo.fetchOfficialIndex(context, forceRefresh = forceRefreshIndex) + // TODO by SuperMonster003 on Jan 17, 2026. + // indexRepo.fetchOfficialIndex(context, forceRefresh = forceRefreshIndex) + emptyList() }.onFailure { // Index fetch exception is not fatal, just log it. // zh-CN: 索引获取异常不算致命, 使用日志记录即可. @@ -135,6 +143,11 @@ class PluginCenterViewModel : ViewModel() { 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? { val packageName = local?.packageName ?: index?.packageName if (packageName.isNullOrBlank()) return null @@ -144,18 +157,21 @@ class PluginCenterViewModel : ViewModel() { val author = local?.author ?: index?.author 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 isInstalled = local != null - // Only mark as updatable when "installed and index version is higher", and fill in updatable target information. - // zh-CN: 仅当 "已安装且索引版本更高" 时, 标记可更新, 并填充可更新目标信息. - val (updatableName, updatableCode, updatableDate) = run { - val defaultVersionInfo = Triple(null, null, null) - versionCodeLocal ?: return@run defaultVersionInfo - val versionCodeIndex = index?.versionCode ?: return@run defaultVersionInfo - if (versionCodeIndex <= versionCodeLocal) return@run defaultVersionInfo - Triple(index.versionName, index.versionCode, index.versionDate) + // 未安装时: installable 取 releases 最新 (第一个). + val latestRelease = index?.releases?.maxByOrNull { it.versionCode } + + // 已安装时: updatable 取 "大于 installed 且未忽略" 的最高版本. + val targetUpdate = run { + if (!isInstalled || versionCodeLocal == null) return@run null + val candidates = index?.releases + ?.filter { it.versionCode > versionCodeLocal } + ?.sortedByDescending { it.versionCode } + ?: emptyList() + candidates.firstOrNull { !UpdateIgnoreStore.isIgnored(packageName, it.versionCode) } } val enabled = enableStore.isEnabled(context, packageName, defaultEnabled = isInstalled) @@ -164,18 +180,28 @@ class PluginCenterViewModel : ViewModel() { title = title, packageName = packageName, versionName = versionNameLocal, - versionCode = versionCodeLocal ?: index?.versionCode, - versionDate = index?.versionDate, - updatableVersionName = updatableName, - updatableVersionCode = updatableCode, - updatableVersionDate = updatableDate, + versionCode = versionCodeLocal ?: latestRelease?.versionCode, + versionDate = latestRelease?.versionDate, + + updatableVersionName = targetUpdate?.versionName, + updatableVersionCode = targetUpdate?.versionCode, + updatableVersionDate = targetUpdate?.versionDate, + updatableApkUrl = targetUpdate?.apkUrl, + updatableApkSha256 = targetUpdate?.apkSha256, + updatableApkSizeBytes = targetUpdate?.apkSizeBytes, + updatableChangelogUrl = targetUpdate?.changelogUrl, + updatableChangelogText = targetUpdate?.changelogText, + author = author, collaborators = collaborators, description = description, - packageSize = local?.packageSize ?: 0, - installableApkUrl = index?.apkUrl, - installableApkSha256 = index?.apkSha256, - installableApkSizeBytes = index?.apkSizeBytes, + + packageSize = local?.packageSize ?: 0L, + + installableApkUrl = latestRelease?.apkUrl, + installableApkSha256 = latestRelease?.apkSha256, + installableApkSizeBytes = latestRelease?.apkSizeBytes, + // TODO 已安装优先用应用图标; 未安装走默认占位图. icon = local?.icon, isEnabled = enabled, diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginEnableStore.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginEnableStore.kt index 131c9c73..09dfc935 100644 --- a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginEnableStore.kt +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginEnableStore.kt @@ -3,17 +3,17 @@ package org.autojs.autojs.core.plugin.center import android.content.Context 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 { - val sp = context.getSharedPreferences(spName, Context.MODE_PRIVATE) + val sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE) return sp.getBoolean(key(packageName), defaultEnabled) } 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) } } diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginIndexEntry.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginIndexEntry.kt index af3fe0d5..5e27c887 100644 --- a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginIndexEntry.kt +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginIndexEntry.kt @@ -19,13 +19,7 @@ data class PluginIndexEntry( /** @sample "paddle-ocr-pp-ocrv5" */ val engineId: String? = null, - val versionName: String, - val versionCode: Long? = null, - val versionDate: String? = null, - - val apkUrl: String? = null, - val apkSha256: String? = null, - val apkSizeBytes: Long? = null, + val releases: List = emptyList(), val tags: List = emptyList(), ) diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginIndexRelease.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginIndexRelease.kt new file mode 100644 index 00000000..2a9b3f0c --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginIndexRelease.kt @@ -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, +) diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginIndexRepository.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginIndexRepository.kt index bf97edb2..78659992 100644 --- a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginIndexRepository.kt +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginIndexRepository.kt @@ -30,7 +30,7 @@ class PluginIndexRepository { private const val KEY_LAST_FAILURE_TS = "last_failure_ts" 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 MAX_RETRY_INTERVAL_MS = 10 * 60_000L // 10 min @@ -266,12 +266,16 @@ class PluginIndexRepository { engine = engine, variant = variant, engineId = engineId, - versionName = versionName, - versionCode = versionCode, - versionDate = versionDate, - apkUrl = apkUrl, - apkSha256 = apkSha256, - apkSizeBytes = apkSize, + releases = listOf( + PluginIndexRelease( + versionName = versionName, + versionCode = versionCode ?: 0L, + versionDate = versionDate, + apkUrl = apkUrl, + apkSha256 = apkSha256, + apkSizeBytes = apkSize, + ), + ), tags = emptyList(), ) } diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginInfoDialogManager.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginInfoDialogManager.kt index 92c47e56..132d1372 100644 --- a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginInfoDialogManager.kt +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginInfoDialogManager.kt @@ -3,6 +3,7 @@ package org.autojs.autojs.core.plugin.center import android.annotation.SuppressLint import android.content.Context import android.graphics.PorterDuff +import android.graphics.drawable.Drawable import android.view.LayoutInflater import android.view.View.MeasureSpec.UNSPECIFIED 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.DisplayUtils 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.toCircular import org.autojs.autojs6.R @@ -59,20 +61,22 @@ object PluginInfoDialogManager { private fun showInstallablePluginInfoDialog(context: Context, item: PluginCenterItem) { val states = listOf(context.getString(R.string.text_installable)) val info = PluginInfoInstallable( - title = item.title, + item = item, states = states, - packageName = item.packageName, - version = item.versionSummary, - author = item.author, - collaborators = item.collaborators, - description = item.description, - packageSize = item.installableApkSizeBytes ?: 0L, lastInstallTime = item.lastInstallTime, 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) { @@ -81,107 +85,63 @@ object PluginInfoDialogManager { if (item.isUpdatable) add(context.getString(R.string.text_updatable)) } val info = PluginInfoInstalled( - title = item.title, + item = item, states = states, - packageName = item.packageName, - version = item.versionSummary, - author = item.author, - collaborators = item.collaborators, - description = item.description, - packageSize = item.packageSize, updatableVersion = item.updatableVersionSummary, firstInstallTime = item.firstInstallTime, 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 dialog = MaterialDialog.Builder(context) - .title(info.title) - .customView(binding.root, false) - .autoDismiss(false) .iconRes(R.drawable.ic_three_dots_outline_small) .limitIconToDefaultSize() + .title(info.title) + .customView(binding.root, false) .negativeText(R.string.dialog_button_dismiss) .onNegative { d, _ -> d.dismiss() } - .apply { - when (info) { - 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) - } - } - } - } - } - } - } - } + .autoDismiss(false) + .apply(builderApplier) .show() - .apply { - makeTextCopyable { titleView } - } + .apply { makeTextCopyable { titleView } } // Hold the current dialog and package name for refreshing on onResume. // zh-CN: 记录 "当前对话框" 与包名, 便于 onResume 刷新. currentDialog = WeakReference(dialog) - currentPackageName = item.packageName + currentPackageName = info.packageName restoreEssentialViews(binding, context, info) updateGuidelines(binding) - binding.stateValueFirst.text = info.states.getOrNull(0) - if (info.states.size > 1) { - binding.stateValueSecond.text = info.states[1] - binding.stateSpliterFirstSecond.isVisible = true - binding.stateValueSecond.isVisible = true + when (info.states.size) { + 0 -> { + binding.stateParent.isVisible = false + } + 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) @@ -190,7 +150,7 @@ object PluginInfoDialogManager { dialog.setCopyableTextIfAbsent(binding.descriptionValue, info.description) 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) DrawableCompat.setTint(d, adjustedImageContrastColor) DrawableCompat.setTintMode(d, PorterDuff.Mode.SRC_IN) @@ -205,10 +165,7 @@ object PluginInfoDialogManager { borderColor = context.getColor(R.color.plugin_center_item_icon_border), ) ) - dialog.iconView.colorFilterWithDesaturateOrNull(item.isEnabled, 0.5F) - if (info is PluginInfoInstalled) { - dialog.makeSettingsLaunchable({ it.iconView }, info.packageName) - } + dialog.iconView.colorFilterWithDesaturateOrNull(info.isEnabled, 0.5F) } // 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") @@ -246,6 +272,9 @@ object PluginInfoDialogManager { binding.pluginItemInfoCollaboratorsThirdParent.isVisible = true } when (info) { + is PluginInfoUpdatable -> { + /* No additional operations needed. */ + } is PluginInfoInstalled -> { info.updatableVersion?.let { binding.versionLabel.text = context.getString(R.string.plugin_item_info_installed_version) @@ -309,44 +338,39 @@ object PluginInfoDialogManager { ) private sealed interface PluginInfoBase { - val title: String - val states: List - val packageName: String - val version: String - val author: String? - val collaborators: List - val description: String? + val item: PluginCenterItem + val states: List get() = emptyList() + val isEnabled: Boolean get() = item.isEnabled + val title: String get() = item.title + val icon: Drawable? get() = item.icon + val packageName: String get() = item.packageName + val version: String? get() = item.versionSummary + val author: String? get() = item.author + val collaborators: List get() = item.collaborators + val description: String? get() = item.description val packageSize: Long - val apkUrl: String? - val sha256: String? + val apkUrl: String? get() = item.installableApkUrl + 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( - override val title: String, + override val item: PluginCenterItem, override val states: List, - override val packageName: String, - override val version: String, - override val author: String?, - override val collaborators: List, - override val description: String?, - override val packageSize: Long, - override val apkUrl: String?, - override val sha256: String?, + override val packageSize: Long = item.installableApkSizeBytes ?: 0L, val lastInstallTime: Long?, val lastUninstallTime: Long?, ) : PluginInfoBase private data class PluginInfoInstalled( - override val title: String, + override val item: PluginCenterItem, override val states: List, - override val packageName: String, - override val version: String, - override val author: String?, - override val collaborators: List, - override val description: String?, - override val packageSize: Long, - override val apkUrl: String?, - override val sha256: String?, + override val packageSize: Long = item.packageSize, val updatableVersion: String? = null, val firstInstallTime: Long?, val lastUpdateTime: Long?, diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginInstallActions.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginInstallActions.kt new file mode 100644 index 00000000..23e3e639 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginInstallActions.kt @@ -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>) { + 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() + } +} diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginInstaller.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginInstaller.kt index 15c47e6c..70239c77 100644 --- a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginInstaller.kt +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginInstaller.kt @@ -17,7 +17,8 @@ import org.autojs.autojs.ui.main.scripts.ApkInfoDialogManager import org.autojs.autojs.util.ClipboardUtils import org.autojs.autojs.util.FileUtils 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.autojs6.R import java.io.EOFException @@ -54,7 +55,7 @@ object PluginInstaller { MaterialDialog.Builder(context) .title(R.string.text_prompt) .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) .onNegative { d, _ -> d.dismiss() } .positiveText(R.string.dialog_button_continue) @@ -166,7 +167,7 @@ object PluginInstaller { .positiveColorRes(R.color.dialog_button_caution) .onPositive { _, _ -> d.getActionButton(DialogAction.POSITIVE).performClick() - UpdateUtils.openUrl(context, url) + IntentUtils.browse(context, url, SnackExceptionHolder(d.view)) } .cancelable(false) .build() @@ -317,7 +318,7 @@ object PluginInstaller { ClipboardUtils.setClip(context, result.message) 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) .onNegative { d, _ -> d.dismiss() diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/ocr/PaddleOcrPluginHost.kt b/app/src/main/java/org/autojs/autojs/core/plugin/ocr/PaddleOcrPluginHost.kt index b71c49ad..36b52885 100644 --- a/app/src/main/java/org/autojs/autojs/core/plugin/ocr/PaddleOcrPluginHost.kt +++ b/app/src/main/java/org/autojs/autojs/core/plugin/ocr/PaddleOcrPluginHost.kt @@ -20,6 +20,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine 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.OcrOptions import org.autojs.plugin.paddle.ocr.OcrResult @@ -106,7 +107,9 @@ object PaddleOcrPluginHost { // e.g. "v5" variant: String? = null, ): 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 (engineId != null) { list.firstOrNull { d -> d.pluginInfo?.id == engineId }?.let { return it } @@ -117,7 +120,10 @@ object PaddleOcrPluginHost { if (engine != null) { 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. diff --git a/app/src/main/java/org/autojs/autojs/network/UpdateChecker.java b/app/src/main/java/org/autojs/autojs/network/UpdateChecker.java index 7db0f371..b8a3f861 100644 --- a/app/src/main/java/org/autojs/autojs/network/UpdateChecker.java +++ b/app/src/main/java/org/autojs/autojs/network/UpdateChecker.java @@ -32,6 +32,7 @@ import org.autojs.autojs.tool.SimpleObserver; import org.autojs.autojs.ui.settings.DisplayVersionHistoriesActivity; import org.autojs.autojs.util.AndroidUtils; 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.TextUtils; import org.autojs.autojs.util.UpdateUtils; @@ -55,6 +56,7 @@ import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; import java.io.BufferedReader; import java.io.File; import java.io.Reader; +import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -77,31 +79,67 @@ import java.util.stream.Collectors; @SuppressLint("CheckResult") 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_HOME = "https://github.com/"; - public static final String URL_VERSION_PROPS_RAW = URL_BASE_GITHUB_RAW + "SuperMonster003/AutoJs6/master/version.properties"; - public static final String URL_VERSION_PROPS_BLOB = URL_BASE_GITHUB_HOME + "SuperMonster003/AutoJs6/blob/master/version.properties"; + private static final String TAG = UpdateChecker.class.getSimpleName(); private final Handler mHandler = new Handler(Looper.getMainLooper()); private final Context mContext; private final View mView; - private final PromptMode mPromptMode; - private final SimpleObserver mCallback; private final Executor mGitHubExecutor = Executors.newSingleThreadExecutor(); - private UpdateChecker(Context context, View view, PromptMode promptMode, SimpleObserver callback) { + private final PromptMode mPromptMode; + private final SimpleObserver 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 callback + ) { mContext = context; mView = view; mPromptMode = promptMode; 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 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() { @@ -119,20 +157,20 @@ public class UpdateChecker { AtomicReference errorRaw = new AtomicReference<>(); Observable obsBlobSafe = getStreamingApi() - .streamingUrl(URL_VERSION_PROPS_BLOB) + .streamingUrl(mRrlVersionPropsBlob) .subscribeOn(Schedulers.io()) .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()); e.printStackTrace(); return Observable.empty(); }); Observable obsRawSafe = getStreamingApi() - .streamingUrl(URL_VERSION_PROPS_RAW) + .streamingUrl(mUrlVersionPropsRaw) .subscribeOn(Schedulers.io()) .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()); e.printStackTrace(); return Observable.empty(); @@ -276,7 +314,7 @@ public class UpdateChecker { } 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.POSITIVE).setText(R.string.dialog_button_retry); 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).setOnClickListener(v -> { 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(); 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); negativeButton.setOnClickListener(v -> mUpdateDialog.dismiss()); @@ -351,56 +392,73 @@ public class UpdateChecker { mUpdateDialog.show(); mGitHubExecutor.execute(() -> { - GitHub github = GHub.getConnection(); + GitHub github = GitHubRepoUtils.getConnection(); if (github == null) { Dialog.setDialogContent(mUpdateDialog, R.string.error_cannot_connect_to_github); return; } - String userName = context.getString(R.string.developer_full_name); - String repoName = context.getString(R.string.app_name); - GHRepository repo = GHub.getRepo(github, userName, repoName); + GHRepository repo = GitHubRepoUtils.getRepo(github, mGitHubUser, mGitHubRepo); 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; } - GHRelease release = GHub.getRelease(repo); + GHRelease release = GitHubRepoUtils.getRelease(repo); if (release == null) { Dialog.setDialogContent(mUpdateDialog, R.string.error_get_github_latest_release); return; } + mHandler.post(() -> setUpdateDialogButtonNeutral(context, release.getHtmlUrl())); + 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); return; } - fetchLatestReleaseNotes(context, versionInfo, repo, release, releaseTag); - PagedIterable assets = GHub.getAssets(release); + fetchLatestChangelog(context, versionInfo, repo, release, releaseTag); + PagedIterable assets = GitHubRepoUtils.getAssets(release); if (assets == null) { mHandler.post(() -> new Dialog.Builder .Prompt(context, R.string.error_empty_github_release_assets) .build().show()); 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); String languageTag = language.getLocalCompatibleLanguageTag(); - String urlSuffix = "app/src/main/assets-app/doc/CHANGELOG-" + languageTag + ".md"; - String urlBlob = "https://github.com/SuperMonster003/AutoJs6/blob/master/" + urlSuffix; - String urlRaw = "https://raw.githubusercontent.com/SuperMonster003/AutoJs6/master/" + urlSuffix; + String urlSuffix = mGitHubChangelogFileProvider.with(languageTag); + + 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 obsBlob = getStreamingApi() .streamingUrl(urlBlob) .subscribeOn(Schedulers.io()) .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(); return Observable.never(); }) @@ -408,7 +466,7 @@ public class UpdateChecker { try { String content = responseBody.string().trim(); 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()) { String assembledHtml = assembleBlobDependenciesForSingleVersion(html, versionInfo.getVersionName(), urlBlob); return Observable.just(Html.fromHtml(assembledHtml, Html.FROM_HTML_MODE_COMPACT)); @@ -423,7 +481,7 @@ public class UpdateChecker { .streamingUrl(urlRaw) .subscribeOn(Schedulers.io()) .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(); return Observable.never(); }) @@ -431,7 +489,7 @@ public class UpdateChecker { try { String content = responseBody.string().trim(); 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()) { String assembledMarkdown = assembleRawDependenciesForSingleVersion(markdown, versionInfo.getVersionName(), urlRaw); String assembledHtml = TextUtils.markdownToHtml(assembledMarkdown); @@ -448,41 +506,40 @@ public class UpdateChecker { .observeOn(AndroidSchedulers.mainThread()) .timeout(23, TimeUnit.SECONDS) .subscribe( - releaseNotesSpannedForSingleVersion -> { - Dialog.setDialogContent(mUpdateDialog, releaseNotesSpannedForSingleVersion); + changelogSpannedForSingleVersion -> { + Dialog.setDialogContent(mUpdateDialog, changelogSpannedForSingleVersion); }, e -> { e.printStackTrace(); Spanned fallbackLatestReleaseNotes = getFallbackLatestReleaseNotes(repo, release); 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; } Dialog.setDialogContent(mUpdateDialog, fallbackLatestReleaseNotes); if (language != Language.ZH_HANS) { 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()); - } } ); } - 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 labelDependency = mContext.getString(R.string.changelog_label_dependency); boolean isFiltered = false; - List filteredReleaseNotes = new ArrayList<>(); - String[] items = releaseNotes.split("\n"); + List filteredChangelog = new ArrayList<>(); + String[] items = changelog.split("\n"); for (String item : items) { Log.d(TAG, "item: " + item); if (item.matches(".*\\b" + labelDependency + "\\b.*")) { isFiltered = true; } else { - filteredReleaseNotes.add(item); + filteredChangelog.add(item); } } @@ -490,8 +547,8 @@ public class UpdateChecker { String anchor = "v" + String.join("", versionName.split("\\.")); String dependenciesSummary = mContext.getString(R.string.text_changelog_item_dependency); - for (int i = filteredReleaseNotes.size() - 1; i >= 0; i--) { - String item = filteredReleaseNotes.get(i); + for (int i = filteredChangelog.size() - 1; i >= 0; i--) { + String item = filteredChangelog.get(i); if (!item.isBlank()) { String assembledHtml = "
  • " + labelImprovement + @@ -502,28 +559,28 @@ public class UpdateChecker { "\" rel=\"nofollow\">" + "CHANGELOG.md" + "
  • "; - filteredReleaseNotes.add(i + 1, assembledHtml); + filteredChangelog.add(i + 1, assembledHtml); 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 labelDependency = mContext.getString(R.string.changelog_label_dependency); boolean isFiltered = false; - List filteredReleaseNotes = new ArrayList<>(); - String[] items = releaseNotes.split("\n"); + List filteredChangelog = new ArrayList<>(); + String[] items = changelog.split("\n"); for (String item : items) { if (item.contains("`" + labelDependency + "`")) { isFiltered = true; } else { - filteredReleaseNotes.add(item); + filteredChangelog.add(item); } } @@ -531,28 +588,21 @@ public class UpdateChecker { String anchor = "v" + String.join("", versionName.split("\\.")); String dependenciesSummary = mContext.getString(R.string.text_changelog_item_dependency); - for (int i = filteredReleaseNotes.size() - 1; i >= 0; i--) { - String item = filteredReleaseNotes.get(i); + for (int i = filteredChangelog.size() - 1; i >= 0; i--) { + String item = filteredChangelog.get(i); if (!item.isBlank()) { String assembledMarkdown = "* `" + labelImprovement + "` " + dependenciesSummary + " _[`CHANGELOG.md`](" + markdownUrl + "#" + anchor + ")_"; - filteredReleaseNotes.add(i + 1, assembledMarkdown); + filteredChangelog.add(i + 1, assembledMarkdown); break; } } - return String.join("\n", filteredReleaseNotes); + return String.join("\n", filteredChangelog); } - return releaseNotes; - } - - 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; + return changelog; } @Nullable - private String parseLatestReleaseNotesFromHtml(String htmlContent) { + private String parseLatestChangelogFromHtml(String htmlContent) { Document document = Jsoup.parse(htmlContent); Element releaseDateHeading = document.selectFirst("div.markdown-heading"); if (releaseDateHeading != null) { @@ -572,7 +622,7 @@ public class UpdateChecker { } @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)"); Matcher matcher = pattern.matcher(markdown); if (matcher.find()) { @@ -590,27 +640,25 @@ public class UpdateChecker { private Spanned getFallbackLatestReleaseNotes(GHRepository repo, GHRelease release) { Spanned htmlSpannedContent = getLatestReleaseFromGitHubRelease(repo, release); 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; } - Log.d(TAG, "Release note got from latest release (fallback)"); + Log.d(TAG, "Fetch the latest release notes (fallback) successfully"); return htmlSpannedContent; } - private static @Nullable Spanned getLatestReleaseFromGitHubRelease(GHRepository repo, GHRelease release) { - CompletableFuture future = CompletableFuture.supplyAsync(() -> { - return GHub.getReleaseHtml(repo, release); + private void setUpdateDialogButtonNeutral(Context context, URL htmlUrl) { + String url = htmlUrl == null ? "" : htmlUrl.toString(); + 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 ghAssets, VersionInfo versionInfo) { + private void setUpdateDialogButtonPositive(@NonNull Context ctx, PagedIterable ghAssets, VersionInfo versionInfo) { MDButton positiveButton = mUpdateDialog.getActionButton(DialogAction.POSITIVE); positiveButton.setTextColor(ctx.getColor(R.color.dialog_button_attraction)); positiveButton.setOnClickListener(v -> { @@ -623,7 +671,7 @@ public class UpdateChecker { mGitHubExecutor.execute(() -> { List abiList = AndroidUtils.getDeviceFilteredAbiList(); abiList.add("universal"); - GHub.Asset targetAsset = GHub.pickAssetIntelligently(ghAssets, abiList); + GitHubRepoUtils.Asset targetAsset = GitHubRepoUtils.pickAssetIntelligently(ghAssets, abiList); 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 { private final Context mContext; @@ -655,8 +712,16 @@ public class UpdateChecker { private PromptMode mPromptMode; private SimpleObserver 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) { mContext = context; + mGitHubUser = context.getString(R.string.developer_full_name); + mGitHubRepo = context.getString(R.string.app_name); } public Builder(@NonNull View view) { @@ -664,18 +729,52 @@ public class UpdateChecker { 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) { - this.mPromptMode = promptMode; + mPromptMode = promptMode; return this; } public Builder setCallback(SimpleObserver callback) { - this.mCallback = callback; + mCallback = callback; return this; } 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); this .title(versionInfo.getVersionName()) - .options(List.of(new MaterialDialog.OptionMenuItemSpec(context.getString(R.string.dialog_button_ignore_current_update), parentDialog -> { - new 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_warn) - .onPositive((tmpDialog, which) -> { - UpdateUtils.addIgnoredVersion(versionInfo); - ViewUtils.showToast(context, R.string.text_done); - parentDialog.dismiss(); - }) - .show(); - }))) - .content(R.string.text_getting_release_notes) - .neutralText(R.string.dialog_button_version_histories) - .neutralColor(context.getColor(R.color.dialog_button_hint)) - .onNeutral((dialog, which) -> { - DisplayVersionHistoriesActivity.launch(context); - }) + .options(List.of( + new MaterialDialog.OptionMenuItemSpec(context.getString(R.string.dialog_button_ignore_current_update), parentDialog -> { + new 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((tmpDialog, which) -> { + UpdateUtils.addIgnoredVersion(versionInfo); + ViewUtils.showToast(context, R.string.text_done); + parentDialog.dismiss(); + }) + .show(); + }), + new MaterialDialog.OptionMenuItemSpec(context.getString(R.string.dialog_button_version_histories), parentDialog -> { + 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) .negativeColor(context.getColor(R.color.dialog_button_default)) .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; @@ -897,15 +998,15 @@ public class UpdateChecker { mGitHubAsset = gitHubAsset; } - public void setAbi(String abi) { - mAbi = abi; - } - @Override public String getAbi() { return mAbi; } + public void setAbi(String abi) { + mAbi = abi; + } + @Override public String getFileName() { return mGitHubAsset.getName(); @@ -928,5 +1029,4 @@ public class UpdateChecker { private static class ObservableEmptyException extends RuntimeException { /* Empty body. */ } - } \ No newline at end of file diff --git a/app/src/main/java/org/autojs/autojs/network/UpdateIgnoreStore.kt b/app/src/main/java/org/autojs/autojs/network/UpdateIgnoreStore.kt new file mode 100644 index 00000000..2a253905 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/network/UpdateIgnoreStore.kt @@ -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 { + 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() + +} diff --git a/app/src/main/java/org/autojs/autojs/network/download/DownloadManager.java b/app/src/main/java/org/autojs/autojs/network/download/DownloadManager.java index 64ca8434..9b58a2f9 100644 --- a/app/src/main/java/org/autojs/autojs/network/download/DownloadManager.java +++ b/app/src/main/java/org/autojs/autojs/network/download/DownloadManager.java @@ -7,33 +7,11 @@ import android.os.Environment; import android.os.Handler; import android.os.Looper; import android.widget.ProgressBar; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import com.afollestad.materialdialogs.DialogAction; import com.afollestad.materialdialogs.MaterialDialog; 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.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; @@ -44,9 +22,28 @@ import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; 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.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. */ @@ -164,7 +161,7 @@ public class DownloadManager { .positiveColorRes(R.color.dialog_button_caution) .onPositive((d2, which2) -> { dialog.getActionButton(DialogAction.POSITIVE).performClick(); - UpdateUtils.openUrl(context, url); + IntentUtils.browse(context, url, new ToastExceptionHolder(context)); }) .cancelable(false) .build() diff --git a/app/src/main/java/org/autojs/autojs/runtime/api/augment/ocr/Ocr.kt b/app/src/main/java/org/autojs/autojs/runtime/api/augment/ocr/Ocr.kt index 53b47246..0a3e451d 100644 --- a/app/src/main/java/org/autojs/autojs/runtime/api/augment/ocr/Ocr.kt +++ b/app/src/main/java/org/autojs/autojs/runtime/api/augment/ocr/Ocr.kt @@ -123,7 +123,7 @@ class Ocr(private val scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime) // @Overload // funcName(img: ImageWrapper, options?: DetectOptionsMLKit | DetectOptionsPaddle): 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 -> { // @Signature @@ -135,7 +135,7 @@ class Ocr(private val scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime) // funcName(img: ImageWrapper, region: OmniRegion): org.autojs.autojs.runtime.api.OcrResult[]; val capt = AugmentableImages.captureScreen(scriptRuntime, emptyArray()) - dispatchOcrWith(scriptRuntime, funcName, arrayOf(capt, arg0, arg1)) + dispatchOcrWith(scriptRuntime, funcName, arrayOf(capt, arg0, arg1), overrideMode, resultsHandler) } 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[]; - dispatchOcrWith(scriptRuntime, funcName, arrayOf(arg0, options)) + dispatchOcrWith(scriptRuntime, funcName, arrayOf(arg0, options), overrideMode, resultsHandler) } else -> { diff --git a/app/src/main/java/org/autojs/autojs/runtime/api/augment/ocr/OcrPaddle.kt b/app/src/main/java/org/autojs/autojs/runtime/api/augment/ocr/OcrPaddle.kt index bef01a7f..4f77c12f 100644 --- a/app/src/main/java/org/autojs/autojs/runtime/api/augment/ocr/OcrPaddle.kt +++ b/app/src/main/java/org/autojs/autojs/runtime/api/augment/ocr/OcrPaddle.kt @@ -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.util.RhinoUtils.coerceBoolean import org.autojs.autojs.util.RhinoUtils.coerceIntNumber +import org.autojs.autojs6.R import org.autojs.plugin.paddle.ocr.OcrOptions import org.mozilla.javascript.NativeArray import org.mozilla.javascript.NativeObject @@ -58,7 +59,7 @@ class OcrPaddle(private val scriptRuntime: ScriptRuntime) : Augmentable(scriptRu } return runBlocking(scriptRuntime.coroutineContext) { 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) } } @@ -73,7 +74,7 @@ class OcrPaddle(private val scriptRuntime: ScriptRuntime) : Augmentable(scriptRu } return runBlocking(scriptRuntime.coroutineContext) { 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) }.map { OcrResult(it.text, it.confidence, it.bounds) } } diff --git a/app/src/main/java/org/autojs/autojs/ui/main/MainActivity.kt b/app/src/main/java/org/autojs/autojs/ui/main/MainActivity.kt index 2a67b326..c9326444 100644 --- a/app/src/main/java/org/autojs/autojs/ui/main/MainActivity.kt +++ b/app/src/main/java/org/autojs/autojs/ui/main/MainActivity.kt @@ -32,8 +32,8 @@ import org.autojs.autojs.event.BackPressedHandler.DoublePressExit import org.autojs.autojs.event.BackPressedHandler.HostActivity import org.autojs.autojs.extension.ViewExtensions.setOnTitleViewLongClickListener import org.autojs.autojs.model.explorer.Explorers -import org.autojs.autojs.permission.DisplayOverOtherAppsPermission import org.autojs.autojs.permission.AllFilesAccessPermission +import org.autojs.autojs.permission.DisplayOverOtherAppsPermission import org.autojs.autojs.permission.PostNotificationsPermission import org.autojs.autojs.service.ForegroundService 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.main.drawer.DrawerFragment.Companion.Event.OnDrawerClosed 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.task.TaskManagerFragment import org.autojs.autojs.ui.settings.PreferencesActivity @@ -101,6 +102,9 @@ class MainActivity : BaseActivity(), DelegateHost, HostActivity { val docsItemIndex: Int get() = findPageIndexByTitle(R.string.text_documentation) + val pluginsIndex: Int + get() = findPageIndexByTitle(R.string.text_plugins) + private fun findPageIndexByTitle(titleRes: Int): Int { var i = 0 while (i < mPagerAdapter.count) { @@ -226,6 +230,7 @@ class MainActivity : BaseActivity(), DelegateHost, HostActivity { mPagerAdapter = FragmentPagerAdapterBuilder(this) .add(ExplorerFragment(), R.string.text_file) .add(DocumentationFragment(), R.string.text_documentation) + .add(PluginFragment(), R.string.text_plugins) .add(TaskManagerFragment(), R.string.text_task) .build() .apply { diff --git a/app/src/main/java/org/autojs/autojs/ui/main/plugin/PluginFloatingActionMenu.java b/app/src/main/java/org/autojs/autojs/ui/main/plugin/PluginFloatingActionMenu.java new file mode 100644 index 00000000..0e093ef0 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/ui/main/plugin/PluginFloatingActionMenu.java @@ -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 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 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); + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/autojs/autojs/ui/main/plugin/PluginFragment.kt b/app/src/main/java/org/autojs/autojs/ui/main/plugin/PluginFragment.kt new file mode 100644 index 00000000..d8001067 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/ui/main/plugin/PluginFragment.kt @@ -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(R.id.plugin_floating_action_menu).also { menu -> + menu.state + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : SimpleObserver() { + 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" + } + +} diff --git a/app/src/main/java/org/autojs/autojs/ui/main/scripts/ExplorerFragment.kt b/app/src/main/java/org/autojs/autojs/ui/main/scripts/ExplorerFragment.kt index fdbc3f1c..8c54b86c 100644 --- a/app/src/main/java/org/autojs/autojs/ui/main/scripts/ExplorerFragment.kt +++ b/app/src/main/java/org/autojs/autojs/ui/main/scripts/ExplorerFragment.kt @@ -77,8 +77,8 @@ class ExplorerFragment : ViewPagerFragment(0), OnFloatingActionButtonClickListen } (activity as? MainActivity)?.apply { val tabLayout: TabLayout = findViewById(R.id.tab) - val docsTab = tabLayout.getTabAt(filesItemIndex) - docsTab?.view?.let { setTabViewClickListeners(it) } + val filesTab = tabLayout.getTabAt(filesItemIndex) + filesTab?.view?.let { setTabViewClickListeners(it) } } restoreViewStates() } @@ -216,19 +216,19 @@ class ExplorerFragment : ViewPagerFragment(0), OnFloatingActionButtonClickListen override fun onClick(button: FloatingActionButton, pos: Int) { mExplorerView?.let { view -> 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( Intent(context, ProjectConfigActivity::class.java) .putExtra(ProjectConfigActivity.EXTRA_PARENT_DIRECTORY, view.currentPage.path) .putExtra(ProjectConfigActivity.EXTRA_NEW_PROJECT, true) .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 } } } diff --git a/app/src/main/java/org/autojs/autojs/ui/settings/AboutActivity.kt b/app/src/main/java/org/autojs/autojs/ui/settings/AboutActivity.kt index 8a98103b..98e166ff 100644 --- a/app/src/main/java/org/autojs/autojs/ui/settings/AboutActivity.kt +++ b/app/src/main/java/org/autojs/autojs/ui/settings/AboutActivity.kt @@ -163,6 +163,7 @@ open class AboutActivity : BaseActivity() { private fun checkForUpdates() { UpdateChecker.Builder(this) .setPromptMode(PromptMode.DIALOG) + .setGitHubMainBranch("master") .build().checkNow() } diff --git a/app/src/main/java/org/autojs/autojs/ui/settings/CheckForUpdatesPreference.kt b/app/src/main/java/org/autojs/autojs/ui/settings/CheckForUpdatesPreference.kt index 329f4551..5a4398d3 100644 --- a/app/src/main/java/org/autojs/autojs/ui/settings/CheckForUpdatesPreference.kt +++ b/app/src/main/java/org/autojs/autojs/ui/settings/CheckForUpdatesPreference.kt @@ -33,6 +33,7 @@ class CheckForUpdatesPreference : MaterialPreference, OnSharedPreferenceChangeLi override fun onClick() { UpdateChecker.Builder(prefContext) .setPromptMode(PromptMode.DIALOG) + .setGitHubMainBranch("master") .build().checkNow() super.onClick() } diff --git a/app/src/main/java/org/autojs/autojs/ui/settings/CheckForUpdatesWithLocalVersionIgnoredPreference.kt b/app/src/main/java/org/autojs/autojs/ui/settings/CheckForUpdatesWithLocalVersionIgnoredPreference.kt index a96d0fcc..5fa67b44 100644 --- a/app/src/main/java/org/autojs/autojs/ui/settings/CheckForUpdatesWithLocalVersionIgnoredPreference.kt +++ b/app/src/main/java/org/autojs/autojs/ui/settings/CheckForUpdatesWithLocalVersionIgnoredPreference.kt @@ -22,6 +22,7 @@ class CheckForUpdatesWithLocalVersionIgnoredPreference : MaterialPreference { override fun onClick() { UpdateChecker.Builder(context) .setPromptMode(PromptMode.DIALOG) + .setGitHubMainBranch("master") .build() .checkNow(true) super.onClick() diff --git a/app/src/main/java/org/autojs/autojs/util/ViewUtils.kt b/app/src/main/java/org/autojs/autojs/util/ViewUtils.kt index 4ed48f4d..02ffa278 100644 --- a/app/src/main/java/org/autojs/autojs/util/ViewUtils.kt +++ b/app/src/main/java/org/autojs/autojs/util/ViewUtils.kt @@ -1006,6 +1006,10 @@ object ViewUtils { toolbar.setNavigationIconColorByColorLuminance(context, aimColor) } + fun Toolbar.setTitlesTextColorByThemeColorLuminance(context: Context) { + this.setTitlesTextColorByColorLuminance(context, ThemeColorManager.colorPrimary) + } + fun Toolbar.setTitlesTextColorByColorLuminance(context: Context, aimColor: Int) { val color = getDayOrNightColorByLuminance(context, aimColor) setTitleTextColor(color) diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 4c15447a..71f38481 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -69,7 +69,19 @@ android:clipToPadding="false" android:visibility="invisible" app:layout_anchor="@id/viewpager" - app:layout_anchorGravity="bottom|end" /> + app:layout_anchorGravity="bottom|end"/> + + diff --git a/app/src/main/res/layout/fragment_plugin.xml b/app/src/main/res/layout/fragment_plugin.xml new file mode 100644 index 00000000..6c3b6fba --- /dev/null +++ b/app/src/main/res/layout/fragment_plugin.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/app/src/main/res/menu/menu_plugin_center.xml b/app/src/main/res/menu/menu_plugin_center.xml index 4cfed1a1..ca44729f 100644 --- a/app/src/main/res/menu/menu_plugin_center.xml +++ b/app/src/main/res/menu/menu_plugin_center.xml @@ -27,7 +27,10 @@ android:id="@+id/action_search" android:icon="@drawable/ic_search_smaller_black_48dp" android:title="@string/text_search" - app:showAsAction="always" /> + android:imeOptions="actionSearch" + android:inputType="text" + app:actionViewClass="androidx.appcompat.widget.SearchView" + app:showAsAction="always|collapseActionView" /> اكتب إعدادات الأمان كتابة إعدادات النظام النوافذ المنبثقة في الخلفية + لم يتم العثور على أي مكونات Paddle OCR إضافية متاحة + مثبّت + غير مثبّت + الكل + فرز حسب الاسم + فرز حسب آخر تحديث + فرز حسب حجم الحزمة diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index ac7eb713..2d478294 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -1169,5 +1169,12 @@ Write security settings Write system settings Display pop-up windows while running in the background + No Paddle OCR plugins available + Installed + Not installed + All + Sort by name + Sort by last update time + Sort by package size diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 185117ae..04b39be4 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1172,5 +1172,12 @@ Escribir la configuración de seguridad Escribir la configuración del sistema Ventanas emergentes en segundo plano + No se encontraron plugins de Paddle OCR disponibles + Instalado + No instalado + Todos + Ordenar por nombre + Ordenar por última actualización + Ordenar por tamaño del paquete diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 882f5b02..d4c69bd6 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1172,5 +1172,12 @@ Écrire les paramètres de sécurité. Écrire les paramètres système Fenêtres contextuelles en arrière-plan + Aucun plugin Paddle OCR disponible + Installé + Non installé + Tous + Trier par nom + Trier par dernière mise à jour + Trier par taille du paquet diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 5aa4dc64..a9bff59c 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -1173,5 +1173,12 @@ セキュリティ設定の書き込み システム設定の書き込み バックグラウンドでのポップアップ表示 + 利用可能な Paddle OCR プラグインが見つかりません + インストール済み + 未インストール + すべて + 名前で並べ替え + 最終更新日で並べ替え + パッケージサイズで並べ替え diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 227d7858..ea024dad 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -1174,5 +1174,12 @@ 보안 설정을 작성하십시오 시스템 설정을 작성하십시오 백그라운드 팝업 + 사용 가능한 Paddle OCR 플러그인을 찾을 수 없습니다 + 설치됨 + 설치되지 않음 + 전체 + 이름순 정렬 + 최근 업데이트순 정렬 + 패키지 크기순 정렬 diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index f8b18a01..8a132f46 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1172,5 +1172,12 @@ Параметры безопасности записи Запись системных настроек Всплывающие окна в фоне + Доступные плагины Paddle OCR не найдены + Установлено + Не установлено + Все + Сортировать по имени + Сортировать по времени последнего обновления + Сортировать по размеру пакета diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 54ace4c9..6f6b3a56 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -1170,5 +1170,12 @@ 修改安全設置 修改系統設置 後台彈出界面 + 未找到可用的 Paddle OCR 插件 + 已安裝 + 未安裝 + 全部 + 按名稱排序 + 按最近更新排序 + 按安裝包大小排序 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index f2501fdd..3ba05cdb 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1170,5 +1170,12 @@ 修改安全設定 修改系統設定 後臺彈出介面 + 未找到可用的 Paddle OCR 外掛 + 已安裝 + 未安裝 + 全部 + 按名稱排序 + 按最近更新排序 + 按安裝包大小排序 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index f6c59b34..4b79970c 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -1170,5 +1170,12 @@ 修改安全设置 修改系统设置 后台弹出界面 + 未找到可用的 Paddle OCR 插件 + 已安装 + 未安装 + 全部 + 按名称排序 + 按最近更新排序 + 按安装包大小排序 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5b77ce64..ee1a6abb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1427,5 +1427,12 @@ Write security settings Write system settings Display pop-up windows while running in the background + No Paddle OCR plugins available + Installed + Not installed + All + Sort by name + Sort by last update time + Sort by package size diff --git a/version.properties b/version.properties index 5e54c667..50d30979 100644 --- a/version.properties +++ b/version.properties @@ -1,5 +1,5 @@ -#Fri Jan 16 23:14:30 CST 2026 -BUILD_TIME=1768576470633 +#Sat Jan 17 15:22:28 CST 2026 +BUILD_TIME=1768634548657 COMPILE_SDK_VERSION=36 IMAGE_QUANT_CMAKE_VERSION=3.22.1 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 TARGET_SDK_VERSION=36 TARGET_SDK_VERSION_INRT=29 -VERSION_BUILD=3622 -VERSION_NAME=6.7.0 Alpha14 +VERSION_BUILD=3623 +VERSION_NAME=6.7.0 Alpha15 VSCODE_EXT_REQUIRED_VERSION=1.0.13