From a99247b80cc10869e67d19291a57dd700dfb4807 Mon Sep 17 00:00:00 2001 From: SuperMonster003 Date: Mon, 8 Dec 2025 23:30:02 +0800 Subject: [PATCH] =?UTF-8?q?6.7.0=20-=20Alpha12=20-=20=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=E4=B8=AD=E5=BF=83=20M1=20-=20=E6=8F=92=E4=BB=B6=E4=B8=AD?= =?UTF-8?q?=E5=BF=83=E6=94=AF=E6=8C=81=E5=AE=89=E8=A3=85=20(=E6=9C=AC?= =?UTF-8?q?=E5=9C=B0/URL),=20=E5=8D=B8=E8=BD=BD,=20=E5=90=AF=E7=94=A8,=20?= =?UTF-8?q?=E7=A6=81=E7=94=A8,=20=E8=AF=A6=E6=83=85=E5=B1=95=E7=A4=BA.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../center/InstalledPluginRepository.kt | 21 +- .../plugin/center/PluginCenterActivity.kt | 103 ++ .../plugin/center/PluginCenterFragment.kt | 83 +- .../core/plugin/center/PluginCenterItem.kt | 62 +- .../plugin/center/PluginCenterItemAdapter.kt | 12 +- .../center/PluginCenterItemViewHolder.kt | 82 +- .../plugin/center/PluginCenterViewModel.kt | 50 +- .../core/plugin/center/PluginEnableStore.kt | 22 + .../core/plugin/center/PluginIndexEntry.kt | 6 +- .../plugin/center/PluginIndexRepository.kt | 11 +- .../plugin/center/PluginInfoDialogManager.kt | 333 ++++++ .../core/plugin/center/PluginInstaller.kt | 246 ++++ .../core/plugin/center/PluginRecentStore.kt | 37 + .../core/plugin/ocr/PaddleOcrPluginHost.kt | 2 +- .../java/org/autojs/autojs/core/pref/Pref.kt | 8 +- .../network/download/DownloadManager.java | 4 +- .../ui/main/scripts/ApkInfoDialogManager.kt | 12 +- .../org/autojs/autojs/ui/main/task/Task.java | 2 +- .../java/org/autojs/autojs/util/FileUtils.kt | 189 ++- .../java/org/autojs/autojs/util/TimeUtils.kt | 2 +- .../java/org/autojs/autojs/util/ViewUtils.kt | 80 ++ ...tem.xml => apk_file_info_dialog_items.xml} | 0 .../res/layout/plugin_info_dialog_items.xml | 1042 +++++++++++++++++ app/src/main/res/menu/menu_plugin_center.xml | 47 + app/src/main/res/values-ar/strings.xml | 48 +- app/src/main/res/values-en/strings.xml | 48 +- app/src/main/res/values-es/strings.xml | 48 +- app/src/main/res/values-fr/strings.xml | 48 +- app/src/main/res/values-ja/strings.xml | 48 +- app/src/main/res/values-ko/strings.xml | 48 +- app/src/main/res/values-night/colors.xml | 1 + app/src/main/res/values-ru/strings.xml | 48 +- app/src/main/res/values-zh-rHK/strings.xml | 48 +- app/src/main/res/values-zh-rTW/strings.xml | 48 +- app/src/main/res/values-zh/strings.xml | 48 +- app/src/main/res/values/colors.xml | 1 + app/src/main/res/values/strings.xml | 49 +- .../autojs/plugin/paddle/ocr/PluginInfo.aidl | 2 +- version.properties | 6 +- 39 files changed, 2792 insertions(+), 203 deletions(-) create mode 100644 app/src/main/java/org/autojs/autojs/core/plugin/center/PluginEnableStore.kt create mode 100644 app/src/main/java/org/autojs/autojs/core/plugin/center/PluginInfoDialogManager.kt create mode 100644 app/src/main/java/org/autojs/autojs/core/plugin/center/PluginInstaller.kt create mode 100644 app/src/main/java/org/autojs/autojs/core/plugin/center/PluginRecentStore.kt rename app/src/main/res/layout/{apk_file_info_dialog_list_item.xml => apk_file_info_dialog_items.xml} (100%) create mode 100644 app/src/main/res/layout/plugin_info_dialog_items.xml create mode 100644 app/src/main/res/menu/menu_plugin_center.xml diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/InstalledPluginRepository.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/InstalledPluginRepository.kt index 65ea2df1..e0cb6d63 100644 --- a/app/src/main/java/org/autojs/autojs/core/plugin/center/InstalledPluginRepository.kt +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/InstalledPluginRepository.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.withContext import org.autojs.autojs.core.plugin.ocr.PaddleOcrPluginHost import org.autojs.autojs6.R import org.autojs.plugin.paddle.ocr.PluginInfo +import java.io.File /** * Local installed plugin discovery (based on existing PaddleOcrPluginHost.discover). @@ -23,8 +24,9 @@ class InstalledPluginRepository { val author: String?, val versionName: String, val versionCode: Long?, - val installTime: Long?, - val updateTime: Long?, + val packageSize: Long, + val firstInstallTime: Long?, + val lastUpdateTime: Long?, val icon: Drawable?, val pluginInfo: PluginInfo?, ) @@ -45,6 +47,16 @@ class InstalledPluginRepository { val versionCode = pkgInfo?.let { PackageInfoCompat.getLongVersionCode(it) } ?: d.pluginInfo?.versionCode val firstInstallTime = pkgInfo?.firstInstallTime val lastUpdateTime = pkgInfo?.lastUpdateTime + val packageSize = run calcPackageSize@{ + val baseApkSize = appInfo?.publicSourceDir?.let { File(it).length() } + ?: appInfo?.sourceDir?.let { File(it).length() } + val splitApkTotalSize = when { + appInfo?.splitPublicSourceDirs != null -> appInfo.splitPublicSourceDirs!!.sumOf { File(it).length() } + appInfo?.splitSourceDirs != null -> appInfo.splitSourceDirs!!.sumOf { File(it).length() } + else -> 0L + } + baseApkSize?.let { it + splitApkTotalSize } ?: 0L + } InstalledPlugin( packageName = packageName, @@ -53,8 +65,9 @@ class InstalledPluginRepository { author = d.pluginInfo?.author, versionName = versionName, versionCode = versionCode, - installTime = firstInstallTime, - updateTime = lastUpdateTime, + packageSize = packageSize, + firstInstallTime = firstInstallTime, + lastUpdateTime = lastUpdateTime, icon = icon, pluginInfo = d.pluginInfo, ) 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 566365c1..309bb956 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 @@ -4,7 +4,19 @@ import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import androidx.activity.result.contract.ActivityResultContracts +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.error.ErrorDialogActivity +import org.autojs.autojs.util.ViewUtils +import org.autojs.autojs.util.ViewUtils.setMenuIconsColorByThemeColorLuminance +import org.autojs.autojs.util.ViewUtils.setNavigationIconColorByThemeColorLuminance import org.autojs.autojs6.R import org.autojs.autojs6.databinding.ActivityPluginCenterBinding @@ -13,6 +25,13 @@ class PluginCenterActivity : BaseActivity() { private lateinit var binding: ActivityPluginCenterBinding + private val pickApkLauncher = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> + uri ?: return@registerForActivityResult + lifecycleScope.launch { + PluginInstaller.installFromFileUriWithPrompt(this@PluginCenterActivity, uri) + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -28,6 +47,90 @@ class PluginCenterActivity : BaseActivity() { setToolbarAsBack(R.string.text_plugin_center) } + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.menu_plugin_center, menu) + setUpToolbarColors() + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.action_install_from_local_file -> { + pickApkLauncher.launch(arrayOf("application/vnd.android.package-archive")) + 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() + lifecycleScope.launch { + runCatching { + PluginInstaller.installFromUrlWithPrompt(this@PluginCenterActivity, url) + }.onFailure { e -> + ErrorDialogActivity.showErrorDialog( + this@PluginCenterActivity, + R.string.text_failed_to_retrieve, + e.message ?: e.toString(), + ) + } + } + } + positiveButton.setTextColor(d.context.getColor(R.color.dialog_button_attraction)) + } + } + } + .alwaysCallInputCallback() + .widgetThemeColor() + .negativeText(R.string.text_cancel) + .negativeColorRes(R.color.dialog_button_default) + .onNegative { d, _ -> d.dismiss() } + .positiveText(R.string.dialog_button_retrieve) + .positiveColorRes(R.color.dialog_button_unavailable) + .autoDismiss(false) + .cancelable(false) + .show() + true + } + R.id.action_search -> { + // TODO action_search + ViewUtils.showToast(this, R.string.text_under_development) + true + } + R.id.action_sort -> { + // TODO action_sort + ViewUtils.showToast(this, R.string.text_under_development) + true + } + R.id.action_filter -> { + // TODO action_filter + ViewUtils.showToast(this, R.string.text_under_development) + true + } + R.id.action_global_settings -> { + // TODO action_global_settings + ViewUtils.showToast(this, R.string.text_under_development) + true + } + else -> super.onOptionsItemSelected(item) + } + } + + private fun setUpToolbarColors() { + binding.toolbar.setMenuIconsColorByThemeColorLuminance(this) + binding.toolbar.setNavigationIconColorByThemeColorLuminance(this) + } + companion object { fun startActivity(context: Context) { 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 20bdd6cd..0a673ae1 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 @@ -1,8 +1,13 @@ package org.autojs.autojs.core.plugin.center +import android.content.BroadcastReceiver import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.net.Uri import android.os.Bundle import android.view.View +import androidx.activity.result.contract.ActivityResultContracts import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope @@ -22,17 +27,40 @@ class PluginCenterFragment : Fragment(R.layout.fragment_plugin_center) { private val vm: PluginCenterViewModel by viewModels() private lateinit var adapter: PluginCenterItemAdapter - private lateinit var context: Context + private lateinit var contextRef: Context + + private val uninstallLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + vm.load(requireContext()) + } + + private var pkgReceiver: BroadcastReceiver? = null override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) _binding = FragmentPluginCenterBinding.bind(view) - val context = requireContext().also { context = it } + val context = requireContext().also { contextRef = it } + + adapter = PluginCenterItemAdapter(object : PluginCenterItemAdapter.Listener { + override fun onToggleEnable(item: PluginCenterItem, enabled: Boolean) { + vm.setEnabled(contextRef, item.packageName, enabled) + item.isEnabled = enabled + } + + override fun onUninstall(item: PluginCenterItem) { + val uri = Uri.parse("package:${item.packageName}") + val intent = Intent(Intent.ACTION_DELETE, uri) + uninstallLauncher.launch(intent) + } + + override fun onDetails(item: PluginCenterItem) { + PluginInfoDialogManager.showPluginInfoDialog(contextRef, item) + } + }) binding.pluginCenterRecyclerView.apply { layoutManager = LinearLayoutManager(context) - adapter = PluginCenterItemAdapter().also { this@PluginCenterFragment.adapter = it } + adapter = this@PluginCenterFragment.adapter addItemDecoration(DividerItemDecoration(context, VERTICAL)) excludePaddingClippableViewFromBottomNavigationBar() } @@ -50,6 +78,55 @@ class PluginCenterFragment : Fragment(R.layout.fragment_plugin_center) { } } + override fun onStart() { + super.onStart() + run registerPackageReceiver@{ + pkgReceiver ?: return@registerPackageReceiver + pkgReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val data = intent.data ?: return + val packageName = data.schemeSpecificPart ?: return + val replacing = intent.getBooleanExtra(Intent.EXTRA_REPLACING, false) + when (intent.action) { + Intent.ACTION_PACKAGE_ADDED -> { + // Package installation (update) does not record "Recently installed" when replacing, but will refresh. + // zh-CN: 替换安装 (更新) 不记录 "最近安装", 但会刷新. + if (!replacing) PluginRecentStore.setLastInstalled(packageName) + vm.load(context) + } + Intent.ACTION_PACKAGE_REMOVED -> { + // Package uninstallation (pre-update phase) does not record "Recently uninstalled" when replacing. + // zh-CN: 替换卸载 (更新前阶段) 不记录 "最近卸载". + if (!replacing) PluginRecentStore.setLastUninstalled(packageName) + vm.load(context) + } + } + } + } + val filter = IntentFilter().apply { + addAction(Intent.ACTION_PACKAGE_ADDED) + addAction(Intent.ACTION_PACKAGE_REMOVED) + addDataScheme("package") + } + requireContext().registerReceiver(pkgReceiver, filter) + } + } + + override fun onStop() { + super.onStop() + pkgReceiver?.let { runCatching { requireContext().unregisterReceiver(it) } } + pkgReceiver = null + } + + override fun onResume() { + super.onResume() + // Refresh once when returning to the page to update install/uninstall status. + // zh-CN: 回到页面时刷新一次, 覆盖安装/卸载后的状态. + if (::contextRef.isInitialized) { + vm.load(contextRef) + } + } + override fun onDestroyView() { super.onDestroyView() _binding = null 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 a150ea1f..a19294ed 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 @@ -1,18 +1,66 @@ package org.autojs.autojs.core.plugin.center import android.graphics.drawable.Drawable +import org.joda.time.DateTime +import org.joda.time.format.DateTimeFormat data class PluginCenterItem( - val packageName: String, val title: String, - val description: String, - val author: String? = null, - val collaborators: List = emptyList(), + val packageName: String, val versionName: String, val versionCode: Long? = null, val versionDate: String? = null, - val isEnabled: Boolean = true, - val isUpdatable: Boolean = false, + var updatableVersionName: String? = null, + var updatableVersionCode: Long? = null, + var updatableVersionDate: String? = null, + val author: String? = null, + val collaborators: List = emptyList(), + val description: String, + + // Size of installed package (aggregated base + splits), 0 for uninstalled. + // zh-CN: 已安装包大小 (聚合 base + splits), 未安装为 0. + val packageSize: Long = 0L, + + // Installable package metadata (from index or network detection). + // zh-CN: 可安装包元信息 (来自索引或网络探测). + val installableApkUrl: String? = null, + val installableApkSha256: String? = null, + val installableApkSizeBytes: Long? = null, + val icon: Drawable? = null, + var isEnabled: Boolean = true, + val isInstalled: Boolean, + val firstInstallTime: Long? = null, + val lastUpdateTime: Long? = null, val settings: PluginCenterItemSettings? = null, -) +) { + val versionSummary: String + get() = formatVersionInfo(versionName, versionCode, versionDate) + + val updatableVersionSummary: String? + get() = updatableVersionName?.let { formatVersionInfo(it, updatableVersionCode, updatableVersionDate) } + + val isUpdatable: Boolean + get() = updatableVersionName != null + + var lastInstallTime: Long? + get() = PluginRecentStore.getLastInstalled(packageName) + set(value) = PluginRecentStore.setLastInstalled(packageName, value ?: System.currentTimeMillis()) + + var lastUninstallTime: Long? + get() = PluginRecentStore.getLastUninstalled(packageName) + set(value) = PluginRecentStore.setLastUninstalled(packageName, value ?: System.currentTimeMillis()) + + private fun formatVersionInfo(versionName: String, versionCode: Long?, versionDate: String?): String { + val code = versionCode?.takeIf { it > 0 } + val date = versionDate?.runCatching { + DateTimeFormat.forPattern("yyyy-MM-dd").print(DateTime.parse(this)) + }?.getOrNull() + return buildString { + append(versionName) + code?.let { append(" ($it)") } + date?.let { append(" | $it") } + } + } + +} diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterItemAdapter.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterItemAdapter.kt index b6ea67a6..6ca59f66 100644 --- a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterItemAdapter.kt +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterItemAdapter.kt @@ -7,13 +7,15 @@ import androidx.recyclerview.widget.RecyclerView import org.autojs.autojs6.databinding.PluginCenterRecyclerViewItemBinding @SuppressLint("NotifyDataSetChanged") -class PluginCenterItemAdapter : RecyclerView.Adapter() { +class PluginCenterItemAdapter( + private val listener: Listener, +) : RecyclerView.Adapter() { internal var items = emptyList() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PluginCenterItemViewHolder { val binding = PluginCenterRecyclerViewItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) - return PluginCenterItemViewHolder(binding) + return PluginCenterItemViewHolder(binding, listener) } override fun onBindViewHolder(holder: PluginCenterItemViewHolder, position: Int) { @@ -29,4 +31,10 @@ class PluginCenterItemAdapter : RecyclerView.Adapter notifyDataSetChanged() } + interface Listener { + fun onToggleEnable(item: PluginCenterItem, enabled: Boolean) + fun onUninstall(item: PluginCenterItem) + fun onDetails(item: PluginCenterItem) + } + } \ No newline at end of file diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterItemViewHolder.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterItemViewHolder.kt index 4fcbb7e1..fe573e17 100644 --- a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterItemViewHolder.kt +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterItemViewHolder.kt @@ -1,8 +1,6 @@ package org.autojs.autojs.core.plugin.center import android.content.res.ColorStateList -import android.graphics.ColorMatrix -import android.graphics.ColorMatrixColorFilter import android.graphics.PorterDuff import android.view.View import android.widget.ImageView @@ -17,12 +15,16 @@ import de.hdodenhof.circleimageview.CircleImageView import org.autojs.autojs.theme.ThemeColorManager import org.autojs.autojs.util.ColorUtils import org.autojs.autojs.util.ViewUtils +import org.autojs.autojs.util.ViewUtils.colorFilterWithDesaturateOrNull import org.autojs.autojs6.R import org.autojs.autojs6.databinding.PluginCenterRecyclerViewItemBinding import org.joda.time.DateTime import org.joda.time.format.DateTimeFormat -class PluginCenterItemViewHolder(itemViewBinding: PluginCenterRecyclerViewItemBinding) : RecyclerView.ViewHolder(itemViewBinding.root) { +class PluginCenterItemViewHolder( + itemViewBinding: PluginCenterRecyclerViewItemBinding, + private val listener: PluginCenterItemAdapter.Listener, +) : RecyclerView.ViewHolder(itemViewBinding.root) { private val context = itemViewBinding.root.context @@ -51,7 +53,11 @@ class PluginCenterItemViewHolder(itemViewBinding: PluginCenterRecyclerViewItemBi private val btnSettingsView = itemViewBinding.btnSettings private val btnDetailsView = itemViewBinding.btnDetails + private lateinit var currentItem: PluginCenterItem + fun bind(item: PluginCenterItem) { + currentItem = item + item.icon?.let { iconView.setImageDrawable(it) } ?: AppCompatResources.getDrawable( iconView.context, R.drawable.ic_plugin_center_default @@ -61,48 +67,67 @@ class PluginCenterItemViewHolder(itemViewBinding: PluginCenterRecyclerViewItemBi iconView.setImageDrawable(d) } ?: iconView.setImageResource(R.mipmap.ic_app_shortcut_plugin_center_adaptive_round) + switchView.setOnCheckedChangeListener(null) switchView.isChecked = item.isEnabled titleView.text = item.title - versionInfoView.text = formatVersionInfo(item.versionName, item.versionCode, item.versionDate) + versionInfoView.text = item.versionSummary authorView.text = item.author descriptionView.text = item.description - btnDeleteView.setButtonState(true) { - ViewUtils.showToast(context, R.string.text_under_development) + if (item.isInstalled) { + btnDeleteView.setButtonState(true) { + listener.onUninstall(currentItem) + } + } else { + btnDeleteView.setButtonState(false) } - if (item.isUpdatable) { + + if (item.isInstalled && item.isUpdatable) { updatableBadgeView.isVisible = true versionInfoForUpdateView.isVisible = true - versionInfoForUpdateView.text = formatVersionInfo(item.versionName, item.versionCode?.let { it + 16 }, item.versionDate?.let { + + // test + val updatableVersionName = item.versionName.also { + item.updatableVersionName = it + } + // test + val updatableVersionCode = item.versionCode?.let { it + 16 }?.also { + item.updatableVersionCode = it + } + // test + val updatableVersionDate = item.versionDate?.let { DateTime.parse(it).plusDays(3).toString("yyyy-MM-dd") - }) + }?.also { + item.updatableVersionDate = it + } + + versionInfoForUpdateView.text = formatVersionInfo(updatableVersionName, updatableVersionCode, updatableVersionDate) btnUpdateView.setButtonState(true) { ViewUtils.showToast(context, R.string.text_under_development) } } else { updatableBadgeView.isVisible = false versionInfoForUpdateView.isVisible = false - btnUpdateView.setButtonState(false) { - ViewUtils.showToast(context, R.string.text_unavailable) - } + btnUpdateView.setButtonState(false) } + if (item.settings != null) { btnSettingsView.setButtonState(true) { ViewUtils.showToast(context, R.string.text_under_development) } } else { - btnSettingsView.setButtonState(false) { - ViewUtils.showToast(context, R.string.text_unavailable) - } + btnSettingsView.setButtonState(false) } + btnDetailsView.setButtonState(true) { - ViewUtils.showToast(context, R.string.text_under_development) + listener.onDetails(currentItem) } applyUiBySwitch(switchView.isChecked, item) switchView.setOnCheckedChangeListener { _, isChecked -> + listener.onToggleEnable(currentItem, isChecked) applyUiBySwitch(isChecked, item) } } @@ -119,8 +144,8 @@ class PluginCenterItemViewHolder(itemViewBinding: PluginCenterRecyclerViewItemBi } } - private fun LinearLayout.setButtonState(enabled: Boolean, onClickListener: View.OnClickListener) { - isEnabled = enabled + private fun LinearLayout.setButtonState(enabled: Boolean, onClickListener: View.OnClickListener? = null) { + this.isEnabled = enabled this.setOnClickListener(onClickListener) } @@ -130,7 +155,11 @@ class PluginCenterItemViewHolder(itemViewBinding: PluginCenterRecyclerViewItemBi val colorPrimaryA30 = context.getColor(R.color.text_color_primary_alpha_30) val colorPrimaryA20 = context.getColor(R.color.text_color_primary_alpha_20) - btnDeleteView.setActionColors(iconColor = colorPrimaryA50, textColor = colorPrimary) + if (btnDeleteView.isEnabled) { + btnDeleteView.setActionColors(iconColor = colorPrimaryA50, textColor = colorPrimary) + } else { + btnDeleteView.setActionColors(iconColor = colorPrimaryA20, textColor = colorPrimaryA30) + } if (btnUpdateView.isEnabled) { if (isOn) { @@ -158,20 +187,7 @@ class PluginCenterItemViewHolder(itemViewBinding: PluginCenterRecyclerViewItemBi updatableBadgeTextView.setTextColor(colorPrimary) } - if (isOn) { - iconView.colorFilter = null - } else { - // Construct the desaturation matrix. - // zh-CN: 构造灰度矩阵. - val desaturate = ColorMatrix().apply { setSaturation(0f) } - // Construct the alpha scaling matrix. - // zh-CN: 构造透明度缩放矩阵. - val alphaMatrix = ColorMatrix().apply { setScale(1f, 1f, 1f, 0.5f) } - // Concatenate: first desaturate, then apply alpha. - // zh-CN: 叠加: 先灰度, 再透明度. - desaturate.postConcat(alphaMatrix) - iconView.colorFilter = ColorMatrixColorFilter(desaturate) - } + iconView.colorFilterWithDesaturateOrNull(isOn, 0.5F) } private fun LinearLayout.setActionColors(iconColor: Int, textColor: Int) { 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 9111b3ee..e72959f1 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 @@ -19,6 +19,7 @@ class PluginCenterViewModel : ViewModel() { private val indexRepo = PluginIndexRepository() private val installedRepo = InstalledPluginRepository() + private val enableStore = PluginEnableStore() private val _items = MutableStateFlow>(emptyList()) val items: StateFlow> = _items @@ -47,9 +48,23 @@ class PluginCenterViewModel : ViewModel() { .map { local -> toPluginCenterItem(context, index = null, local = local) } _items.value = fromIndex + extraLocals + + // Refresh dialog if showing when returning to the page. + // zh-CN: 返回页面时, 对话框如果正在显示则刷新. + PluginInfoDialogManager.refreshIfShowing(context, _items.value) } } + fun setEnabled(context: Context, packageName: String, enabled: Boolean) { + enableStore.setEnabled(context, packageName, enabled) + // Update in-memory state too, to avoid a second full refresh. + // zh-CN: 内存态也更新, 避免二次全量刷新. + _items.value = _items.value.map { + if (it.packageName == packageName) it.copy(isEnabled = enabled) else it + } + PluginInfoDialogManager.refreshIfShowing(context, _items.value) + } + private fun toPluginCenterItem(context: Context, index: PluginIndexEntry?, local: InstalledPluginRepository.InstalledPlugin?): PluginCenterItem { val packageName = local?.packageName ?: index?.packageName.orEmpty() val title = local?.title ?: index?.title ?: packageName @@ -58,25 +73,34 @@ class PluginCenterViewModel : ViewModel() { val collaborators = index?.collaborators ?: emptyList() val versionName = local?.versionName ?: index?.versionName ?: context.getString(R.string.text_unknown) - val localCode = local?.versionCode - val indexCode = index?.versionCode - val isInstalled = local != null - val isUpdatable = isInstalled && (indexCode != null && indexCode > (localCode ?: -1)) + val enabled = enableStore.isEnabled(context, packageName, defaultEnabled = isInstalled) return PluginCenterItem( - packageName = packageName, title = title, - description = description, + packageName = packageName, + versionName = versionName, + versionCode = local?.versionCode ?: index?.versionCode, + // TODO M1: 显示索引日期; 仅本地项时可为空. + versionDate = index?.versionDate, + updatableVersionName = index?.versionName, + updatableVersionCode = index?.versionCode, + updatableVersionDate = index?.versionDate, author = author, collaborators = collaborators, - versionName = versionName, - versionCode = localCode ?: indexCode, - versionDate = index?.versionDate, // M1: 显示索引日期; 仅本地项时可为空 - isEnabled = true, // M1: 先统一 true, M2 再接入启用状态持久化 - isUpdatable = isUpdatable, - icon = local?.icon, // 已安装优先用应用图标; 未安装走默认占位图 - settings = null, // M1 暂不接入单插件设置入口 + description = description, + packageSize = local?.packageSize ?: 0, + installableApkUrl = index?.apkUrl, + installableApkSha256 = index?.apkSha256, + installableApkSizeBytes = index?.apkSizeBytes, + // TODO 已安装优先用应用图标; 未安装走默认占位图. + icon = local?.icon, + isEnabled = enabled, + isInstalled = isInstalled, + firstInstallTime = local?.firstInstallTime, + lastUpdateTime = local?.lastUpdateTime, + // TODO M1 暂不接入单插件设置入口. + settings = null, ) } } 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 new file mode 100644 index 00000000..131c9c73 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginEnableStore.kt @@ -0,0 +1,22 @@ +package org.autojs.autojs.core.plugin.center + +import android.content.Context +import androidx.core.content.edit + +class PluginEnableStore { + + private val spName = "plugin_center_enable_state" + + fun isEnabled(context: Context, packageName: String, defaultEnabled: Boolean = true): Boolean { + val sp = context.getSharedPreferences(spName, 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) + sp.edit { putBoolean(key(packageName), enabled) } + } + + private fun key(packageName: String) = "key_\$_enabled_plugin_\$_$packageName" + +} 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 0fc65493..af3fe0d5 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 @@ -16,12 +16,16 @@ data class PluginIndexEntry( val engine: String? = null, /** @sample "v5" */ val variant: String? = null, - /** @sample "paddle-ocr-v5" */ + /** @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 tags: List = emptyList(), ) 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 d62fd9f0..63fa88e2 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 @@ -8,7 +8,7 @@ import android.content.Context class PluginIndexRepository { suspend fun fetchOfficialIndex(context: Context): List { - // M1 先预置 1 条官方样例 "Paddle OCR (PP-OCRv5)", 便于与本地已安装合并显示. + // TODO M1 先预置 1 条官方样例 "Paddle OCR (PP-OCRv5)", 便于与本地已安装合并显示. return listOf( PluginIndexEntry( packageName = "io.github.supermonster003.autojs6.plugin.paddleocr.v5", @@ -19,11 +19,16 @@ class PluginIndexRepository { versionName = "0.1.0", versionCode = 17L, versionDate = "2025-11-21", - iconUrl = null, // M1 暂不拉网图标, 使用应用图标或默认图标. + // TODO M1 若索引的下载地址/哈希/尺寸未知, 则先设置为 null. + apkUrl = null, + apkSha256 = null, + apkSizeBytes = null, + // TODO M1 暂不拉网图标, 使用应用图标或默认图标. + iconUrl = null, tags = listOf("official", "ocr", "paddle", "v5"), engine = "paddle-ocr", variant = "v5", - engineId = "paddle-ocr-v5", + engineId = "paddle-ocr-pp-ocrv5", ), ) } 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 new file mode 100644 index 00000000..509f5c85 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginInfoDialogManager.kt @@ -0,0 +1,333 @@ +package org.autojs.autojs.core.plugin.center + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.graphics.PorterDuff +import android.view.LayoutInflater +import android.view.View.MeasureSpec.UNSPECIFIED +import android.widget.TextView +import androidx.appcompat.content.res.AppCompatResources +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.graphics.drawable.DrawableCompat +import androidx.core.net.toUri +import androidx.core.view.isVisible +import com.afollestad.materialdialogs.DialogAction +import com.afollestad.materialdialogs.MaterialDialog +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.autojs.autojs.extension.MaterialDialogExtensions.makeSettingsLaunchable +import org.autojs.autojs.extension.MaterialDialogExtensions.makeTextCopyable +import org.autojs.autojs.extension.MaterialDialogExtensions.setCopyableTextIfAbsent +import org.autojs.autojs.runtime.api.augment.converter.core.Bytes +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.colorFilterWithDesaturateOrNull +import org.autojs.autojs.util.ViewUtils.toCircular +import org.autojs.autojs6.R +import org.autojs.autojs6.databinding.PluginInfoDialogItemsBinding +import java.lang.ref.WeakReference +import kotlin.math.roundToInt + +object PluginInfoDialogManager { + + // Hold the current dialog and package name for refreshing on onResume. + // zh-CN: 持有当前对话框与包名, 便于 onResume 时刷新. + private var currentDialog: WeakReference? = null + private var currentPackageName: String? = null + + fun refreshIfShowing(context: Context, allItems: List) { + val dialog = currentDialog?.get() ?: return + val pkg = currentPackageName ?: return + if (!dialog.isShowing) return + val target = allItems.firstOrNull { it.packageName == pkg } ?: return + dialog.dismiss() + showPluginInfoDialog(context, target) + } + + @JvmStatic + fun showPluginInfoDialog(context: Context, item: PluginCenterItem) { + if (item.isInstalled) { + showInstalledPluginInfoDialog(context, item) + } else { + showInstallablePluginInfoDialog(context, item) + } + } + + private fun showInstallablePluginInfoDialog(context: Context, item: PluginCenterItem) { + val states = listOf(context.getString(R.string.text_installable)) + val info = PluginInfoInstallable( + title = item.title, + 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, + ) + showPluginInfoDialogInternal(context, item, info) + } + + private fun showInstalledPluginInfoDialog(context: Context, item: PluginCenterItem) { + val enabledRes = if (item.isEnabled) R.string.text_enabled else R.string.text_disabled + val states = mutableListOf(context.getString(enabledRes)).apply { + if (item.isUpdatable) add(context.getString(R.string.text_updatable)) + } + val info = PluginInfoInstalled( + title = item.title, + 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, + ) + showPluginInfoDialogInternal(context, item, info) + } + + private fun showPluginInfoDialogInternal(context: Context, item: PluginCenterItem, info: PluginInfoBase) { + 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() + .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, _ -> + info.apkUrl?.let { url -> + d.dismiss() + CoroutineScope(Dispatchers.IO).launch { + PluginInstaller.installFromUrlWithPrompt(context, url, item.installableApkSha256) + } + } ?: run { + val positiveButton = d.getActionButton(DialogAction.POSITIVE) + positiveButton.setTextColor(d.context.getColor(R.color.dialog_button_unavailable)) + MaterialDialog.Builder(d.context) + .title(R.string.text_failed_to_install) + .content(d.context.getString(R.string.error_no_available_url_provided_for_current_plugin)) + } + } + } + is PluginInfoInstalled -> { + positiveText(R.string.text_uninstall) + positiveColorRes(R.color.dialog_button_warn) + onPositive { d, _ -> + // TODO 确定卸载对话框. + d.dismiss() + context.startActivity(Intent(Intent.ACTION_DELETE, "package:${item.packageName}".toUri())) + } + if (item.isUpdatable) { + neutralText(R.string.text_update) + neutralColorRes(R.color.dialog_button_attraction) + onNeutral { d, _ -> + d.dismiss() + // TODO 后续实现更新逻辑 (下载新包 -> 安装). + } + } + } + } + } + .show() + .apply { + makeTextCopyable { titleView } + } + + // Hold the current dialog and package name for refreshing on onResume. + // zh-CN: 记录 "当前对话框" 与包名, 便于 onResume 刷新. + currentDialog = WeakReference(dialog) + currentPackageName = item.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 + } + + dialog.setCopyableTextIfAbsent(binding.packageNameValue, info.packageName) + dialog.setCopyableTextIfAbsent(binding.versionValue, info.version) + dialog.setCopyableTextIfAbsent(binding.pluginItemInfoAuthorValue, info.author) + 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 adjustedImageContrastColor = ColorUtils.adjustColorForContrast(context.getColor(R.color.window_background), ThemeColorManager.colorPrimary, 2.3) + DrawableCompat.setTint(d, adjustedImageContrastColor) + DrawableCompat.setTintMode(d, PorterDuff.Mode.SRC_IN) + } ?: AppCompatResources.getDrawable(context, R.mipmap.ic_app_shortcut_plugin_center_adaptive_round) + + if (dialogIcon != null) { + dialog.setIcon( + dialogIcon.toCircular( + context = context, + sizePx = DisplayUtils.dpToPx(48.0F).roundToInt(), + borderWidthPx = context.resources.getDimensionPixelSize(R.dimen.plugin_center_item_icon_border_width), + 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) + } + } + + // Installable package: If the index does not provide size, try to HEAD request to get it, update display after success. + // zh-CN: 可安装包: 若索引未给 size, 尝试 HEAD 获取, 成功后更新显示. + if (info is PluginInfoInstallable && info.packageSize <= 0 && !info.apkUrl.isNullOrBlank()) { + // Asynchronously probe the size and refresh the view. + // zh-CN: 异步探测大小并刷新视图. + CoroutineScope(Dispatchers.IO).launch { + val size = PluginInstaller.probeContentLength(info.apkUrl) + if (size != null && size > 0 && currentDialog?.get() === dialog) { + // TODO 更新当前 item 的 "可安装包大小" 仅用于对话框展示 (持久化可留到 M2). + withContext(Dispatchers.Main) { + dialog.setCopyableTextIfAbsent(binding.pluginItemInfoPackageSizeValue, formatSize(size)) + } + } + } + } + } + + @SuppressLint("SetTextI18n") + private fun restoreEssentialViews(binding: PluginInfoDialogItemsBinding, context: Context, info: PluginInfoBase) { + if (info.collaborators.isNotEmpty()) { + binding.pluginItemInfoCollaboratorsFirstLabel.text = "${context.getString(R.string.plugin_item_info_collaborators)} [1/${info.collaborators.size}]" + binding.pluginItemInfoCollaboratorsFirstValue.text = info.collaborators[0] + binding.pluginItemInfoCollaboratorsFirstParent.isVisible = true + } + if (info.collaborators.size > 1) { + binding.pluginItemInfoCollaboratorsSecondLabel.text = "${context.getString(R.string.plugin_item_info_collaborators)} [2/${info.collaborators.size}]" + binding.pluginItemInfoCollaboratorsSecondValue.text = info.collaborators[1] + binding.pluginItemInfoCollaboratorsSecondParent.isVisible = true + } + if (info.collaborators.size > 2) { + binding.pluginItemInfoCollaboratorsThirdLabel.text = "${context.getString(R.string.plugin_item_info_collaborators)} [3/${info.collaborators.size}]" + binding.pluginItemInfoCollaboratorsThirdValue.text = info.collaborators[2] + binding.pluginItemInfoCollaboratorsThirdParent.isVisible = true + } + when (info) { + is PluginInfoInstalled -> { + info.updatableVersion?.let { + binding.versionLabel.text = context.getString(R.string.plugin_item_info_installed_version) + binding.updatableVersionValue.text = it + binding.updatableVersionParent.isVisible = true + } + info.firstInstallTime?.setupListItemView(binding.pluginItemInfoFirstInstallTimeParent, binding.pluginItemInfoFirstInstallTimeValue) + info.lastUpdateTime?.setupListItemView(binding.pluginItemInfoLastUpdateTimeParent, binding.pluginItemInfoLastUpdateTimeValue) + } + is PluginInfoInstallable -> { + info.lastInstallTime?.setupListItemView(binding.pluginItemInfoLastInstallTimeParent, binding.pluginItemInfoLastInstallTimeValue) + info.lastUninstallTime?.setupListItemView(binding.pluginItemInfoLastUninstallTimeParent, binding.pluginItemInfoLastUninstallTimeValue) + } + } + } + + private fun updateGuidelines(binding: PluginInfoDialogItemsBinding) { + val filteredBindings = listOf( + binding.stateLabel to binding.stateGuideline, + binding.packageNameLabel to binding.packageNameGuideline, + binding.versionLabel to binding.versionGuideline, + binding.updatableVersionLabel to binding.updatableVersionGuideline, + binding.pluginItemInfoAuthorLabel to binding.pluginItemInfoAuthorGuideline, + binding.pluginItemInfoCollaboratorsFirstLabel to binding.pluginItemInfoCollaboratorsFirstGuideline, + binding.pluginItemInfoCollaboratorsSecondLabel to binding.pluginItemInfoCollaboratorsSecondGuideline, + binding.pluginItemInfoCollaboratorsThirdLabel to binding.pluginItemInfoCollaboratorsThirdGuideline, + binding.descriptionLabel to binding.descriptionGuideline, + binding.pluginItemInfoPackageSizeLabel to binding.pluginItemInfoPackageSizeGuideline, + binding.pluginItemInfoFirstInstallTimeLabel to binding.pluginItemInfoFirstInstallTimeGuideline, + binding.pluginItemInfoLastUpdateTimeLabel to binding.pluginItemInfoLastUpdateTimeGuideline, + binding.pluginItemInfoLastInstallTimeLabel to binding.pluginItemInfoLastInstallTimeGuideline, + binding.pluginItemInfoLastUninstallTimeLabel to binding.pluginItemInfoLastUninstallTimeGuideline, + ).filter { (it.first.parent as? ConstraintLayout)?.isVisible == true } + + @Suppress("DuplicatedCode") + val maxWidth = filteredBindings.maxOfOrNull { it.first.apply { measure(UNSPECIFIED, UNSPECIFIED) }.measuredWidth } ?: return + + filteredBindings.forEach { (_, guideline) -> + guideline.layoutParams = (guideline.layoutParams as ConstraintLayout.LayoutParams).also { + it.guideBegin = maxWidth + } + } + } + + private fun Long.setupListItemView(parentView: ConstraintLayout, valueView: TextView) { + this.takeIf { it > 0 }?.let { + valueView.text = TimeUtils.formatTimestamp(it) + parentView.isVisible = true + } + } + + private fun formatSize(size: Long): String = Bytes.string( + source = size.toDouble(), + fromUnit = "B", + toUnit = "AUTO", + useIecIdentifier = true, + useSpace = true, + fractionDigits = 1, + trimTrailingZero = false, + signature = "pluginItemInfo.getPackageSize", + ) + + 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 packageSize: Long + } + + private data class PluginInfoInstallable( + override val title: String, + 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, + val apkUrl: String?, + val lastInstallTime: Long?, + val lastUninstallTime: Long?, + ) : PluginInfoBase + + private data class PluginInfoInstalled( + override val title: String, + 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, + val updatableVersion: String? = null, + val firstInstallTime: Long?, + val lastUpdateTime: Long?, + ) : PluginInfoBase + +} 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 new file mode 100644 index 00000000..5fad3a5a --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginInstaller.kt @@ -0,0 +1,246 @@ +package org.autojs.autojs.core.plugin.center + +import android.content.Context +import android.content.Intent +import android.content.res.ColorStateList +import android.net.Uri +import androidx.core.content.FileProvider +import com.afollestad.materialdialogs.DialogAction +import com.afollestad.materialdialogs.MaterialDialog +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.autojs.autojs.network.download.DownloadManager +import org.autojs.autojs.runtime.api.Mime +import org.autojs.autojs.ui.error.ErrorDialogActivity +import org.autojs.autojs.ui.main.scripts.ApkInfoDialogManager +import org.autojs.autojs.util.FileUtils +import org.autojs.autojs.util.FileUtils.toCacheFile +import org.autojs.autojs6.R +import org.spongycastle.pqc.math.linearalgebra.IntegerFunctions.pow +import java.io.EOFException +import java.io.File +import java.io.IOException +import java.net.HttpURLConnection +import java.net.URL +import java.security.DigestInputStream +import java.security.MessageDigest +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.math.roundToInt + +/** + * Installer: + * - Local file: Uri installation (display APK info dialog before installation) + * - URL: Install after downloading to cache (display progress dialog during download) + * + * zh-CN: + * + * 安装器: + * - 本地文件: Uri 安装 (安装前显示 APK 信息对话框) + * - URL: 下载到缓存后安装 (下载时显示进度对话框) + */ +object PluginInstaller { + + suspend fun installFromFileUriWithPrompt(context: Context, uri: Uri) = runCatching { + ApkInfoDialogManager.showApkInfoDialog(context, uri.toCacheFile(context)) { + onPositive { dialog, _ -> + dialog.dismiss() + if (FileUtils.isLikelyApk(context, uri)) { + installFromFileUri(context, uri) + return@onPositive + } + 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) + .negativeColorRes(R.color.dialog_button_default) + .onNegative { d, _ -> d.dismiss() } + .positiveText(R.string.dialog_button_continue) + .positiveColorRes(R.color.dialog_button_not_recommended) + .onPositive { d, _ -> + d.dismiss() + installFromFileUri(context, uri) + } + .autoDismiss(false) + .cancelable(false) + .show() + } + } + }.onFailure { e -> + ErrorDialogActivity.showErrorDialog( + context.applicationContext, + R.string.text_failed_to_install, + e.message ?: e.toString(), + ) + } + + fun installFromFileUri(context: Context, uri: Uri) { + val intent = Intent(Intent.ACTION_VIEW).apply { + setDataAndType(uri, Mime.APPLICATION_VND_ANDROID_PACKAGE_ARCHIVE) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + context.startActivity(intent) + } + + suspend fun installFromUrlWithPrompt(context: Context, url: String, expectedSha256: String? = null) { + when (val result = downloadWithProgress(context, url, expectedSha256)) { + is DownloadResult.Success -> installFromFileUriWithPrompt(context, result.uri) + is DownloadResult.Failure -> ErrorDialogActivity.showErrorDialog( + context, + result.titleRes, + result.message, + ) + is DownloadResult.Cancelled -> { + // User cancelled, no need to prompt. + // zh-CN: 用户取消, 无需提示. + } + } + } + + suspend fun installFromUrl(context: Context, url: String, expectedSha256: String? = null) { + val result = downloadWithProgress(context, url, expectedSha256) + if (result is DownloadResult.Success) { + installFromFileUri(context, result.uri) + } else if (result is DownloadResult.Failure) { + ErrorDialogActivity.showErrorDialog(context, result.titleRes, result.message) + } + } + + // Probe URL size (HEAD). + // zh-CN: 探测 URL 大小 (HEAD). + suspend fun probeContentLength(url: String): Long? = withContext(Dispatchers.IO) { + val conn = (URL(url).openConnection() as HttpURLConnection).apply { + requestMethod = "HEAD" + connectTimeout = 12_000 + readTimeout = 12_000 + } + runCatching { + conn.connect() + val code = conn.responseCode + if (code in 200..299) { + val len = conn.getHeaderFieldLong("Content-Length", -1L) + if (len > 0) len else null + } else null + }.onFailure { conn.disconnect() } + .also { conn.disconnect() } + .getOrNull() + } + + private suspend fun downloadWithProgress( + context: Context, + url: String, + expectedSha256: String?, + ): DownloadResult { + val cancelFlag = AtomicBoolean(false) + val dialog = MaterialDialog.Builder(context) + .title(R.string.text_downloading) + .progress(false, 100, true) + .negativeText(R.string.text_cancel) + .negativeColorRes(R.color.dialog_button_default) + .onNegative { d, _ -> + cancelFlag.set(true) + d.getActionButton(DialogAction.NEGATIVE).isEnabled = false + } + .cancelable(false) + .autoDismiss(false) + .show() + + dialog.setProgressNumberFormat(context.getString(R.string.text_half_ellipsis)) + dialog.setProgress(0) + + val progressBar = dialog.getProgressBar() + progressBar.setProgressTintList(ColorStateList.valueOf(context.getColor(R.color.dialog_progress_download_tint))) + progressBar.setProgressBackgroundTintList(ColorStateList.valueOf(context.getColor(R.color.dialog_progress_download_bg_tint))) + + try { + val cache = File(context.cacheDir, "plugin_dl").apply { if (!exists()) mkdirs() } + val name = guessFileName(url) + val out = File(cache, name) + + val (len, sha256Hex) = withContext(Dispatchers.IO) { + val conn = (URL(url).openConnection() as HttpURLConnection).apply { + connectTimeout = 15_000 + readTimeout = 30_000 + } + conn.connect() + val code = conn.responseCode + if (code !in 200..299) throw HttpStatusException(code, conn.responseMessage ?: "HTTP error") + val total = conn.contentLengthLong.takeIf { it > 0 } ?: -1L + + conn.inputStream.use { input -> + val md = MessageDigest.getInstance("SHA-256") + DigestInputStream(input, md).use { din -> + out.outputStream().use { fos -> + val buf = ByteArray(DEFAULT_BUFFER_SIZE) + var read: Int + var downloaded = 0L + var lastUpdateTs = 0L + while (true) { + if (cancelFlag.get()) throw CancellationException("User cancelled") + read = din.read(buf) + if (read == -1) break + fos.write(buf, 0, read) + downloaded += read + val now = System.currentTimeMillis() + if (total > 0 && (now - lastUpdateTs > 80)) { + val pct = ((downloaded * 100f) / total).coerceIn(0f, 100f) + withContext(Dispatchers.Main) { + dialog.setProgressNumberFormat(DownloadManager.getProgressMegaBytesFormat( + context, + downloaded.toFloat() / pow(2, 20), + total.toFloat() / pow(2, 20), + )) + dialog.setProgress(pct.roundToInt()) + } + lastUpdateTs = now + } + } + fos.flush() + } + } + val hex = md.digest().joinToString("") { "%02x".format(it) } + Pair(if (total > 0) total else out.length(), hex) + } + } + + if (expectedSha256 != null && !expectedSha256.equals(sha256Hex, ignoreCase = true)) { + throw ChecksumMismatchException(expectedSha256, sha256Hex) + } + + val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", out) + return DownloadResult.Success(uri, len, sha256Hex) + } catch (_: CancellationException) { + return DownloadResult.Cancelled + } catch (he: HttpStatusException) { + return DownloadResult.Failure(R.string.text_failed_to_retrieve, "HTTP ${he.code}: ${he.message}") + } catch (me: ChecksumMismatchException) { + return DownloadResult.Failure(R.string.text_integrity_verification_failed, context.getString(R.string.text_sha256_mismatch_multiline_expected_actual, me.expected, me.actual)) + } catch (ioe: EOFException) { + return DownloadResult.Failure(R.string.text_failed_to_retrieve, "Unexpected EOF: ${ioe.message}") + } catch (ioe: IOException) { + return DownloadResult.Failure(R.string.text_failed_to_retrieve, "Network/IO error: ${ioe.message}") + } catch (e: SecurityException) { + return DownloadResult.Failure(R.string.text_failed_to_install, "Security error: ${e.message}") + } catch (e: Throwable) { + return DownloadResult.Failure(R.string.text_failed_to_retrieve, e.message ?: e.toString()) + } finally { + dialog.dismiss() + } + } + + private fun guessFileName(url: String): String { + val last = url.substringAfterLast('/').substringBefore('?') + require(last.isNotBlank()) { "Invalid url: $url" } + return if (last.endsWith(".apk", ignoreCase = true)) last else "$last.apk" + } + + private data class HttpStatusException(val code: Int, override val message: String) : RuntimeException(message) + private data class ChecksumMismatchException(val expected: String, val actual: String) : RuntimeException("sha256 mismatch") + + sealed interface DownloadResult { + data class Success(val uri: Uri, val length: Long, val sha256: String) : DownloadResult + data class Failure(val titleRes: Int, val message: String) : DownloadResult + data object Cancelled : DownloadResult + } +} \ No newline at end of file diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginRecentStore.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginRecentStore.kt new file mode 100644 index 00000000..c314ed75 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginRecentStore.kt @@ -0,0 +1,37 @@ +package org.autojs.autojs.core.plugin.center + +import android.content.Context +import androidx.core.content.edit +import org.autojs.autojs.app.GlobalAppContext + +object PluginRecentStore { + + private const val SP = "plugin_center_recent" + + private val context by lazy { GlobalAppContext.get() } + + fun setLastInstalled(packageName: String, ts: Long = System.currentTimeMillis()) { + context.getSharedPreferences(SP, Context.MODE_PRIVATE).edit { + putLong(keyInstalled(packageName), ts) + } + } + + fun setLastUninstalled(packageName: String, ts: Long = System.currentTimeMillis()) { + context.getSharedPreferences(SP, Context.MODE_PRIVATE).edit { + putLong(keyUninstalled(packageName), ts) + } + } + + fun getLastInstalled(packageName: String): Long? { + return context.getSharedPreferences(SP, Context.MODE_PRIVATE).getLong(keyInstalled(packageName), 0L).takeIf { it > 0 } + } + + fun getLastUninstalled(packageName: String): Long? { + return context.getSharedPreferences(SP, Context.MODE_PRIVATE).getLong(keyUninstalled(packageName), 0L).takeIf { it > 0 } + } + + private fun keyInstalled(packageName: String) = "key_\$_last_install_time_\$_$packageName" + + private fun keyUninstalled(packageName: String) = "key_\$_last_uninstall_time_\$_$packageName" + +} \ No newline at end of file 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 871eabd5..e4fd360c 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 @@ -99,7 +99,7 @@ object PaddleOcrPluginHost { suspend fun select( context: Context, - // e.g. "paddle-ocr-v5" + // e.g. "paddle-ocr-pp-ocrv5" engineId: String? = null, // e.g. "paddle-ocr" engine: String? = null, diff --git a/app/src/main/java/org/autojs/autojs/core/pref/Pref.kt b/app/src/main/java/org/autojs/autojs/core/pref/Pref.kt index fbd846a7..cb5a29f1 100644 --- a/app/src/main/java/org/autojs/autojs/core/pref/Pref.kt +++ b/app/src/main/java/org/autojs/autojs/core/pref/Pref.kt @@ -100,7 +100,7 @@ object Pref { return null } val dt = DateTime(ts) - val fmt = DateTimeFormat.forPattern("yyyy/MM/dd HH:mm") + val fmt = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm") return fmt.print(dt) } @@ -313,9 +313,15 @@ object Pref { @JvmStatic fun putLong(@KeyRes keyRes: Int, value: Long) = sPref.edit { putLong(key(keyRes), value) } + @JvmStatic + fun putLong(key: String, value: Long) = sPref.edit { putLong(key, value) } + @JvmStatic fun getLong(@KeyRes keyRes: Int, defValue: Long) = sPref.getLong(key(keyRes), defValue) + @JvmStatic + fun getLong(key: String, defValue: Long) = sPref.getLong(key, defValue) + @JvmStatic fun putStringSet(@KeyRes keyRes: Int, values: MutableSet) = sPref.edit { putStringSet(key(keyRes), values) } 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 64ae64e8..64ca8434 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 @@ -253,13 +253,13 @@ public class DownloadManager { } } - private String getProgressKiloBytesFormat(Context context, float readKiloBytes, float totalKiloBytes) { + public static String getProgressKiloBytesFormat(Context context, float readKiloBytes, float totalKiloBytes) { return String.format(Language.getPrefLanguage().getLocale(), context.getString(R.string.format_dialog_progress_number_format_kilo_bytes), readKiloBytes, totalKiloBytes); } - private String getProgressMegaBytesFormat(Context context, float readMegaBytes, float totalMegaBytes) { + public static String getProgressMegaBytesFormat(Context context, float readMegaBytes, float totalMegaBytes) { return String.format(Language.getPrefLanguage().getLocale(), context.getString(R.string.format_dialog_progress_number_format_mega_bytes), readMegaBytes, totalMegaBytes); diff --git a/app/src/main/java/org/autojs/autojs/ui/main/scripts/ApkInfoDialogManager.kt b/app/src/main/java/org/autojs/autojs/ui/main/scripts/ApkInfoDialogManager.kt index 0c2ab72d..6feaf5f0 100644 --- a/app/src/main/java/org/autojs/autojs/ui/main/scripts/ApkInfoDialogManager.kt +++ b/app/src/main/java/org/autojs/autojs/ui/main/scripts/ApkInfoDialogManager.kt @@ -32,15 +32,16 @@ import org.autojs.autojs.runtime.api.AppUtils import org.autojs.autojs.util.IntentUtils import org.autojs.autojs.util.IntentUtils.ToastExceptionHolder import org.autojs.autojs6.R -import org.autojs.autojs6.databinding.ApkFileInfoDialogListItemBinding +import org.autojs.autojs6.databinding.ApkFileInfoDialogItemsBinding import java.io.File object ApkInfoDialogManager { @JvmStatic + @JvmOverloads @SuppressLint("SetTextI18n") - fun showApkInfoDialog(context: Context, apkFile: File) { - val binding = ApkFileInfoDialogListItemBinding.inflate(LayoutInflater.from(context)) + fun showApkInfoDialog(context: Context, apkFile: File, builderApplier: (MaterialDialog.Builder.() -> Unit)? = null) { + val binding = ApkFileInfoDialogItemsBinding.inflate(LayoutInflater.from(context)) // Create an independent Scope for the Dialog, bind its lifecycle with the Dialog. // zh-CN: 针对 Dialog 独立创建一个 Scope, 生命周期与 Dialog 绑定. @@ -70,6 +71,7 @@ object ApkInfoDialogManager { .negativeColorRes(R.color.dialog_button_default) .neutralColorRes(R.color.dialog_button_hint) .onNegative { materialDialog, _ -> materialDialog.dismiss() } + .also { builder -> builderApplier?.invoke(builder) } .show() .apply { makeTextCopyable { titleView } @@ -192,7 +194,7 @@ object ApkInfoDialogManager { packageManager.getPackageArchiveInfo(apkFilePath, GET_META_DATA) }.getOrNull() - private fun restoreEssentialViews(binding: ApkFileInfoDialogListItemBinding, context: Context) { + private fun restoreEssentialViews(binding: ApkFileInfoDialogItemsBinding, context: Context) { listOf( Triple(binding.labelNameLabel, binding.labelNameColon, binding.labelNameValue) to R.string.text_label_name, Triple(binding.packageNameLabel, binding.packageNameColon, binding.packageNameValue) to R.string.apk_info_package_name, @@ -211,7 +213,7 @@ object ApkInfoDialogManager { } } - private fun updateGuidelines(binding: ApkFileInfoDialogListItemBinding) { + private fun updateGuidelines(binding: ApkFileInfoDialogItemsBinding) { val filteredBindings = listOf( binding.labelNameLabel to binding.labelNameGuideline, binding.packageNameLabel to binding.packageNameGuideline, diff --git a/app/src/main/java/org/autojs/autojs/ui/main/task/Task.java b/app/src/main/java/org/autojs/autojs/ui/main/task/Task.java index 0833a349..140e3c7a 100644 --- a/app/src/main/java/org/autojs/autojs/ui/main/task/Task.java +++ b/app/src/main/java/org/autojs/autojs/ui/main/task/Task.java @@ -71,7 +71,7 @@ public abstract class Task { if (mTimedTask != null) { long nextTime = mTimedTask.getNextTime(mContext); return mContext.getString(R.string.text_next_run_time) + ": " + - DateTimeFormat.forPattern("yyyy/MM/dd HH:mm").print(nextTime); + DateTimeFormat.forPattern("yyyy-MM-dd HH:mm").print(nextTime); } else { assert mIntentTask != null; Integer desc = TimedTaskSettingActivity.ACTION_DESC_MAP.get(mIntentTask.getAction()); diff --git a/app/src/main/java/org/autojs/autojs/util/FileUtils.kt b/app/src/main/java/org/autojs/autojs/util/FileUtils.kt index 285f49f1..d3a7928d 100644 --- a/app/src/main/java/org/autojs/autojs/util/FileUtils.kt +++ b/app/src/main/java/org/autojs/autojs/util/FileUtils.kt @@ -1,9 +1,16 @@ package org.autojs.autojs.util +import android.content.Context +import android.net.Uri +import android.provider.OpenableColumns +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.autojs.autojs.project.ProjectConfig import org.autojs.autojs.util.FileUtils.TYPE.Companion.TYPE_NAME_PREFIX_REGEX import java.io.File import java.util.function.Predicate +import java.util.zip.ZipFile +import java.util.zip.ZipInputStream import kotlin.text.RegexOption.IGNORE_CASE /** @@ -39,6 +46,109 @@ object FileUtils { } } + @JvmStatic + fun probeApk(file: File): ApkProbeResult { + if (!file.isFile || file.length() < 4) { + return ApkProbeResult(false, false, false, false, false) + } + var zipReadable = false + var hasManifest = false + var hasDex = false + var hasArsc = false + var hasRes = false + + try { + ZipFile(file).use { zip -> + zipReadable = true + val entries = zip.entries() + while (entries.hasMoreElements()) { + val e = entries.nextElement() + val name = e.name + when { + name.equals("AndroidManifest.xml", ignoreCase = false) -> hasManifest = true + name.equals("classes.dex", ignoreCase = false) -> hasDex = true + name.equals("resources.arsc", ignoreCase = false) -> hasArsc = true + // Must be a directory entry, or any entry starting with "res/". + // zh-CN: 需要是目录项, 或存在以 "res/" 开头的任何条目. + name.startsWith("res/") -> hasRes = true + } + if (hasManifest && (hasDex || hasArsc || hasRes)) break + } + } + } catch (_: Throwable) { + // Not a valid ZIP or failed to read. + // zh-CN: 不是合法 ZIP 或读取失败. + zipReadable = false + } + + return ApkProbeResult( + isZipReadable = zipReadable, + hasAndroidManifest = hasManifest, + hasClassesDex = hasDex, + hasResourcesArsc = hasArsc, + hasResDir = hasRes, + ) + } + + @JvmStatic + fun isLikelyApk(file: File): Boolean = probeApk(file).isLikelyApk + + @JvmStatic + fun isLikelyApk(context: Context, uri: Uri): Boolean { + context.contentResolver.openInputStream(uri)?.use { input -> + ZipInputStream(input).use { zis -> + var hasManifest = false + var hasDexOrArscOrRes = false + var entry = zis.nextEntry + while (entry != null) { + val name = entry.name + if (name == "AndroidManifest.xml") hasManifest = true + if (name == "classes.dex" || name == "resources.arsc" || name.startsWith("res/")) { + hasDexOrArscOrRes = true + } + if (hasManifest && hasDexOrArscOrRes) return true + entry = zis.nextEntry + } + } + } + return false + } + + suspend fun Uri.toCacheFile( + context: Context, + subDir: String = "from_uri", + preferName: String? = null, + ): File = withContext(Dispatchers.IO) { + val uri = this@toCacheFile + val dir = File(context.cacheDir, subDir).apply { if (!exists()) mkdirs() } + + val finalName = preferName + ?: run getNameFromMeta@{ + context.contentResolver.query( + /* uri = */ uri, + /* projection = */ arrayOf(OpenableColumns.DISPLAY_NAME), + /* selection = */ null, + /* selectionArgs = */ null, + /* sortOrder = */ null, + )?.use { cursor -> + val idx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + if (idx >= 0 && cursor.moveToFirst()) cursor.getString(idx) else null + } + } + ?: uri.lastPathSegment?.substringAfterLast('/') + ?: "temp_${System.currentTimeMillis()}" + + val outFile = File(dir, finalName) + + context.contentResolver.openInputStream(uri)?.use { input -> + outFile.outputStream().use { output -> + input.copyTo(output) + } + } ?: error("Unable to open input stream for uri: $uri") + + outFile + } + private fun withRegexPrefix(regexGetter: () -> String) = TYPE_NAME_PREFIX_REGEX + regexGetter.invoke() // @Hint by SuperMonster003 on Dec 1, 2024. @@ -2907,7 +3017,8 @@ object FileUtils { } - private fun File.isMpeg2TsLike(): Boolean { + @JvmStatic + fun File.isMpeg2TsLike(): Boolean { val magicByte = 0x47.toByte() val tsPacketSize = 188 return runCatching { @@ -2921,29 +3032,35 @@ object FileUtils { }.getOrElse { false } } - private fun File.isTypeScriptLike(): Boolean { + @JvmStatic + fun File.isTypeScriptLike(): Boolean { return checkStartsWith("function", "import", "export", "interface", "class") } - private fun File.isObjectiveCLike(): Boolean { + @JvmStatic + fun File.isObjectiveCLike(): Boolean { return checkContains("@interface", "@implementation", "#import") } - private fun File.isMatlabLike(): Boolean { + @JvmStatic + fun File.isMatlabLike(): Boolean { return checkStartsWith("function", "%", "end") } - private fun File.isGenerateDataLike(): Boolean { + @JvmStatic + fun File.isGenerateDataLike(): Boolean { return checkStartsWith("DATA", "INFO", "HEADER", "META", "RECORD", "BINARY") || checkContains("DATA_TYPE", "VERSION", "FORMAT") } - private fun File.isWavefrontObjLike(): Boolean { + @JvmStatic + fun File.isWavefrontObjLike(): Boolean { return checkContains("Wavefront", "mtllib", "3ds max") || checkRegex(Regex("^v\\s+-?\\d")) } - private fun File.isVcdDataLike() = runCatching { + @JvmStatic + fun File.isVcdDataLike() = runCatching { val riffMagicNumber = byteArrayOf(0x52, 0x49, 0x46, 0x46) // "RIFF" this@isVcdDataLike.inputStream().use { inputStream -> val buffer = ByteArray(4) @@ -2952,7 +3069,8 @@ object FileUtils { } }.getOrElse { false } - private fun File.isBinArchiveLike() = runCatching { + @JvmStatic + fun File.isBinArchiveLike() = runCatching { val zipMagicNumber = byteArrayOf(0x50.toByte(), 0x4B.toByte(), 0x03.toByte(), 0x04.toByte()) val gzipMagicNumber = byteArrayOf(0x1F.toByte(), 0x8B.toByte()) this@isBinArchiveLike.inputStream().use { inputStream -> @@ -2966,7 +3084,8 @@ object FileUtils { false }.getOrElse { false } - private fun File.isBinDiscImageLike() = runCatching { + @JvmStatic + fun File.isBinDiscImageLike() = runCatching { this@isBinDiscImageLike.inputStream().use { inputStream -> val header = ByteArray(2048) // Read first sector if (inputStream.read(header) != header.size) return false @@ -2978,7 +3097,8 @@ object FileUtils { }.getOrElse { false } // Determines if a CUE file describes audio tracks - private fun File.isAudioCueSheetLike() = runCatching { + @JvmStatic + fun File.isAudioCueSheetLike() = runCatching { this@isAudioCueSheetLike.useLines { lines -> lines.any { line -> line.contains(Regex("""(?i)FILE .* (\.wav|\.mp3|\.flac)""")) || // Checks for common audio extensions @@ -2988,7 +3108,8 @@ object FileUtils { }.getOrElse { false } // Determines if a CUE file describes a disk image - private fun File.isDiskImageCueSheetLike() = runCatching { + @JvmStatic + fun File.isDiskImageCueSheetLike() = runCatching { this@isDiskImageCueSheetLike.useLines { lines -> lines.any { line -> line.contains(Regex("""(?i)FILE .* (\.bin|\.iso)""")) || // Checks for common binary extensions @@ -2998,7 +3119,8 @@ object FileUtils { }.getOrElse { false } // Determines if an M3U file is likely an audio playlist - private fun File.isAudioM3ULike() = runCatching { + @JvmStatic + fun File.isAudioM3ULike() = runCatching { this@isAudioM3ULike.useLines { lines -> lines.any { line -> line.contains(Regex("""(?i)\.(mp3|wav|aac|flac|ape|m4a|ogg)$""")) // Checks for common audio file extensions @@ -3007,7 +3129,8 @@ object FileUtils { }.getOrElse { false } // Determines if an M3U file is likely a video playlist - private fun File.isVideoM3ULike() = runCatching { + @JvmStatic + fun File.isVideoM3ULike() = runCatching { this@isVideoM3ULike.useLines { lines -> lines.any { line -> line.contains(Regex("""(?i)\.(mp4|avi|mkv|mov|wmv|ts)$""")) // Checks for common video file extensions @@ -3015,17 +3138,20 @@ object FileUtils { } }.getOrElse { false } - private fun File.isProguardConfigLike() = checkContains( + @JvmStatic + fun File.isProguardConfigLike() = checkContains( "-injars", "-outjars", "-libraryjars", "-printmapping", "-overloadaggressively", maxLinesToCheck = 200, ) - private fun File.isQmakeProjectLike() = checkContains( + @JvmStatic + fun File.isQmakeProjectLike() = checkContains( "TEMPLATE", "TARGET", "CONFIG", "SOURCES", "HEADERS", "RESOURCES", "INCLUDEPATH", "LIBS", "DEFINES", maxLinesToCheck = 200, ) - private fun File.isMarkdownMDLike() = runCatching { + @JvmStatic + fun File.isMarkdownMDLike() = runCatching { this@isMarkdownMDLike.useLines { lines -> lines.any { line -> return@any line.startsWith("#") @@ -3035,7 +3161,8 @@ object FileUtils { } }.getOrElse { false } - private fun File.isSegaMDLike() = runCatching { + @JvmStatic + fun File.isSegaMDLike() = runCatching { this@isSegaMDLike.inputStream().use { inputStream -> val header = ByteArray(512) // 假设 SEGA 游戏文件有特定的头部 if (inputStream.read(header) != header.size) return false @@ -3048,7 +3175,8 @@ object FileUtils { }.getOrElse { false } // Function to check if a file is likely a macro based on scripting patterns found in samples - private fun File.isMacroFileLike(): Boolean { + @JvmStatic + fun File.isMacroFileLike(): Boolean { return runCatching { this.useLines { lines -> lines.any { line -> @@ -3069,7 +3197,8 @@ object FileUtils { } // Function to determine if a file is likely a Monkey's Audio (.ape) file - private fun File.isMonkeyAudioLike(): Boolean { + @JvmStatic + fun File.isMonkeyAudioLike(): Boolean { return runCatching { this.inputStream().use { inputStream -> val header = ByteArray(4) @@ -3082,7 +3211,8 @@ object FileUtils { }.getOrElse { false } } - private fun File.isEbuStlLike(): Boolean { + @JvmStatic + fun File.isEbuStlLike(): Boolean { // EBU - Subtitling data exchange format return runCatching { this.inputStream().use { inputStream -> @@ -3116,7 +3246,8 @@ object FileUtils { }.getOrElse { false } } - private fun File.isModelStlLike(): Boolean { + @JvmStatic + fun File.isModelStlLike(): Boolean { return runCatching { this.inputStream().use { inputStream -> val header = ByteArray(80) // STL binary files start with an 80-byte header @@ -3146,7 +3277,8 @@ object FileUtils { }.getOrElse { false } } - private fun File.isModel3dsLike(): Boolean { + @JvmStatic + fun File.isModel3dsLike(): Boolean { return runCatching { this.inputStream().use { inputStream -> val header = ByteArray(2) @@ -3158,7 +3290,8 @@ object FileUtils { }.getOrElse { false } } - private fun File.isNintendo3dsLike(): Boolean { + @JvmStatic + fun File.isNintendo3dsLike(): Boolean { return runCatching { this.inputStream().use { inputStream -> val magicBytes = ByteArray(4) @@ -3242,4 +3375,14 @@ object FileUtils { */ data class CandidateCriterion(val criterion: Predicate, val weight: Int) + data class ApkProbeResult( + val isZipReadable: Boolean, + val hasAndroidManifest: Boolean, + val hasClassesDex: Boolean, + val hasResourcesArsc: Boolean, + val hasResDir: Boolean, + ) { + val isLikelyApk = isZipReadable && hasAndroidManifest && (hasClassesDex || hasResourcesArsc || hasResDir) + } + } diff --git a/app/src/main/java/org/autojs/autojs/util/TimeUtils.kt b/app/src/main/java/org/autojs/autojs/util/TimeUtils.kt index fe975ad9..479a6cea 100644 --- a/app/src/main/java/org/autojs/autojs/util/TimeUtils.kt +++ b/app/src/main/java/org/autojs/autojs/util/TimeUtils.kt @@ -32,7 +32,7 @@ object TimeUtils { @JvmStatic @JvmOverloads - fun formatTimestamp(ts: Long, pattern: String = "yyyy/MM/dd HH:mm"): String { + fun formatTimestamp(ts: Long, pattern: String = "yyyy-MM-dd HH:mm"): String { val dt = DateTime(ts) val fmt = DateTimeFormat.forPattern(pattern) return fmt.print(dt) 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 7391c648..4e859d7a 100644 --- a/app/src/main/java/org/autojs/autojs/util/ViewUtils.kt +++ b/app/src/main/java/org/autojs/autojs/util/ViewUtils.kt @@ -11,11 +11,20 @@ import android.content.res.Configuration import android.content.res.Configuration.UI_MODE_NIGHT_MASK import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.BitmapShader +import android.graphics.Canvas +import android.graphics.Color import android.graphics.ColorFilter +import android.graphics.ColorMatrix +import android.graphics.ColorMatrixColorFilter +import android.graphics.Paint import android.graphics.PorterDuff import android.graphics.PorterDuff.Mode.SRC_IN import android.graphics.PorterDuffColorFilter import android.graphics.Rect +import android.graphics.Shader +import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.graphics.drawable.GradientDrawable @@ -61,6 +70,7 @@ import org.autojs.autojs.theme.ThemeColorManager import org.autojs.autojs.util.StringUtils.key import org.autojs.autojs6.R import kotlin.math.floor +import kotlin.math.min import kotlin.math.roundToInt /** @@ -623,6 +633,76 @@ object ViewUtils { this.setColorsByColorLuminance(context, ThemeColorManager.colorPrimary) } + fun ImageView.colorFilterWithDesaturateOrNull(isOn: Boolean, alpha: Float = 0.5F) { + if (isOn) { + this.colorFilter = null + } else { + this.colorFilterWithDesaturate(alpha) + } + } + + fun ImageView.colorFilterWithDesaturate(alpha: Float = 0.5F) { + // Construct the desaturation matrix. + // zh-CN: 构造灰度矩阵. + val desaturate = ColorMatrix().apply { setSaturation(0f) } + // Construct the alpha scaling matrix. + // zh-CN: 构造透明度缩放矩阵. + val alphaMatrix = ColorMatrix().apply { setScale(1f, 1f, 1f, 0.5f) } + // Concatenate: first desaturate, then apply alpha. + // zh-CN: 叠加: 先灰度, 再透明度. + desaturate.postConcat(alphaMatrix) + this.colorFilter = ColorMatrixColorFilter(desaturate) + } + + @JvmStatic + fun Drawable.toCircular( + context: Context, + sizePx: Int, + borderWidthPx: Int = 0, + borderColor: Int = Color.TRANSPARENT, + ): Drawable { + val src = this + val bmp = Bitmap.createBitmap(sizePx, sizePx, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bmp) + + // First stretch/center draw the source drawable to sizePx * sizePx bitmap. + // zh-CN: 先把源 drawable 拉伸/居中绘制到 sizePx * sizePx 的位图上. + val tmp = Bitmap.createBitmap(sizePx, sizePx, Bitmap.Config.ARGB_8888) + Canvas(tmp).apply { + val w = src.intrinsicWidth.takeIf { it > 0 } ?: sizePx + val h = src.intrinsicHeight.takeIf { it > 0 } ?: sizePx + val scale = min(sizePx / w.toFloat(), sizePx / h.toFloat()) + val dw = (w * scale).roundToInt() + val dh = (h * scale).roundToInt() + val left = (sizePx - dw) / 2 + val top = (sizePx - dh) / 2 + src.setBounds(left, top, left + dw, top + dh) + src.draw(this) + } + + // Draw circle using BitmapShader. + // zh-CN: 用 BitmapShader 画圆. + val shader = BitmapShader(tmp, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP) + val paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.DITHER_FLAG).apply { + this.shader = shader + } + val radius = sizePx / 2f + val contentRadius = radius - borderWidthPx.coerceAtLeast(0) + canvas.drawCircle(radius, radius, contentRadius, paint) + + // Optional stroke. + // zh-CN: 可选描边. + if (borderWidthPx > 0) { + val stroke = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.STROKE + color = borderColor + strokeWidth = borderWidthPx.toFloat() + } + canvas.drawCircle(radius, radius, radius - borderWidthPx / 2f, stroke) + } + + return BitmapDrawable(context.resources, bmp) + } @JvmStatic fun setSearchViewColorsByColorLuminance(context: Context, searchView: SearchView, aimColor: Int) { searchView.setColorsByColorLuminance(context, aimColor) diff --git a/app/src/main/res/layout/apk_file_info_dialog_list_item.xml b/app/src/main/res/layout/apk_file_info_dialog_items.xml similarity index 100% rename from app/src/main/res/layout/apk_file_info_dialog_list_item.xml rename to app/src/main/res/layout/apk_file_info_dialog_items.xml diff --git a/app/src/main/res/layout/plugin_info_dialog_items.xml b/app/src/main/res/layout/plugin_info_dialog_items.xml new file mode 100644 index 00000000..d5cb3a2a --- /dev/null +++ b/app/src/main/res/layout/plugin_info_dialog_items.xml @@ -0,0 +1,1042 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_plugin_center.xml b/app/src/main/res/menu/menu_plugin_center.xml new file mode 100644 index 00000000..4cfed1a1 --- /dev/null +++ b/app/src/main/res/menu/menu_plugin_center.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 13e1a8cb..a3b1f784 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -112,6 +112,7 @@ فتح لوحة الألوان يترك إزالة + جلب إعادة المحاولة يحفظ اعدادات النظام @@ -303,6 +304,7 @@ انقر الطويل على زر \"تشغيل\" لتصحيح الأخطاء تأخير قبل الحلقة 0 للحلقة اللانهائية + أدخل عنوان URL يشير إلى ملحق (Plugin) بعيد.\nمثال: \"https://example.com/plugin.apk\". آخر استخدام: %1$s فشل خيط \"blob"\ نجح خيط \"blob"\ وجرى حفظ التخزين المؤقت دون اتصال @@ -339,7 +341,17 @@ العادة لم يتم تثبيت منشئ APK لا يتمتع AutoJs6 بالوصول إلى الجذر لتسجيل البرنامج النصي + المؤلف + المتعاونون + أول تثبيت + الإصدار المثبّت + آخر تثبيت + آخر إزالة + آخر تحديث + حجم الحزمة + إصدار متاح هل أنت متأكد من تجاهل إصدار التحديث الحالي؟\nيمكنك إدارة جميع الإصدارات التي تم تجاهلها بواسطة إعدادات التطبيق. + قد لا يكون الملف الحالي حزمة ملحق صالحة. هل تريد المتابعة بالتثبيت؟\n\nURI: \"%1$s\" هناك حاجة إلى إعادة تشغيل التطبيق لتطبيق إعدادات جديدة قد تكون هناك حاجة إلى إعادة تشغيل التطبيق لجعل اللغة مطبقًا كما هو متوقع شاشة كابتوري فريجرونيد خدمة @@ -435,6 +447,8 @@ توثيق AutoJs6 سجل سجل + AutoJs6 Plugin Center + Plugins AutoJs6 إعدادات إعدادات مصدر الرمز @@ -483,9 +497,11 @@ مسح اختيار الملف مسح السيناريو قبل التنفيذ تحديثات مسح الحالات التي تم فحصها + Click icon to add launcher shortcut انقر فوق العنصر لإزالته اضغط على العنصر لعرض التفاصيل انقر فوق \"نعم\" للانتقال إلى الإعدادات + Click other areas to exit selection التشغيل المتكرر وضع العميل استنساخ مكتبة ألوان @@ -542,6 +558,7 @@ حذف حذف الكل حذف الخط + الوصف تفاصيل تفاصيل المطور قيد التطوير خيارات للمطور @@ -555,6 +572,7 @@ دقّة شاشة الجهاز التحميل الان الدليل + معطّل عرض على تطبيقات أخرى يوصى بإذن \"العرض عبر التطبيقات الأخرى\" لعرض جميع عناصر واجهة المستخدم بشكل صحيح مهمة يمكن التخلص منها @@ -587,6 +605,8 @@ تمكين خدمة إمكانية الوصول مع توقيت الوصول إلى الجذر تمكين خدمة إمكانية الوصول مع إعدادات آمنة تلقائيًا تمكين خدمة إمكانية الوصول مع إعدادات آمنة + تفعيل الملحق + مفعّل خطأ فشل نسخ الملف: %s تقرير الشوائب @@ -617,10 +637,12 @@ فشل في منح الوصول فشل في منح الشاشة على إذن التطبيقات الأخرى فشل في الاستيراد + فشل التثبيت فشل في تحديد الموقع فشل في تسجيل الدخول فشل في تسجيل فشل في تقديم + فشل الجلب فشل حفظ المشروع البعيد في التخزين المحلي فشل في إرسال إدخالات السجل فشل في كتابة الملف @@ -637,6 +659,7 @@ اسم الملف لا يمكن أن يحتوي على أي من الأحرف التالية: \\ / : * ? " < > | اسم الملف طويل جدًا نقل الملفات + تصفية تجد العثور على فصول جافا التالي @@ -655,6 +678,7 @@ رمز تم إنشاؤه استرداد ملاحظات الإصدار ... تم استخدام عنوان URL للنسخ الاحتياطي + إعدادات عامة اذهب للاعدادات\" منح الوصول إلى AutoJs6 في تطبيق Shizuku ممنوح @@ -682,6 +706,10 @@ فحص حدود التصميم فحص التسلسل الهرمي للتخطيط تثبيت + التثبيت من \"ملف محلي\" + التثبيت من \"URL\" + تثبيت الملحق من \"URL\" + قابل للتثبيت تمت إزالة حرف غير صالح اسم الحزمة غير صالح مشروع غير صالح @@ -701,6 +729,7 @@ لم يتم التحقق من المخزن كلمة مرور مخزن المفاتيح ملصق + الحالة آخر فحص: %s النشاط الاخير أحدث حزمة @@ -879,6 +908,9 @@ اسم الإدخال الرجاء الانتظار... يرجى المحاولة مرة أخرى لاحقًا... + Plugin center + تفاصيل الملحق + Plugins موقع المؤشر فشل تبديل \"موقع المؤشر\".\nالوصول إلى الجذر مطلوب. نشر الإخطارات @@ -994,6 +1026,7 @@ خطة التوقيع مقاس تم تصدير %d من العناصر + فرز مسار رمز المصدر أذونات خاصة وضع مستقر @@ -1035,8 +1068,11 @@ @string/text_under_development الغاء التحميل ينسحب + Uninstall غير معروف لم يتم التحقق + Updatable + Update التحديثات تم مسح الحالات التي تم فحصها في وقت لاحق @@ -1079,14 +1115,8 @@ اكتب إعدادات الأمان إعدادات النظام الآمنة ، التي تحتوي على تفضيلات النظام التي يمكن أن تقرأها التطبيقات ولكن لا يُسمح لها بالكتابة.\nهذه هي لتفضيلات يجب على المستخدم تعديلها بشكل صريح من خلال واجهة المستخدم لتطبيق النظام.\nمع إذن إعدادات النظام الآمن ، يمكن للتطبيقات العادية تعديل الإعدادات الآمنة مباشرة (مثل خدمة إمكانية الوصول). كتابة إعدادات النظام - Update - Plugin center - Plugins - Updatable - Uninstall - AutoJs6 Plugin Center - Plugins - Click icon to add launcher shortcut - Click other areas to exit selection + فشل التحقق من السلامة + عدم تطابق SHA-256.\n\nالمتوقّع: %1$s\nالفعلي: %2$s + لم يتم توفير عنوان URL متاح للملحق الحالي diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 602fb39d..150b7003 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -107,6 +107,7 @@ Palette Quit Remove + Retrieve Retry Save System settings @@ -298,6 +299,7 @@ Long click \"Run\" button to debug Delay before loop 0 for infinite loop + Enter a URL pointing to a remote plugin address.\nFor example \"https://example.com/plugin.apk\". Latest used: %1$s \"Blob\" thread request failed \"Blob\" thread request successful, writing offline cache @@ -334,7 +336,17 @@ Custom APK Builder is not installed AutoJs6 has no root access to record a script + Author + Collaborators + First install + Installed ver. + Last install + Last uninstall + Last update + Package size + Updatable ver. Are you sure to ignore current update version?\nYou can manage all ignored versions by app settings. + The current file may not be a valid plugin package, do you want to continue with installation?\n\nURI: \"%1$s\" An app restart is needed to apply new settings An app restart may be needed to make language applied as expected Screen capturer foreground service @@ -430,6 +442,8 @@ Docs AutoJs6 Log Log + AutoJs6 Plugin Center + Plugins AutoJs6 Settings Settings Source code @@ -478,9 +492,11 @@ Clear file selection Clear pre-execute script Clear updates checked states + Click icon to add launcher shortcut Click the item to remove Click an item to view details Click \"OK\" to go to settings + Click other areas to exit selection Frequent operation Client mode Clone a color library @@ -537,6 +553,7 @@ Delete Delete All Delete line + Description Details Developer details is under development Developer options @@ -550,6 +567,7 @@ Device screen resolution Download now Directory + Disabled Display over other apps \"Display over other apps\" permission is recommended to make all widgets displayed properly Disposable task @@ -582,6 +600,8 @@ Enable accessibility service with root access timed out Enable accessibility service with secure settings automatically Enable accessibility service with secure settings timed out + Enable plugin + Enabled Error Failed to copy file: %s Bug report @@ -612,10 +632,12 @@ Failed to grant access Failed to grant display over other apps permission Failed to import + Failed to install Failed to locate Failed to login Failed to register Failed to submit + Failed to retrieve Failed to save remote project to local storage Failed to send log entries Failed to write file @@ -632,6 +654,7 @@ Filename cannot contain the following characters: \\ / : * ? " < > | Filename is too long Files transfer + Filter Find Find Java classes Next @@ -650,6 +673,7 @@ Generated code Retrieving release notes... Backup URL has been used + Global settings Go to \"Settings\" Grant AutoJs6 access in Shizuku app Granted @@ -677,6 +701,10 @@ Inspect layout bounds Inspect layout hierarchy Install + Install from \"Local File\" + Install from \"URL\" + Install plugin from \"URL\" + Installable Invalid character is removed Invalid package name Invalid project @@ -696,6 +724,7 @@ Keystore has not been verified Key Store Password Label + State Last checked: %s Latest activity Latest package @@ -874,6 +903,9 @@ Input name Please wait... Please wait a moment before trying again... + Plugin center + Plugin details + Plugins Pointer location Toggle \"pointer location\" failed.\nRoot access is required. Post notifications @@ -989,6 +1021,7 @@ Signature Scheme Size %d items exported + Sort Source code path Special permissions Stable mode @@ -1030,8 +1063,11 @@ @string/text_under_development Undo Undo + Uninstall Unknown Unverified + Updatable + Update Updates Updates checked states cleared Later @@ -1074,14 +1110,8 @@ Write security settings Secure system settings, containing system preferences that applications can read but are not allowed to write.\nThese are for preferences that the user must explicitly modify through the UI of a system app.\nWith secure system settings permission, normal applications can directly modify the secure settings (such as accessibility service). Write system settings - Update - Plugin center - Plugins - Updatable - Uninstall - AutoJs6 Plugin Center - Plugins - Click icon to add launcher shortcut - Click other areas to exit selection + Integrity verification failed + SHA-256 mismatch.\n\nExpected: %1$s\nActual: %2$s + No available URL provided for current plugin diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index cb0f0b2c..e7439041 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -110,6 +110,7 @@ Abrir paleta Salir Eliminar + Obtener Reintentar Guardar Configuración del sistema @@ -301,6 +302,7 @@ Haga un clic largo en el botón \"Ejecutar\" para depurar Retraso antes del bucle 0 para bucle infinito + Introduce una URL que apunte a la dirección de un complemento remoto.\nPor ejemplo, \"https://example.com/plugin.apk\". Último uso: %1$s Hilo \"blob\" fallido Hilo \"blob\" exitoso, escribiendo caché sin conexión @@ -337,7 +339,17 @@ Personalizado APK Builder no está instalado AutoJs6 no tiene acceso a la raíz para grabar un script + Autor + Colaboradores + Primera inst. + Vers. inst. + Última inst. + Última desinst. + Última act. + Tamaño paquete + Vers. actualiz. ¿Está seguro de ignorar la versión actual de la actualización?\nPuedes gestionar todas las versiones ignoradas en los ajustes de la aplicación. + El archivo actual puede no ser un paquete de complemento válido. ¿Deseas continuar con la instalación?\n\nURI: \"%1$s\" Es necesario reiniciar la aplicación para aplicar los nuevos ajustes Puede ser necesario reiniciar la aplicación para que el idioma se aplique como se espera Servicio de primer plano del capturador de pantalla @@ -433,6 +445,8 @@ Docs AutoJs6 Registrar Registrar + AutoJs6 Plugin Center + Plugins AutoJs6 Configuración Configuración Código fuente @@ -481,9 +495,11 @@ Borrar selección de archivos Borrar script de pre-ejecución Borrar los estados de comprobación de las actualizaciones + Click icon to add launcher shortcut Haga clic en el elemento a eliminar Toca un elemento para ver los detalles Haga clic en \"Aceptar\" para ir a la configuración + Click other areas to exit selection Funcionamiento frecuente Modo cliente Clonar biblioteca de colores @@ -540,6 +556,7 @@ Borrar Eliminar todo Borrar línea + Descripción Detalles Los detalles del desarrollador están en desarrollo Opciones del desarrollador @@ -553,6 +570,7 @@ Resolución de pantalla del dispositivo Descargar ahora Directorio + Desactivado Mostrar sobre otras aplicaciones Se recomienda el permiso \"Mostrar sobre otras aplicaciones\" para que todos los widgets se muestren correctamente Tarea desechable @@ -585,6 +603,8 @@ Habilitar el servicio de accesibilidad con acceso a la raíz con tiempo de espera Habilitar el servicio de accesibilidad con configuración segura automáticamente Habilitación del servicio de accesibilidad con configuración segura agotada + Activar complemento + Activado Error No se ha podido copiar el archivo: %s Informe de error @@ -615,10 +635,12 @@ Fallo al conceder el acceso Fallo al conceder el permiso de visualización sobre otras aplicaciones Fallo en la importación + Error al instalar No se pudo localizar Fallo en el inicio de sesión Fallo en el registro Fallo al enviar + Error al obtener Error al guardar el proyecto remoto en el almacenamiento local Error al enviar entradas de registro Fallo al escribir el archivo @@ -635,6 +657,7 @@ El nombre del archivo no puede contener ninguno de los siguientes caracteres: \\ / : * ? " < > | El nombre del archivo es demasiado largo Transferencia de archivos + Filtrar Buscar Buscar clases Java Sigui @@ -653,6 +676,7 @@ Código generado Recuperando las notas de la versión... Se ha utilizado la URL de copia de seguridad + Ajustes globales Ir a \"Configuración\" Conceder privilegios AutoJs6 en una aplicación Shizuku Concedido @@ -680,6 +704,10 @@ Inspeccionar los límites del diseño Inspeccionar la jerarquía del diseño Instalar + Instalar desde \"Archivo local\" + Instalar desde \"URL\" + Instalar complemento desde \"URL\" + Instalable Carácter inválido ha sido removido Nombre de paquete no válido Proyecto no válido @@ -699,6 +727,7 @@ El almacén de claves no ha sido verificado Contraseña del almacén de claves Etiqueta + Estado Última comprobación: %s Última actividad Último paquete @@ -877,6 +906,9 @@ Nombre de entrada Por favor, espere... Por favor, inténtalo de nuevo más tarde... + Plugin center + Detalles del complemento + Plugins Ubicación del puntero Falló la conmutación de la \"ubicación del puntero\".\nSe requiere acceso a la raíz. Notificaciones postales @@ -992,6 +1024,7 @@ Esquema de firma Tamaño %d elementos exportados + Ordenar Ruta del código fuente Permisos especiales Modo estable @@ -1033,8 +1066,11 @@ @string/text_under_development Revocar Revoc + Uninstall Desconocido No verificado + Updatable + Update Actualizaciones Actualizaciones comprobadas estados borrados Más adelante @@ -1077,14 +1113,8 @@ Escribir la configuración de seguridad Ajustes de seguridad del sistema, que contienen preferencias del sistema que las aplicaciones pueden leer pero no pueden escribir.\nSe trata de preferencias que el usuario debe modificar explícitamente a través de la interfaz de usuario de una aplicación del sistema.\nCon el permiso de configuración segura del sistema, las aplicaciones normales pueden modificar directamente la configuración segura (como el servicio de accesibilidad). Escribir la configuración del sistema - Update - Plugin center - Plugins - Updatable - Uninstall - AutoJs6 Plugin Center - Plugins - Click icon to add launcher shortcut - Click other areas to exit selection + Discordancia de SHA-256.\n\nEsperado: %1$s\nReal: %2$s + La verificación de integridad falló + No se proporcionó ninguna URL disponible para el complemento actual diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index b4501dff..25085e44 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -110,6 +110,7 @@ Palette Quit Supprimer + Récupérer Retourner Enregistrer Paramètres du système @@ -301,6 +302,7 @@ Cliquez longuement sur le bouton \"Run\" pour déboguer Délai avant boucle 0 pour une boucle infinie + Saisissez une URL pointant vers l\'adresse d\'un plugin distant.\nPar exemple \"https://example.com/plugin.apk\". Dernière utilisation : %1$s Échec du thread \"blob\" Thread \"blob\" réussi, écriture du cache hors ligne @@ -337,7 +339,17 @@ Custom APK Builder n\'est pas installé AutoJs6 n\'a pas d\'accès root pour enregistrer un script. + Auteur + Collaborateurs + 1re inst. + Ver. installée + Dern. inst. + Dern. désinst. + Dern. maj + Taille du paquet + Ver. maj disp. Êtes-vous sûr d\'ignorer la version de mise à jour actuelle?\nVous pouvez gérer toutes les versions ignorées dans les paramètres de l\'application. + Le fichier actuel n\'est peut‑être pas un paquet de plugin valide. Voulez‑vous continuer l\'installation ?\n\nURI : \"%1$s\" Un redémarrage de l\'application est nécessaire pour appliquer les nouveaux paramètres. Un redémarrage de l\'appli peut être nécessaire pour que la langue soit appliquée comme prévu. Service de capture d\'écran en avant-plan @@ -433,6 +445,8 @@ Docs AutoJs6 Journal Journal + AutoJs6 Plugin Center + Plugins AutoJs6 Réglages Réglages Code source @@ -481,9 +495,11 @@ Effacer la sélection de fichiers Effacer le script de pré-exécution Effacer les états vérifiés des mises à jour + Click icon to add launcher shortcut Cliquez sur l\'élément à supprimer Touchez un élément pour afficher les détails Cliquez sur \"OK\" pour accéder aux paramètres. + Click other areas to exit selection Une opération fréquente Mode client Cloner une bibliothèque de couleurs @@ -540,6 +556,7 @@ Suppression Tout supprimer Supprimer la ligne + Description Détails Les détails du développeur sont en cours de développement Les options du développeur @@ -553,6 +570,7 @@ Résolution d\'écran de l\'appareil Téléchargement immédiat Directory + Désactivé Affichage sur les autres apps La permission \"Display over other apps\" est recommandée pour que tous les widgets s\'affichent correctement. Tâche à supprimer @@ -585,6 +603,8 @@ Activation du service d\'accessibilité avec accès root temporisé Activer le service d\'accessibilité avec des paramètres sécurisés automatiquement. Activer le service d\'accessibilité avec des paramètres sécurisés temporairement. + Activer le plugin + Activé Erreur Failed to copy file : %s Rapport de bug @@ -615,10 +635,12 @@ Failed to grant access Fail to grant display over other apps permission Fail to import + Échec de l\'installation Échec de la localisation Fail to login Failed to register Failed to submit + Échec de la récupération Échec de l\'enregistrement du projet distant dans le stockage local Échec de l\'envoi des entrées de journal Fail to write file @@ -635,6 +657,7 @@ Le nom du fichier ne peut pas contenir les caractères suivants : \\ / : * ? " < > | Le nom du fichier est trop long Transfert de fichiers + Filtrer Recherche Recherche de classes Java Suiv @@ -653,6 +676,7 @@ Code généré Retrouver les release notes... L\'URL de sauvegarde a été utilisée + Paramètres globaux Aller à \"Settings\" Accorder des privilèges AutoJs6 dans une application Shizuku Granted @@ -680,6 +704,10 @@ Inspecter les limites de la mise en page Inspecter la hiérarchie des dispositions Installation + Installer depuis \"Fichier local\" + Installer depuis \"URL\" + Installer le plugin depuis \"URL\" + Installable Caractère invalide est supprimé Nom de paquet non valide Projet non valide @@ -699,6 +727,7 @@ Le magasin de clés n\'a pas été vérifié Mot de passe du magasin de clés Étiquette + État Dernière vérification : %s Dernière activité Dernier paquet @@ -877,6 +906,9 @@ Nom de l\'entrée Veuillez patienter... Veuillez réessayer cette opération plus tard... + Plugin center + Détails du plugin + Plugins L\'emplacement du pointeur Toggle \"pointer location\" failed.\nL\'accès à la racine est nécessaire. Notifications postales @@ -992,6 +1024,7 @@ Schéma de signature Taille %d items exported + Trier Chemin du code source Autorisations spéciales Mode stable @@ -1033,8 +1066,11 @@ @string/text_under_development Révoquer Révoq + Uninstall Inconnu Non vérifié + Updatable + Update Mises à jour Mise à jour des états vérifiés et effacés Plus tard @@ -1077,14 +1113,8 @@ Écrire les paramètres de sécurité. Paramètres de sécurité du système, contenant les préférences du système que les applications peuvent lire mais ne sont pas autorisées à écrire.\nIl s\'agit des préférences que l\'utilisateur doit explicitement modifier par le biais de l\'interface utilisateur d\'une application système.\nAvec l\'autorisation de paramètres de sécurité du système, les applications normales peuvent directement modifier les paramètres de sécurité (comme le service d\'accessibilité). Écrire les paramètres système - Update - Plugin center - Plugins - Updatable - Uninstall - AutoJs6 Plugin Center - Plugins - Click icon to add launcher shortcut - Click other areas to exit selection + Échec de la vérification de l\'intégrité + Incohérence du SHA-256.\n\nAttendu : %1$s\nObtenu : %2$s + Aucune URL disponible n\'a été fournie pour le plug-in actuel diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index c0f7d28c..2b8eacd6 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -111,6 +111,7 @@ パレットを開く 終了する 削除 + 取得 再試行 保存する システム設定 @@ -302,6 +303,7 @@ 実行」ボタン長押しでデバッグ ループ前の遅延時間 無限ループの場合は 0 + リモートプラグインの URL を入力してください. \n例: \"https://example.com/plugin.apk\" 最終使用時: %1$s \"Blob\" スレッドが失敗 \"Blob\" スレッドが成功、オフラインキャッシュを書き込み @@ -338,7 +340,17 @@ カスタム APK Builder がインストールされていない AutoJs6 にスクリプトを記録するためのルートアクセス権がない + 作者 + 協力者 + 初回インストール + インストール済み Ver. + 最終インストール + 最終アンインストール + 最終更新 + パッケージサイズ + 更新可 Ver. 現在のアップデートバージョンを無視して大丈夫ですか?\n無視したバージョンは, アプリの設定で管理することができます + このファイルは有効なプラグインパッケージではない可能性があります. インストールを続行しますか?\n\nURI: \"%1$s\" 新しい設定を適用するには, アプリの再起動が必要です 言語を正しく適用するために, アプリの再起動が必要な場合があります スクリーンキャプチャーのフォアグラウンドサービス @@ -434,6 +446,8 @@ 文書 AutoJs6 ログ ログ + AutoJs6 Plugin Center + Plugins AutoJs6 設定 設定 ソースコード @@ -482,9 +496,11 @@ ファイル選択の解除 実行前スクリプトのクリア アップデートのチェック状態をクリアする + Click icon to add launcher shortcut 削除する項目をクリックします 項目をタップして詳細を表示します \"OK\" をクリックすると設定に進みます + Click other areas to exit selection 頻繁に行う操作 クライアントモード カラーライブラリを複製 @@ -541,6 +557,7 @@ 削除 すべて削除 行削除 + 説明 詳細 デベロッパーの詳細については, 現在開発中です デベロッパーオプション @@ -554,6 +571,7 @@ デバイスの画面解像度 今すぐダウンロード ディレクトリ + 無効 他のアプリの上に表示する すべてのウィジェットを正しく表示するために, \"Display over other apps\" パーミッションの使用を推奨します 使い捨てタスク @@ -586,6 +604,8 @@ ルートアクセスがタイムアウトした状態でアクセシビリティサービスを有効にする セキュアな設定でアクセシビリティサービスを自動的に有効にする セキュアな設定でアクセシビリティサービスを有効化するとタイムアウトする + プラグインを有効化 + 有効 エラー ファイルのコピーに失敗しました. %s バグレポート @@ -616,10 +636,12 @@ アクセス権の付与に失敗しました 他のアプリの上に表示する権限の付与に失敗しました インポートに失敗しました + インストールに失敗 見つかりませんでした ログインに失敗しました 登録の失敗 投稿の失敗 + 取得に失敗 リモートプロジェクトをローカルストレージに保存できませんでした ログ・エントリの送信に失敗しました ファイルの書き込みに失敗しました @@ -636,6 +658,7 @@ ファイル名に以下の文字を含めることはできません: \\ / : * ? " < > | ファイル名が長すぎます ファイルの転送 + フィルター 検索 Java クラスの検索 次へ @@ -654,6 +677,7 @@ 生成されたコード リリースノートの取得中... バックアップ URL を使用しました + グローバル設定 \"設定\" に進む Shizuku アプリケーションで AutoJs6 権限を付与する 許可する @@ -681,6 +705,10 @@ レイアウト境界の検査 レイアウト階層の検査 インストール + \"ローカルファイル\" からインストール + \"URL\" からインストール + \"URL\" からプラグインをインストール + インストール可 無効な文字が削除されました パッケージ名が無効です プロジェクトが無効です @@ -700,6 +728,7 @@ キーストアは確認されていません キーストアのパスワード ラベル + 状態 最終チェック: %s 最新の活動 最新のパッケージ @@ -878,6 +907,9 @@ 入力名 しばらくお待ちください... しばらくしてからもう一度お試しください... + Plugin center + プラグイン詳細 + Plugins ポインターの位置 トグル「ポインターの位置」に失敗しました.\nroot 権限が必要です ゆうびんけいほう @@ -993,6 +1025,7 @@ 署名スキーム サイズ エクスポートされた項目 %d + 並べ替え ソースコードのパス 特殊な権限 安定モード @@ -1034,8 +1067,11 @@ @string/text_under_development 元に戻す 戻る + Uninstall 不明 未確認 + Updatable + Update アップデート 更新のチェック状態を解除 後日 @@ -1078,14 +1114,8 @@ セキュリティ設定の書き込み アプリケーションが読み取ることはできるが, 書き込むことはできないシステム環境設定を含む, 安全なシステム設定です.\nこれは, ユーザーがシステムアプリの UI を通じて明示的に変更する必要がある環境設定のためのものです.\nセキュアなシステム設定を許可すると, 通常のアプリケーションはセキュアな設定 (アクセシビリティサービスなど) を直接変更できるようになります システム設定の書き込み - Update - Plugin center - Plugins - Updatable - Uninstall - AutoJs6 Plugin Center - Plugins - Click icon to add launcher shortcut - Click other areas to exit selection + 整合性の検証に失敗しました + SHA-256 が一致しません.\n\n期待値: %1$s\n実際: %2$s + 現在のプラグインに利用可能な URL は提供されていません diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index acac1d59..ffe9a744 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -112,6 +112,7 @@ 팔레트 열기 그만두다 제거하다 + 가져오기 다시 해 보다 구하다 환경 설정 @@ -303,6 +304,7 @@ \"실행\" 버튼을 긴 클릭하여 디버그를 클릭하십시오 루프 전 지연 무한 루프의 경우 0 + 원격 플러그인 주소를 가리키는 URL을 입력하세요.\n예: \"https://example.com/plugin.apk\" 최종 사용: %1$s \"Blob\" 스레드 요청 실패 \"Blob\" 스레드 요청 성공, 오프라인 캐시 기록 @@ -339,7 +341,17 @@ 관습 APK 빌더가 설치되지 않았습니다 AutoJs6 에는 스크립트를 기록 할 루트 액세스가 없습니다 + 제작자 + 협력자 + 최초 설치 + 설치 버전 + 마지막 설치 + 마지막 제거 + 마지막 업데이트 + 패키지 크기 + 업데이트 가능 현재 업데이트 버전을 무시해야합니까?\n앱 설정으로 무시 된 버전을 모두 관리 할 수 있습니다. + 현재 파일이 유효한 플러그인 패키지가 아닐 수 있습니다. 설치를 계속하시겠습니까?\n\nURI: \"%1$s\" 새로운 설정을 적용하려면 앱 재시작이 필요합니다 예상대로 언어를 적용하려면 앱 재시작이 필요할 수 있습니다. 스크린 캡처 전경 서비스 @@ -435,6 +447,8 @@ 문서 AutoJs6 통나무 통나무 + AutoJs6 Plugin Center + Plugins AutoJs6 설정 설정 소스 코드 @@ -483,9 +497,11 @@ 파일 선택을 지우십시오 사전 에코 슈트 스크립트를 지우십시오 확인 된 상태에서 명확한 업데이트 + Click icon to add launcher shortcut 제거하려면 항목을 클릭하십시오 항목을 탭하면 자세한 정보를 볼 수 있습니다 \"확인\"을 클릭하여 설정으로 이동하십시오 + Click other areas to exit selection 빈번한 작동 클라이언트 모드 색상 라이브러리 복제 @@ -542,6 +558,7 @@ 삭제 모두 삭제 라인 삭제 + 설명 세부 개발자 세부 사항이 개발 중입니다 개발자 옵션 @@ -555,6 +572,7 @@ 기기 화면 해상도 지금 다운로드하십시오 예배 규칙서 + 비활성화됨 다른 앱에 표시됩니다 \"다른 앱 위의 디스플레이\"권한은 모든 위젯을 올바르게 표시하는 것이 좋습니다. 일회용 작업 @@ -587,6 +605,8 @@ 루트 액세스 시간이 초과 된 접근성 서비스를 활성화하십시오 안전한 설정을 통해 접근성 서비스를 자동으로 활성화하십시오 안전한 설정 시간이 초과되는 접근성 서비스를 활성화하십시오 + 플러그인 활성화 + 활성화됨 오류 파일을 복사하지 못했습니다: %s 버그 보고서 @@ -617,10 +637,12 @@ 액세스 권한을 부여하지 못했습니다 다른 앱 권한을 통해 디스플레이를 부여하지 못했습니다 가져 오지 못했습니다 + 설치 실패 찾을 수 없음 로그인 실패 등록하지 못했습니다 제출하지 못했습니다 + 가져오기 실패 원격 프로젝트를 로컬 저장소에 저장하지 못했습니다 로그 항목을 보내지 못했습니다 파일을 쓰지 못했습니다 @@ -637,6 +659,7 @@ 파일 이름에 다음 문자를 포함할 수 없습니다: \\ / : * ? " < > | 파일 이름이 너무 깁니다 파일 전송 + 필터 찾다 Java 클래스를 찾으십시오 다음을 @@ -655,6 +678,7 @@ 생성 된 코드 릴리스 노트 검색 ... 백업 URL 이 사용되었습니다 + 전역 설정 설정으로 바로 가기\" Shizuku 애플리케이션에서 AutoJs6 권한 부여하기 부여된 @@ -682,6 +706,10 @@ 레이아웃 경계를 검사하십시오 레이아웃 계층 구조를 검사하십시오 설치 + \"로컬 파일\"에서 설치 + \"URL\"에서 설치 + \"URL\"에서 플러그인 설치 + 설치 가능 잘못된 문자가 제거되었습니다 잘못된 패키지 이름 잘못된 프로젝트 @@ -701,6 +729,7 @@ 키 저장소가 확인되지 않았습니다 키 저장소 비밀번호 이름 + 상태 마지막으로 확인 된: %s 최신 활동 최신 패키지 @@ -879,6 +908,9 @@ 입력 이름 잠시만 기다려주세요... 잠시 후 다시 시도해 주세요... + Plugin center + 플러그인 상세 + Plugins 포인터 위치 \"포인터 위치\"토글이 실패했습니다.\n루트 액세스가 필요합니다. 게시물 알림 @@ -994,6 +1026,7 @@ 서명 스킴 크기 내보낸 %d 항목 + 정렬 소스 코드 경로 특수 권한 안정적인 모드 @@ -1035,8 +1068,11 @@ @string/text_under_development 실행 취소 + Uninstall 알 수 없음 미확인 + Updatable + Update 업데이트 확인 된 상태가 지워졌습니다 나중 @@ -1079,14 +1115,8 @@ 보안 설정을 작성하십시오 애플리케이션이 읽을 수 있지만 쓸 수없는 시스템 환경 설정을 포함하는 보안 시스템 설정.\n이들은 사용자가 시스템 앱의 UI 를 통해 명시 적으로 수정 해야하는 선호도입니다.\n보안 시스템 설정 권한을 사용하면 일반 애플리케이션이 보안 설정 (예: 접근성 서비스)을 직접 수정할 수 있습니다. 시스템 설정을 작성하십시오 - Update - Plugin center - Plugins - Updatable - Uninstall - AutoJs6 Plugin Center - Plugins - Click icon to add launcher shortcut - Click other areas to exit selection + 무결성 검증에 실패했습니다 + SHA-256 값이 일치하지 않습니다.\n\n예상: %1$s\n실제: %2$s + 현재 플러그인에 사용 가능한 URL 이 제공되지 않았습니다 diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 9313cc72..a15df18b 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -33,6 +33,7 @@ #C7A4FF #0DA798 #BCAAA4 + #BCAAA4 @color/md_gray_700 #009624 @color/dialog_button_finish diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 8f62c48c..0db6a97d 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -110,6 +110,7 @@ Открыть палитру Выйти Удалить + Получить Повторная попытка Сохранить Системные настройки @@ -301,6 +302,7 @@ Длительное нажатие кнопки \"Выполнить\" для отладки Задержка перед циклом 0 для бесконечного цикла + Введите URL, указывающий на удалённый плагин.\nНапример: \"https://example.com/plugin.apk\". Последнее использование: %1$s Поток \"blob\" неудачен Поток \"blob\" успешен, запись офлайн-кеша @@ -337,7 +339,17 @@ Пользовательский APK Builder не установлен AutoJs6 не имеет root-доступа для записи скрипта + Автор + Соавторы + Перв. устан. + Уст. вер. + Посл. устан. + Посл. удал. + Посл. обновл. + Размер пакета + Обновл. вер. Вы уверены, что игнорируете текущую версию обновления?\nВы можете управлять всеми игнорируемыми версиями в настройках приложения. + Текущий файл может быть недействительным пакетом плагина. Продолжить установку?\n\nURI: \"%1$s\" Для применения новых настроек требуется перезапуск приложения Перезапуск приложения может потребоваться для того, чтобы язык применялся как положено Служба захвата экрана на переднем плане @@ -433,6 +445,8 @@ документы AutoJs6 Журнал Журнал + AutoJs6 Plugin Center + Plugins AutoJs6 Настройки Настройки Исходный код @@ -481,9 +495,11 @@ Очистить выбор файла Очистить сценарий предварительного выполнения Очистить состояния проверки обновлений + Click icon to add launcher shortcut Нажмите на элемент для удаления Нажмите на пункт, чтобы просмотреть подробности Нажмите \"OK\" для перехода к настройкам + Click other areas to exit selection Частые операции Режим клиента Клонировать цветовую библиотеку @@ -540,6 +556,7 @@ Удалить Удалить все Удалить строку + Описание Детали Детали разработчика находятся в стадии разработки Параметры разработчика @@ -553,6 +570,7 @@ Разрешение экрана устройства Загрузить сейчас Каталог + Отключено Отображать поверх других приложений Разрешение \"Отображать поверх других приложений\" рекомендуется для правильного отображения всех виджетов. Одноразовая задача @@ -585,6 +603,8 @@ Включить службу доступности с корневым доступом по таймеру Автоматическое включение службы доступности с безопасными настройками Включить службу доступа с безопасными настройками по таймеру + Включить плагин + Включено Ошибка Не удалось скопировать файл: %s Отчет об ошибке @@ -615,10 +635,12 @@ Не удалось предоставить доступ Не удалось предоставить разрешение на отображение поверх других приложений Не удалось импортировать + Не удалось установить Не удалось найти Не удалось войти в систему Не удалось зарегистрироваться Не удалось отправить + Не удалось получить Не удалось сохранить удаленный проект в локальное хранилище Не удалось отправить записи журнала Не удалось записать файл @@ -635,6 +657,7 @@ Имя файла не может содержать следующие символы: \\ / : * ? " < > | Имя файла слишком длинное Передача файлов + Фильтр Найти Найти классы Java Следу @@ -653,6 +676,7 @@ Сгенерированный код Извлечение заметок о выпуске... Использован URL-адрес резервной копии + Глобальные настройки Перейдите в \"Настройки\" Предоставление привилегий AutoJs6 в приложении Shizuku Разрешено @@ -680,6 +704,10 @@ Осмотр границ макета Проверить иерархию макета Установить + Установить из \"Локального файла\" + Установить из \"URL\" + Установить плагин из \"URL\" + Устанавливаемый Недопустимый символ удален Неверное имя пакета Неверный проект @@ -699,6 +727,7 @@ Хранилище ключей не подтверждено Пароль хранилища ключей Метка + Состояние Последняя проверка: %s Последняя активность Последний пакет @@ -877,6 +906,9 @@ Имя ввода Пожалуйста, подождите... Пожалуйста, повторите попытку позже... + Plugin center + Сведения о плагине + Plugins Расположение указателя Переключение \"Расположение указателя\" не удалось.\nТребуется корневой доступ. почтовые уведомления @@ -992,6 +1024,7 @@ Схема подписи Размер Экспортировано %d элементов + Сортировать Путь к исходному коду Специальные разрешения Стабильный режим @@ -1033,8 +1066,11 @@ @string/text_under_development Отозвать Верни + Uninstall Неизвестно Не подтверждено + Updatable + Update Обновления Обновления проверенные состояния очищены Позже @@ -1077,14 +1113,8 @@ Параметры безопасности записи Настройки безопасности системы, содержащие системные предпочтения, которые приложения могут читать, но не имеют права записывать.\nОни предназначены для параметров, которые пользователь должен явно изменить через пользовательский интерфейс системного приложения.\nПри наличии разрешения на безопасные системные настройки обычные приложения могут напрямую изменять безопасные настройки (например, служба доступности). Запись системных настроек - Update - Plugin center - Plugins - Updatable - Uninstall - AutoJs6 Plugin Center - Plugins - Click icon to add launcher shortcut - Click other areas to exit selection + Сбой проверки целостности + Несовпадение SHA-256.\n\nОжидалось: %1$s\nФактически: %2$s + Для текущего плагина не указан доступный URL-адрес diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 934a33a6..f50ae375 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -108,6 +108,7 @@ 打開調色盤 放棄 移除 + 獲取 重試 保存 系統設置 @@ -299,6 +300,7 @@ 長按 \"運行\" 圖標可啓動調試 開始循環前的延遲 0 表示無限循環 + 輸入一個指向遠程插件地址的 URL.\n例如 \"https://example.com/plugin.apk\". 最近使用: %1$s 線程 "blob" 請求失敗 線程 "blob" 請求成功, 寫入離線緩存 @@ -335,7 +337,17 @@ 自定義 打包插件未安裝 AutoJs6 無 root 權限, 無法錄製腳本 + 開發者 + 合作者 + 首次安裝 + 已安裝版本 + 最近安裝 + 最近卸載 + 最近更新 + 安裝包大小 + 可更新版本 確定忽略當前更新版本嗎?\n在應用設置中可管理已忽略的所有版本. + 當前文件可能不是有效的插件包, 是否繼續安裝?\n\nURI: \"%1$s\" 需要重啓應用才能完成文檔源切換 部分內容可能需要重啓應用才能完成語言切換 屏幕捕獲器前台服務 @@ -431,6 +443,8 @@ 文檔 AutoJs6 日誌 日誌 + AutoJs6 Plugin Center + Plugins AutoJs6 設置 設置 軟件源碼 @@ -479,9 +493,11 @@ 清空文件選擇 清空預執行腳本 清除更新檢查狀態 + Click icon to add launcher shortcut 點擊條目可移除 點擊條目可查看詳情 點擊 \"確定\" 跳轉到設置頁面 + Click other areas to exit selection 操作頻率過快 客户端模式 克隆顏色庫 @@ -538,6 +554,7 @@ 刪除 刪除全部 刪除行 + 描述 詳情 \"開發者詳情\" 正在開發中... 開發者選項 @@ -551,6 +568,7 @@ 設備屏幕分辨率 直接下載 文件夾 + 已禁用 顯示在其他應用上層 建議授予 \"顯示在其他應用上層\" 權限以確保應用窗口組件正常顯示 一次性任務 @@ -583,6 +601,8 @@ 使用 root 權限啓用無障礙服務超時 使用修改安全設置權限自動啓用無障礙服務 使用修改安全設置權限啓用無障礙服務超時 + 啓用插件 + 已啓用 錯誤 文件複製失敗: %s 錯誤報告 @@ -613,10 +633,12 @@ 授權失敗 顯示在其他應用上層權限授予失敗 導入失敗 + 安裝失敗 定位失敗 登錄失敗 註冊失敗 提交失敗 + 獲取失敗 無法保存遠程項目至本地 日誌條目發送失敗 文件寫入失敗 @@ -633,6 +655,7 @@ 文件名不能包含以下字符: \\ / : * ? " < > | 文件名太長 文件遷移 + 篩選 查找 搜索 Java 類 下一個 @@ -651,6 +674,7 @@ 已生成代碼 正在獲取版本信息... 備用 URL 已使用 + 全局設置 打開 \"設置\" 在 Shizuku 應用中授予 AutoJs6 權限 已授予 @@ -678,6 +702,10 @@ 佈局範圍分析 佈局層次分析 安裝 + 從 \"本地文件\" 安裝 + 從 \"URL\" 安裝 + 從 \"URL\" 安裝插件 + 可安裝 無效字符已被移除 無效包名 無效項目 @@ -697,6 +725,7 @@ 密鑰尚未驗證 密鑰庫密碼 名稱 + 狀態 上次檢查: %s 最近活動 最近包名 @@ -875,6 +904,9 @@ 請輸入名稱 請稍候... 請稍後再嘗試此操作... + Plugin center + 插件詳情 + Plugins 指針位置 切換 \"指針位置\" 顯示狀態失敗\n可能缺少 root 權限 發佈通知權限 @@ -990,6 +1022,7 @@ 簽名方案 大小 已導出 %d 個條目 + 排序 源代碼路徑 特殊權限 穩定模式 @@ -1031,8 +1064,11 @@ @string/text_under_development 撤銷 撤銷 + Uninstall 未知 未驗證 + Updatable + Update 更新 更新檢查狀態已清除 稍後提示 @@ -1075,14 +1111,8 @@ 修改安全設置 安全設置包含應用程序可讀但不可寫入的設置選項, 這些選項只能由 UI 或系統級別應用修改.\n被授予 \"修改安全設置權限\" 後, 普通應用可直接修改上述安全設置 (例如無障礙服務). 修改系統設置 - Update - Plugin center - Plugins - Updatable - Uninstall - AutoJs6 Plugin Center - Plugins - Click icon to add launcher shortcut - Click other areas to exit selection + 完整性驗證失敗 + SHA-256 驗證不一致.\n\n期望值: %1$s\n實際值: %2$s + 當前插件未提供可用的 URL diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 050cf68e..2fd426d6 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -108,6 +108,7 @@ 開啟調色盤 放棄 移除 + 獲取 重試 儲存 系統設定 @@ -299,6 +300,7 @@ 長按 \"執行\" 圖示可啟動除錯 開始迴圈前的延遲 0 表示無限迴圈 + 輸入一個指向遠端外掛地址的 URL.\n例如 \"https://example.com/plugin.apk\". 最近使用: %1$s 執行緒 "blob" 請求失敗 執行緒 "blob" 請求成功, 寫入離線快取 @@ -335,7 +337,17 @@ 自定義 打包外掛未安裝 AutoJs6 無 root 許可權, 無法錄製指令碼 + 開發者 + 合作者 + 首次安裝 + 已安裝版本 + 最近安裝 + 最近解除安裝 + 最近更新 + 安裝包大小 + 可更新版本 確定忽略當前更新版本嗎?\n在應用設定中可管理已忽略的所有版本. + 當前檔案可能不是有效的外掛包, 是否繼續安裝?\n\nURI: \"%1$s\" 需要重啟應用才能完成文件源切換 部分內容可能需要重啟應用才能完成語言切換 螢幕捕獲器前臺服務 @@ -431,6 +443,8 @@ 文件 AutoJs6 日誌 日誌 + AutoJs6 Plugin Center + Plugins AutoJs6 設定 設定 軟體原始碼 @@ -479,9 +493,11 @@ 清空檔案選擇 清空預執行指令碼 清除更新檢查狀態 + Click icon to add launcher shortcut 點選條目可移除 點選條目可檢視詳情 點選 \"確定\" 跳轉到設定頁面 + Click other areas to exit selection 操作頻率過快 客戶端模式 克隆顏色庫 @@ -538,6 +554,7 @@ 刪除 刪除全部 刪除行 + 描述 詳情 \"開發者詳情\" 正在開發中... 開發者選項 @@ -551,6 +568,7 @@ 裝置螢幕解析度 直接下載 資料夾 + 已禁用 顯示在其他應用上層 建議授予 \"顯示在其他應用上層\" 許可權以確保應用視窗元件正常顯示 一次性任務 @@ -583,6 +601,8 @@ 使用 root 許可權啟用無障礙服務超時 使用修改安全設定許可權自動啟用無障礙服務 使用修改安全設定許可權啟用無障礙服務超時 + 啟用外掛 + 已啟用 錯誤 檔案複製失敗: %s 錯誤報告 @@ -613,10 +633,12 @@ 授權失敗 顯示在其他應用上層許可權授予失敗 匯入失敗 + 安裝失敗 定位失敗 登入失敗 註冊失敗 提交失敗 + 獲取失敗 無法儲存遠端專案至本地 日誌條目傳送失敗 檔案寫入失敗 @@ -633,6 +655,7 @@ 檔案名稱不能包含以下字符: \\ / : * ? " < > | 檔案名稱太長 檔案遷移 + 篩選 查詢 搜尋 Java 類 下一個 @@ -651,6 +674,7 @@ 已生成程式碼 正在獲取版本資訊... 備用 URL 已使用 + 全域性設定 開啟 \"設定\" 在 Shizuku 應用中授予 AutoJs6 許可權 已授予 @@ -678,6 +702,10 @@ 佈局範圍分析 佈局層次分析 安裝 + 從 \"本地檔案\" 安裝 + 從 \"URL\" 安裝 + 從 \"URL\" 安裝外掛 + 可安裝 無效字元已被移除 無效包名 無效專案 @@ -697,6 +725,7 @@ 密鑰尚未驗證 密鑰庫密碼 名稱 + 狀態 上次檢查: %s 最近活動 最近包名 @@ -875,6 +904,9 @@ 請輸入名稱 請稍候... 請稍後再嘗試此操作... + Plugin center + 外掛詳情 + Plugins 指標位置 切換 \"指標位置\" 顯示狀態失敗\n可能缺少 root 許可權 釋出通知許可權 @@ -990,6 +1022,7 @@ 簽名方案 大小 已匯出 %d 個條目 + 排序 原始碼路徑 特殊許可權 穩定模式 @@ -1031,8 +1064,11 @@ @string/text_under_development 撤銷 撤銷 + Uninstall 未知 未驗證 + Updatable + Update 更新 更新檢查狀態已清除 稍後提示 @@ -1075,14 +1111,8 @@ 修改安全設定 安全設定包含應用程式可讀但不可寫入的設定選項, 這些選項只能由 UI 或系統級別應用修改.\n被授予 \"修改安全設定許可權\" 後, 普通應用可直接修改上述安全設定 (例如無障礙服務). 修改系統設定 - Update - Plugin center - Plugins - Updatable - Uninstall - AutoJs6 Plugin Center - Plugins - Click icon to add launcher shortcut - Click other areas to exit selection + 完整性驗證失敗 + SHA-256 驗證不一致.\n\n期望值: %1$s\n實際值: %2$s + 當前外掛未提供可用的 URL diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 1e2765d6..1c27454a 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -108,6 +108,7 @@ 打开调色盘 放弃 移除 + 获取 重试 保存 系统设置 @@ -299,6 +300,7 @@ 长按 \"运行\" 图标可启动调试 开始循环前的延迟 0 表示无限循环 + 输入一个指向远程插件地址的 URL.\n例如 \"https://example.com/plugin.apk\". 最近使用: %1$s 线程 \"blob\" 请求失败 线程 \"blob\" 请求成功, 写入离线缓存 @@ -335,7 +337,17 @@ 自定义 打包插件未安装 AutoJs6 无 root 权限, 无法录制脚本 + 开发者 + 合作者 + 首次安装 + 已安装版本 + 最近安装 + 最近卸载 + 最近更新 + 安装包大小 + 可更新版本 确定忽略当前更新版本吗?\n在应用设置中可管理已忽略的所有版本. + 当前文件可能不是有效的插件包, 是否继续安装?\n\nURI: \"%1$s\" 需要重启应用才能完成文档源切换 部分内容可能需要重启应用才能完成语言切换 屏幕捕获器前台服务 @@ -431,6 +443,8 @@ 文档 AutoJs6 日志 日志 + AutoJs6 插件中心 + 插件 AutoJs6 设置 设置 软件源码 @@ -479,9 +493,11 @@ 清空文件选择 清空预执行脚本 清除更新检查状态 + 点击图标添加启动器快捷方式 点击条目可移除 点击条目可查看详情 点击 \"确定\" 跳转到设置页面 + 点击其他区域退出选择 操作频率过快 客户端模式 克隆颜色库 @@ -538,6 +554,7 @@ 删除 删除全部 删除行 + 描述 详情 \"开发者详情\" 正在开发中... 开发者选项 @@ -551,6 +568,7 @@ 设备屏幕分辨率 直接下载 文件夹 + 已禁用 显示在其他应用上层 建议授予 \"显示在其他应用上层\" 权限以确保应用窗口组件正常显示 一次性任务 @@ -583,6 +601,8 @@ 使用 root 权限启用无障碍服务超时 使用修改安全设置权限自动启用无障碍服务 使用修改安全设置权限启用无障碍服务超时 + 启用插件 + 已启用 错误 文件复制失败: %s 错误报告 @@ -613,10 +633,12 @@ 授权失败 显示在其他应用上层权限授予失败 导入失败 + 安装失败 定位失败 登录失败 注册失败 提交失败 + 获取失败 无法保存远程项目至本地 日志条目发送失败 文件写入失败 @@ -633,6 +655,7 @@ 文件名不能包含下列任何字符: \\ / : * ? " < > | 文件名太长 文件迁移 + 筛选 查找 搜索 Java 类 下一个 @@ -651,6 +674,7 @@ 已生成代码 正在获取版本信息... 备用 URL 已使用 + 全局设置 打开 \"设置\" 在 Shizuku 应用中授予 AutoJs6 权限 已授予 @@ -678,6 +702,10 @@ 布局范围分析 布局层次分析 安装 + 从 \"本地文件\" 安装 + 从 \"URL\" 安装 + 从 \"URL\" 安装插件 + 可安装 无效字符已被移除 无效包名 无效项目 @@ -697,6 +725,7 @@ 密钥尚未验证 密钥库密码 名称 + 状态 上次检查: %s 最近活动 最近包名 @@ -875,6 +904,9 @@ 请输入名称 请稍候... 请稍后再尝试此操作... + 插件中心 + 插件详情 + 插件 指针位置 切换 \"指针位置\" 显示状态失败\n可能缺少 root 权限 发布通知权限 @@ -990,6 +1022,7 @@ 签名方案 大小 已导出 %d 个条目 + 排序 源代码路径 特殊权限 稳定模式 @@ -1031,8 +1064,11 @@ @string/text_under_development 撤销 撤销 + 卸载 未知 未验证 + 可更新 + 更新 更新 更新检查状态已清除 稍后提示 @@ -1075,14 +1111,8 @@ 修改安全设置 安全设置包含应用程序可读但不可写入的设置选项, 这些选项只能由 UI 或系统级别应用修改.\n被授予 \"修改安全设置权限\" 后, 普通应用可直接修改上述安全设置 (例如无障碍服务). 修改系统设置 - 更新 - 插件中心 - 插件 - 可更新 - 卸载 - AutoJs6 插件中心 - 插件 - 点击图标添加启动器快捷方式 - 点击其他区域退出选择 + 完整性验证失败 + SHA-256 验证不一致.\n\n期望值: %1$s\n实际值: %2$s + 当前插件未提供可用的 URL diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 971828cf..087047bd 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -87,6 +87,7 @@ #009624 #0DA798 #A1887F + #A1887F @color/dialog_button_finish #BDBDBD #F57C00 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e9bb2689..f0ff9668 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -182,6 +182,7 @@ VERSION_BUILD VERSION_NAME : + | \? ABIs AccuWeather @@ -359,6 +360,7 @@ Palette Quit Remove + Retrieve Retry Save System Settings @@ -553,6 +555,7 @@ Long click \"Run\" button to debug Delay before loop 0 for infinite loop + Enter a URL pointing to a remote plugin address.\nFor example \"https://example.com/plugin.apk\". Latest used: %1$s \"Blob\" thread request failed \"Blob\" thread request successful, writing offline cache @@ -589,7 +592,17 @@ Custom APK Builder is not installed AutoJs6 has no root access to record a script + Author + Collaborators + First install + Installed ver. + Last install + Last uninstall + Last update + Package size + Updatable ver. Are you sure to ignore current update version?\nYou can manage all ignored versions by app settings. + The current file may not be a valid plugin package, do you want to continue with installation?\n\nURI: \"%1$s\" An app restart is needed to apply new settings An app restart may be needed to make language applied as expected Screen capturer foreground service @@ -685,6 +698,8 @@ Docs AutoJs6 Log Log + AutoJs6 Plugin Center + Plugins AutoJs6 Settings Settings Source code @@ -733,9 +748,11 @@ Clear file selection Clear pre-execute script Clear updates checked states + Click icon to add launcher shortcut Click the item to remove Click an item to view details Click \"OK\" to go to settings + Click other areas to exit selection Frequent operation Client mode Clone a color library @@ -792,6 +809,7 @@ Delete Delete All Delete line + Description Details Developer details is under development Developer options @@ -805,6 +823,7 @@ Device screen resolution Download now Directory + Disabled Display over other apps \"Display over other apps\" permission is recommended to make all widgets displayed properly Disposable task @@ -837,6 +856,8 @@ Enable accessibility service with root access timed out Enable accessibility service with secure settings automatically Enable accessibility service with secure settings timed out + Enable plugin + Enabled Error Failed to copy file: %s Bug report @@ -867,10 +888,12 @@ Failed to grant access Failed to grant display over other apps permission Failed to import + Failed to install Failed to locate Failed to login Failed to register Failed to submit + Failed to retrieve Failed to save remote project to local storage Failed to send log entries Failed to write file @@ -887,6 +910,7 @@ Filename cannot contain the following characters: \\ / : * ? " < > | Filename is too long Files transfer + Filter Find Find Java classes Next @@ -905,6 +929,7 @@ Generated code Retrieving release notes... Backup URL has been used + Global settings Go to \"Settings\" Grant AutoJs6 access in Shizuku app Granted @@ -932,6 +957,10 @@ Inspect layout bounds Inspect layout hierarchy Install + Install from \"Local File\" + Install from \"URL\" + Install plugin from \"URL\" + Installable Invalid character is removed Invalid package name Invalid project @@ -951,6 +980,7 @@ Keystore has not been verified Key Store Password Label + State Last checked: %s Latest activity Latest package @@ -1129,6 +1159,9 @@ Input name Please wait... Please wait a moment before trying again... + Plugin center + Plugin details + Plugins Pointer location Toggle \"pointer location\" failed.\nRoot access is required. Post notifications @@ -1244,6 +1277,7 @@ Signature Scheme Size %d items exported + Sort Source code path Special permissions Stable mode @@ -1285,8 +1319,11 @@ @string/text_under_development Undo Undo + Uninstall Unknown Unverified + Updatable + Update Updates Updates checked states cleared Later @@ -1329,14 +1366,8 @@ Write security settings Secure system settings, containing system preferences that applications can read but are not allowed to write.\nThese are for preferences that the user must explicitly modify through the UI of a system app.\nWith secure system settings permission, normal applications can directly modify the secure settings (such as accessibility service). Write system settings - Update - Plugin center - Plugins - Updatable - Uninstall - AutoJs6 Plugin Center - Plugins - Click icon to add launcher shortcut - Click other areas to exit selection + Integrity verification failed + SHA-256 mismatch.\n\nExpected: %1$s\nActual: %2$s + No available URL provided for current plugin diff --git a/plugin-api/paddle-ocr/src/main/aidl/org/autojs/plugin/paddle/ocr/PluginInfo.aidl b/plugin-api/paddle-ocr/src/main/aidl/org/autojs/plugin/paddle/ocr/PluginInfo.aidl index 8ea34005..a7cc456e 100644 --- a/plugin-api/paddle-ocr/src/main/aidl/org/autojs/plugin/paddle/ocr/PluginInfo.aidl +++ b/plugin-api/paddle-ocr/src/main/aidl/org/autojs/plugin/paddle/ocr/PluginInfo.aidl @@ -12,7 +12,7 @@ parcelable PluginInfo { long versionCode; @nullable String versionDate; - /** @example "paddle-ocr-v5" */ + /** @example "paddle-ocr-pp-ocrv5" */ @nullable String id; /** @sample "paddle-ocr" */ @nullable String engine; diff --git a/version.properties b/version.properties index 2216ad59..70c2fec5 100644 --- a/version.properties +++ b/version.properties @@ -1,5 +1,5 @@ -#Fri Nov 28 14:10:00 CST 2025 -BUILD_TIME=1764310200816 +#Mon Dec 08 20:22:09 CST 2025 +BUILD_TIME=1765196529144 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=3516 +VERSION_BUILD=3528 VERSION_NAME=6.7.0 Alpha12 VSCODE_EXT_REQUIRED_VERSION=1.0.8