6.7.0 - Alpha12 - 插件中心 M1 - 插件中心完善更新流程 (更新提示/下载/校验/安装/失败重试)
This commit is contained in:
@@ -13,7 +13,6 @@ import com.afollestad.materialdialogs.MaterialDialog
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.autojs.autojs.extension.MaterialDialogExtensions.widgetThemeColor
|
import org.autojs.autojs.extension.MaterialDialogExtensions.widgetThemeColor
|
||||||
import org.autojs.autojs.ui.BaseActivity
|
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
|
||||||
import org.autojs.autojs.util.ViewUtils.setMenuIconsColorByThemeColorLuminance
|
import org.autojs.autojs.util.ViewUtils.setMenuIconsColorByThemeColorLuminance
|
||||||
import org.autojs.autojs.util.ViewUtils.setNavigationIconColorByThemeColorLuminance
|
import org.autojs.autojs.util.ViewUtils.setNavigationIconColorByThemeColorLuminance
|
||||||
@@ -74,15 +73,16 @@ class PluginCenterActivity : BaseActivity() {
|
|||||||
positiveButton.setOnClickListener {
|
positiveButton.setOnClickListener {
|
||||||
d.dismiss()
|
d.dismiss()
|
||||||
val url = input.trim().toString()
|
val url = input.trim().toString()
|
||||||
|
val context = this@PluginCenterActivity
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
runCatching {
|
runCatching {
|
||||||
PluginInstaller.installFromUrlWithPrompt(this@PluginCenterActivity, url)
|
PluginInstaller.installFromUrlWithPrompt(context, url)
|
||||||
}.onFailure { e ->
|
}.onFailure { e ->
|
||||||
ErrorDialogActivity.showErrorDialog(
|
MaterialDialog.Builder(context)
|
||||||
this@PluginCenterActivity,
|
.title(R.string.text_failed_to_retrieve)
|
||||||
R.string.text_failed_to_retrieve,
|
.content(e.message ?: e.toString())
|
||||||
e.message ?: e.toString(),
|
.positiveText(R.string.dialog_button_dismiss)
|
||||||
)
|
.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL
|
import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.autojs.autojs.util.ViewUtils.excludePaddingClippableViewFromBottomNavigationBar
|
import org.autojs.autojs.util.ViewUtils.excludePaddingClippableViewFromBottomNavigationBar
|
||||||
@@ -48,14 +49,42 @@ class PluginCenterFragment : Fragment(R.layout.fragment_plugin_center) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onUninstall(item: PluginCenterItem) {
|
override fun onUninstall(item: PluginCenterItem) {
|
||||||
val uri = Uri.parse("package:${item.packageName}")
|
MaterialDialog.Builder(context)
|
||||||
val intent = Intent(Intent.ACTION_DELETE, uri)
|
.title(R.string.text_prompt)
|
||||||
uninstallLauncher.launch(intent)
|
.content(R.string.text_confirm_to_uninstall)
|
||||||
|
.negativeText(R.string.dialog_button_cancel)
|
||||||
|
.neutralColorRes(R.color.dialog_button_default)
|
||||||
|
.positiveText(R.string.dialog_button_confirm)
|
||||||
|
.positiveColorRes(R.color.dialog_button_caution)
|
||||||
|
.onPositive { _, _ ->
|
||||||
|
val uri = Uri.parse("package:${item.packageName}")
|
||||||
|
val intent = Intent(Intent.ACTION_DELETE, uri)
|
||||||
|
uninstallLauncher.launch(intent)
|
||||||
|
}
|
||||||
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDetails(item: PluginCenterItem) {
|
override fun onDetails(item: PluginCenterItem) {
|
||||||
PluginInfoDialogManager.showPluginInfoDialog(contextRef, item)
|
PluginInfoDialogManager.showPluginInfoDialog(contextRef, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onUpdate(item: PluginCenterItem) {
|
||||||
|
val url = item.installableApkUrl
|
||||||
|
when {
|
||||||
|
url.isNullOrBlank() -> MaterialDialog.Builder(contextRef)
|
||||||
|
.title(R.string.text_failed_to_update)
|
||||||
|
.content(R.string.error_no_available_url_provided_for_current_plugin)
|
||||||
|
.positiveText(R.string.dialog_button_dismiss)
|
||||||
|
.show()
|
||||||
|
else -> viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
PluginInstaller.installFromUrlWithPrompt(
|
||||||
|
context = contextRef,
|
||||||
|
url = url,
|
||||||
|
expectedSha256 = item.installableApkSha256,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
binding.pluginCenterRecyclerView.apply {
|
binding.pluginCenterRecyclerView.apply {
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
package org.autojs.autojs.core.plugin.center
|
package org.autojs.autojs.core.plugin.center
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
|
import org.autojs.autojs6.R
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
import org.joda.time.format.DateTimeFormat
|
import org.joda.time.format.DateTimeFormat
|
||||||
|
|
||||||
@@ -37,8 +42,12 @@ data class PluginCenterItem(
|
|||||||
val versionSummary: String
|
val versionSummary: String
|
||||||
get() = formatVersionInfo(versionName, versionCode, versionDate)
|
get() = formatVersionInfo(versionName, versionCode, versionDate)
|
||||||
|
|
||||||
val updatableVersionSummary: String?
|
val updatableVersionSummary: String
|
||||||
get() = updatableVersionName?.let { formatVersionInfo(it, updatableVersionCode, updatableVersionDate) }
|
get() = formatVersionInfo(
|
||||||
|
updatableVersionName ?: versionName,
|
||||||
|
updatableVersionCode ?: versionCode,
|
||||||
|
updatableVersionDate,
|
||||||
|
)
|
||||||
|
|
||||||
val isUpdatable: Boolean
|
val isUpdatable: Boolean
|
||||||
get() = updatableVersionName != null
|
get() = updatableVersionName != null
|
||||||
@@ -63,4 +72,23 @@ data class PluginCenterItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun uninstall(context: Context) {
|
||||||
|
context.startActivity(Intent(Intent.ACTION_DELETE, "package:$packageName".toUri()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun uninstallWithPrompt(context: Context, dialog: MaterialDialog? = null) {
|
||||||
|
MaterialDialog.Builder(context)
|
||||||
|
.title(R.string.text_prompt)
|
||||||
|
.content(R.string.text_confirm_to_uninstall)
|
||||||
|
.negativeText(R.string.dialog_button_cancel)
|
||||||
|
.neutralColorRes(R.color.dialog_button_default)
|
||||||
|
.positiveText(R.string.dialog_button_confirm)
|
||||||
|
.positiveColorRes(R.color.dialog_button_caution)
|
||||||
|
.onPositive { _, _ ->
|
||||||
|
runCatching { uninstall(context) }
|
||||||
|
dialog?.dismiss()
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ class PluginCenterItemAdapter(
|
|||||||
fun onToggleEnable(item: PluginCenterItem, enabled: Boolean)
|
fun onToggleEnable(item: PluginCenterItem, enabled: Boolean)
|
||||||
fun onUninstall(item: PluginCenterItem)
|
fun onUninstall(item: PluginCenterItem)
|
||||||
fun onDetails(item: PluginCenterItem)
|
fun onDetails(item: PluginCenterItem)
|
||||||
|
fun onUpdate(item: PluginCenterItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -18,8 +18,6 @@ import org.autojs.autojs.util.ViewUtils
|
|||||||
import org.autojs.autojs.util.ViewUtils.colorFilterWithDesaturateOrNull
|
import org.autojs.autojs.util.ViewUtils.colorFilterWithDesaturateOrNull
|
||||||
import org.autojs.autojs6.R
|
import org.autojs.autojs6.R
|
||||||
import org.autojs.autojs6.databinding.PluginCenterRecyclerViewItemBinding
|
import org.autojs.autojs6.databinding.PluginCenterRecyclerViewItemBinding
|
||||||
import org.joda.time.DateTime
|
|
||||||
import org.joda.time.format.DateTimeFormat
|
|
||||||
|
|
||||||
class PluginCenterItemViewHolder(
|
class PluginCenterItemViewHolder(
|
||||||
itemViewBinding: PluginCenterRecyclerViewItemBinding,
|
itemViewBinding: PluginCenterRecyclerViewItemBinding,
|
||||||
@@ -83,32 +81,15 @@ class PluginCenterItemViewHolder(
|
|||||||
btnDeleteView.setButtonState(false)
|
btnDeleteView.setButtonState(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.isInstalled && item.isUpdatable) {
|
val showUpdate = item.isInstalled && item.isUpdatable
|
||||||
updatableBadgeView.isVisible = true
|
updatableBadgeView.isVisible = showUpdate
|
||||||
versionInfoForUpdateView.isVisible = true
|
versionInfoForUpdateView.isVisible = showUpdate
|
||||||
|
if (showUpdate) {
|
||||||
// test
|
versionInfoForUpdateView.text = item.updatableVersionSummary
|
||||||
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) {
|
btnUpdateView.setButtonState(true) {
|
||||||
ViewUtils.showToast(context, R.string.text_under_development)
|
listener.onUpdate(currentItem)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
updatableBadgeView.isVisible = false
|
|
||||||
versionInfoForUpdateView.isVisible = false
|
|
||||||
btnUpdateView.setButtonState(false)
|
btnUpdateView.setButtonState(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,18 +113,6 @@ class PluginCenterItemViewHolder(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun LinearLayout.setButtonState(enabled: Boolean, onClickListener: View.OnClickListener? = null) {
|
private fun LinearLayout.setButtonState(enabled: Boolean, onClickListener: View.OnClickListener? = null) {
|
||||||
this.isEnabled = enabled
|
this.isEnabled = enabled
|
||||||
this.setOnClickListener(onClickListener)
|
this.setOnClickListener(onClickListener)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import org.autojs.autojs6.R
|
|||||||
/**
|
/**
|
||||||
* Loads both index plugins and locally installed plugins,
|
* Loads both index plugins and locally installed plugins,
|
||||||
* merging them into a PluginCenterItem list.
|
* merging them into a PluginCenterItem list.
|
||||||
*
|
*
|
||||||
* zh-CN: 统一加载索引插件与本地已安装插件, 合并为 PluginCenterItem 列表.
|
* zh-CN: 统一加载索引插件与本地已安装插件, 合并为 PluginCenterItem 列表.
|
||||||
*/
|
*/
|
||||||
class PluginCenterViewModel : ViewModel() {
|
class PluginCenterViewModel : ViewModel() {
|
||||||
@@ -34,15 +34,15 @@ class PluginCenterViewModel : ViewModel() {
|
|||||||
|
|
||||||
val installedByPkg = installed.associateBy { it.packageName }
|
val installedByPkg = installed.associateBy { it.packageName }
|
||||||
|
|
||||||
// 1. Use index to drive UI first (ensuring "installable but not installed" items are displayed).
|
// Use index to drive UI, to display installable and updatable items.
|
||||||
// 1. [ zh-CN ] 优先用索引驱动 UI (确保 "未安装但可安装" 的项也能显示).
|
// zh-CN: 用索引驱动 UI, 展示可安装及可更新的项.
|
||||||
val fromIndex = indexEntries.map { e ->
|
val fromIndex = indexEntries.map { e ->
|
||||||
val local = installedByPkg[e.packageName]
|
val local = installedByPkg[e.packageName]
|
||||||
toPluginCenterItem(context, index = e, local = local)
|
toPluginCenterItem(context, index = e, local = local)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Add items that "exist locally but not in index" (third-party or not indexed yet).
|
// Add items that "exist locally but not in index" (third-party or not indexed yet).
|
||||||
// 2. [ zh-CN ] 补充 "本地存在但索引里暂时没有" 的项 (第三方或暂未入索引).
|
// zh-CN: 补充 "本地存在但索引里暂时没有" 的项 (第三方或暂未入索引).
|
||||||
val extraLocals = installed
|
val extraLocals = installed
|
||||||
.filter { ins -> indexEntries.none { it.packageName == ins.packageName } }
|
.filter { ins -> indexEntries.none { it.packageName == ins.packageName } }
|
||||||
.map { local -> toPluginCenterItem(context, index = null, local = local) }
|
.map { local -> toPluginCenterItem(context, index = null, local = local) }
|
||||||
@@ -57,8 +57,6 @@ class PluginCenterViewModel : ViewModel() {
|
|||||||
|
|
||||||
fun setEnabled(context: Context, packageName: String, enabled: Boolean) {
|
fun setEnabled(context: Context, packageName: String, enabled: Boolean) {
|
||||||
enableStore.setEnabled(context, packageName, enabled)
|
enableStore.setEnabled(context, packageName, enabled)
|
||||||
// Update in-memory state too, to avoid a second full refresh.
|
|
||||||
// zh-CN: 内存态也更新, 避免二次全量刷新.
|
|
||||||
_items.value = _items.value.map {
|
_items.value = _items.value.map {
|
||||||
if (it.packageName == packageName) it.copy(isEnabled = enabled) else it
|
if (it.packageName == packageName) it.copy(isEnabled = enabled) else it
|
||||||
}
|
}
|
||||||
@@ -72,20 +70,31 @@ class PluginCenterViewModel : ViewModel() {
|
|||||||
val author = local?.author ?: index?.author
|
val author = local?.author ?: index?.author
|
||||||
val collaborators = index?.collaborators ?: emptyList()
|
val collaborators = index?.collaborators ?: emptyList()
|
||||||
|
|
||||||
val versionName = local?.versionName ?: index?.versionName ?: context.getString(R.string.text_unknown)
|
val versionNameLocal = local?.versionName ?: index?.versionName ?: context.getString(R.string.text_unknown)
|
||||||
|
val versionCodeLocal = local?.versionCode
|
||||||
val isInstalled = local != null
|
val isInstalled = local != null
|
||||||
|
|
||||||
|
// Mark as updatable and populate update target information only when "installed and index version is higher".
|
||||||
|
// zh-CN: 仅当 "已安装且索引版本更高" 时, 标记可更新, 并填充可更新目标信息.
|
||||||
|
val (updatableName, updatableCode, updatableDate) = run {
|
||||||
|
val defaultVersionInfo = Triple(null, null, null)
|
||||||
|
versionCodeLocal ?: return@run defaultVersionInfo
|
||||||
|
val versionCodeIndex = index?.versionCode ?: return@run defaultVersionInfo
|
||||||
|
if (versionCodeIndex <= versionCodeLocal) return@run defaultVersionInfo
|
||||||
|
Triple(index.versionName, index.versionCode, index.versionDate)
|
||||||
|
}
|
||||||
|
|
||||||
val enabled = enableStore.isEnabled(context, packageName, defaultEnabled = isInstalled)
|
val enabled = enableStore.isEnabled(context, packageName, defaultEnabled = isInstalled)
|
||||||
|
|
||||||
return PluginCenterItem(
|
return PluginCenterItem(
|
||||||
title = title,
|
title = title,
|
||||||
packageName = packageName,
|
packageName = packageName,
|
||||||
versionName = versionName,
|
versionName = versionNameLocal,
|
||||||
versionCode = local?.versionCode ?: index?.versionCode,
|
versionCode = versionCodeLocal ?: index?.versionCode,
|
||||||
// TODO M1: 显示索引日期; 仅本地项时可为空.
|
|
||||||
versionDate = index?.versionDate,
|
versionDate = index?.versionDate,
|
||||||
updatableVersionName = index?.versionName,
|
updatableVersionName = updatableName,
|
||||||
updatableVersionCode = index?.versionCode,
|
updatableVersionCode = updatableCode,
|
||||||
updatableVersionDate = index?.versionDate,
|
updatableVersionDate = updatableDate,
|
||||||
author = author,
|
author = author,
|
||||||
collaborators = collaborators,
|
collaborators = collaborators,
|
||||||
description = description,
|
description = description,
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ class PluginIndexRepository {
|
|||||||
description = "百度飞桨光学字符识别插件",
|
description = "百度飞桨光学字符识别插件",
|
||||||
author = "SuperMonster003",
|
author = "SuperMonster003",
|
||||||
collaborators = emptyList(),
|
collaborators = emptyList(),
|
||||||
versionName = "0.1.0",
|
versionName = "0.1.5",
|
||||||
versionCode = 17L,
|
versionCode = 15L,
|
||||||
versionDate = "2025-11-21",
|
versionDate = "2025-11-25",
|
||||||
// TODO M1 若索引的下载地址/哈希/尺寸未知, 则先设置为 null.
|
// TODO M1 若索引的下载地址/哈希/尺寸未知, 则先设置为 null.
|
||||||
apkUrl = null,
|
apkUrl = null,
|
||||||
apkSha256 = null,
|
apkSha256 = null,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package org.autojs.autojs.core.plugin.center
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.graphics.PorterDuff
|
import android.graphics.PorterDuff
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View.MeasureSpec.UNSPECIFIED
|
import android.view.View.MeasureSpec.UNSPECIFIED
|
||||||
@@ -10,7 +9,6 @@ import android.widget.TextView
|
|||||||
import androidx.appcompat.content.res.AppCompatResources
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.core.graphics.drawable.DrawableCompat
|
import androidx.core.graphics.drawable.DrawableCompat
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.afollestad.materialdialogs.DialogAction
|
import com.afollestad.materialdialogs.DialogAction
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
@@ -72,6 +70,7 @@ object PluginInfoDialogManager {
|
|||||||
lastInstallTime = item.lastInstallTime,
|
lastInstallTime = item.lastInstallTime,
|
||||||
lastUninstallTime = item.lastUninstallTime,
|
lastUninstallTime = item.lastUninstallTime,
|
||||||
apkUrl = item.installableApkUrl,
|
apkUrl = item.installableApkUrl,
|
||||||
|
sha256 = item.installableApkSha256,
|
||||||
)
|
)
|
||||||
showPluginInfoDialogInternal(context, item, info)
|
showPluginInfoDialogInternal(context, item, info)
|
||||||
}
|
}
|
||||||
@@ -93,6 +92,8 @@ object PluginInfoDialogManager {
|
|||||||
updatableVersion = item.updatableVersionSummary,
|
updatableVersion = item.updatableVersionSummary,
|
||||||
firstInstallTime = item.firstInstallTime,
|
firstInstallTime = item.firstInstallTime,
|
||||||
lastUpdateTime = item.lastUpdateTime,
|
lastUpdateTime = item.lastUpdateTime,
|
||||||
|
apkUrl = item.installableApkUrl,
|
||||||
|
sha256 = item.installableApkSha256,
|
||||||
)
|
)
|
||||||
showPluginInfoDialogInternal(context, item, info)
|
showPluginInfoDialogInternal(context, item, info)
|
||||||
}
|
}
|
||||||
@@ -114,34 +115,50 @@ object PluginInfoDialogManager {
|
|||||||
positiveText(R.string.text_install)
|
positiveText(R.string.text_install)
|
||||||
positiveColorRes(R.color.dialog_button_attraction)
|
positiveColorRes(R.color.dialog_button_attraction)
|
||||||
onPositive { d, _ ->
|
onPositive { d, _ ->
|
||||||
info.apkUrl?.let { url ->
|
val url = info.apkUrl
|
||||||
d.dismiss()
|
when {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
url.isNullOrBlank() -> {
|
||||||
PluginInstaller.installFromUrlWithPrompt(context, url, item.installableApkSha256)
|
MaterialDialog.Builder(context)
|
||||||
|
.title(R.string.text_failed_to_install)
|
||||||
|
.content(R.string.error_no_available_url_provided_for_current_plugin)
|
||||||
|
.positiveText(R.string.dialog_button_dismiss)
|
||||||
|
.show()
|
||||||
|
val positiveButton = d.getActionButton(DialogAction.POSITIVE)
|
||||||
|
positiveButton.setTextColor(d.context.getColor(R.color.dialog_button_unavailable))
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
d.dismiss()
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
PluginInstaller.installFromUrlWithPrompt(context, url, info.sha256)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} ?: 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 -> {
|
is PluginInfoInstalled -> {
|
||||||
positiveText(R.string.text_uninstall)
|
positiveText(R.string.text_uninstall)
|
||||||
positiveColorRes(R.color.dialog_button_warn)
|
positiveColorRes(R.color.dialog_button_warn)
|
||||||
onPositive { d, _ ->
|
onPositive { d, _ -> item.uninstallWithPrompt(context, d) }
|
||||||
// TODO 确定卸载对话框.
|
|
||||||
d.dismiss()
|
|
||||||
context.startActivity(Intent(Intent.ACTION_DELETE, "package:${item.packageName}".toUri()))
|
|
||||||
}
|
|
||||||
if (item.isUpdatable) {
|
if (item.isUpdatable) {
|
||||||
neutralText(R.string.text_update)
|
neutralText(R.string.text_update)
|
||||||
neutralColorRes(R.color.dialog_button_attraction)
|
neutralColorRes(R.color.dialog_button_attraction)
|
||||||
onNeutral { d, _ ->
|
onNeutral { d, _ ->
|
||||||
d.dismiss()
|
val url = info.apkUrl
|
||||||
// TODO 后续实现更新逻辑 (下载新包 -> 安装).
|
when {
|
||||||
|
url.isNullOrBlank() -> {
|
||||||
|
MaterialDialog.Builder(context)
|
||||||
|
.title(R.string.text_failed_to_update)
|
||||||
|
.content(R.string.error_no_available_url_provided_for_current_plugin)
|
||||||
|
.positiveText(R.string.dialog_button_dismiss)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
d.dismiss()
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
PluginInstaller.installFromUrlWithPrompt(context, url, info.sha256)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,17 +211,17 @@ object PluginInfoDialogManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Installable package: If the index does not provide size, try to HEAD request to get it, update display after success.
|
// If the index does not provide size, try to HEAD request to get it, update display after success.
|
||||||
// zh-CN: 可安装包: 若索引未给 size, 尝试 HEAD 获取, 成功后更新显示.
|
// zh-CN: 若索引未给 size, 尝试 HEAD 获取, 成功后更新显示.
|
||||||
if (info is PluginInfoInstallable && info.packageSize <= 0 && !info.apkUrl.isNullOrBlank()) {
|
if (info.packageSize <= 0) {
|
||||||
// Asynchronously probe the size and refresh the view.
|
info.apkUrl.takeUnless { it.isNullOrBlank() }?.let { url ->
|
||||||
// zh-CN: 异步探测大小并刷新视图.
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
val size = PluginInstaller.probeContentLength(url)
|
||||||
val size = PluginInstaller.probeContentLength(info.apkUrl)
|
if (size != null && size > 0 && currentDialog?.get() === dialog) {
|
||||||
if (size != null && size > 0 && currentDialog?.get() === dialog) {
|
// TODO 更新当前 item 的 "可安装包大小" 仅用于对话框展示 (持久化可留到 M2).
|
||||||
// TODO 更新当前 item 的 "可安装包大小" 仅用于对话框展示 (持久化可留到 M2).
|
withContext(Dispatchers.Main) {
|
||||||
withContext(Dispatchers.Main) {
|
dialog.setCopyableTextIfAbsent(binding.pluginItemInfoPackageSizeValue, formatSize(size))
|
||||||
dialog.setCopyableTextIfAbsent(binding.pluginItemInfoPackageSizeValue, formatSize(size))
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -300,6 +317,8 @@ object PluginInfoDialogManager {
|
|||||||
val collaborators: List<String>
|
val collaborators: List<String>
|
||||||
val description: String
|
val description: String
|
||||||
val packageSize: Long
|
val packageSize: Long
|
||||||
|
val apkUrl: String?
|
||||||
|
val sha256: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class PluginInfoInstallable(
|
private data class PluginInfoInstallable(
|
||||||
@@ -311,7 +330,8 @@ object PluginInfoDialogManager {
|
|||||||
override val collaborators: List<String>,
|
override val collaborators: List<String>,
|
||||||
override val description: String,
|
override val description: String,
|
||||||
override val packageSize: Long,
|
override val packageSize: Long,
|
||||||
val apkUrl: String?,
|
override val apkUrl: String?,
|
||||||
|
override val sha256: String?,
|
||||||
val lastInstallTime: Long?,
|
val lastInstallTime: Long?,
|
||||||
val lastUninstallTime: Long?,
|
val lastUninstallTime: Long?,
|
||||||
) : PluginInfoBase
|
) : PluginInfoBase
|
||||||
@@ -325,6 +345,8 @@ object PluginInfoDialogManager {
|
|||||||
override val collaborators: List<String>,
|
override val collaborators: List<String>,
|
||||||
override val description: String,
|
override val description: String,
|
||||||
override val packageSize: Long,
|
override val packageSize: Long,
|
||||||
|
override val apkUrl: String?,
|
||||||
|
override val sha256: String?,
|
||||||
val updatableVersion: String? = null,
|
val updatableVersion: String? = null,
|
||||||
val firstInstallTime: Long?,
|
val firstInstallTime: Long?,
|
||||||
val lastUpdateTime: Long?,
|
val lastUpdateTime: Long?,
|
||||||
|
|||||||
@@ -12,12 +12,11 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.autojs.autojs.network.download.DownloadManager
|
import org.autojs.autojs.network.download.DownloadManager
|
||||||
import org.autojs.autojs.runtime.api.Mime
|
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.ui.main.scripts.ApkInfoDialogManager
|
||||||
import org.autojs.autojs.util.FileUtils
|
import org.autojs.autojs.util.FileUtils
|
||||||
import org.autojs.autojs.util.FileUtils.toCacheFile
|
import org.autojs.autojs.util.FileUtils.toCacheFile
|
||||||
|
import org.autojs.autojs.util.UpdateUtils
|
||||||
import org.autojs.autojs6.R
|
import org.autojs.autojs6.R
|
||||||
import org.spongycastle.pqc.math.linearalgebra.IntegerFunctions.pow
|
|
||||||
import java.io.EOFException
|
import java.io.EOFException
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@@ -30,14 +29,14 @@ import kotlin.math.roundToInt
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Installer:
|
* Installer:
|
||||||
* - Local file: Uri installation (display APK info dialog before installation)
|
* - Local file: Uri installation
|
||||||
* - URL: Install after downloading to cache (display progress dialog during download)
|
* - URL: Install after downloading to cache
|
||||||
*
|
*
|
||||||
* zh-CN:
|
* zh-CN:
|
||||||
*
|
*
|
||||||
* 安装器:
|
* 安装器:
|
||||||
* - 本地文件: Uri 安装 (安装前显示 APK 信息对话框)
|
* - 本地文件: Uri 安装
|
||||||
* - URL: 下载到缓存后安装 (下载时显示进度对话框)
|
* - URL: 下载到缓存后安装
|
||||||
*/
|
*/
|
||||||
object PluginInstaller {
|
object PluginInstaller {
|
||||||
|
|
||||||
@@ -67,11 +66,11 @@ object PluginInstaller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.onFailure { e ->
|
}.onFailure { e ->
|
||||||
ErrorDialogActivity.showErrorDialog(
|
MaterialDialog.Builder(context)
|
||||||
context.applicationContext,
|
.title(R.string.text_failed_to_install)
|
||||||
R.string.text_failed_to_install,
|
.content(e.message ?: e.toString())
|
||||||
e.message ?: e.toString(),
|
.positiveText(R.string.dialog_button_dismiss)
|
||||||
)
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun installFromFileUri(context: Context, uri: Uri) {
|
fun installFromFileUri(context: Context, uri: Uri) {
|
||||||
@@ -84,29 +83,77 @@ object PluginInstaller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun installFromUrlWithPrompt(context: Context, url: String, expectedSha256: String? = null) {
|
suspend fun installFromUrlWithPrompt(context: Context, url: String, expectedSha256: String? = null) {
|
||||||
when (val result = downloadWithProgress(context, url, expectedSha256)) {
|
var attempt = 0
|
||||||
is DownloadResult.Success -> installFromFileUriWithPrompt(context, result.uri)
|
while (true) {
|
||||||
is DownloadResult.Failure -> ErrorDialogActivity.showErrorDialog(
|
when (val result = downloadWithProgress(context, url, expectedSha256)) {
|
||||||
context,
|
is DownloadResult.Success -> {
|
||||||
result.titleRes,
|
installFromFileUriWithPrompt(context, result.uri)
|
||||||
result.message,
|
return
|
||||||
)
|
}
|
||||||
is DownloadResult.Cancelled -> {
|
is DownloadResult.Cancelled -> {
|
||||||
// User cancelled, no need to prompt.
|
// User cancelled, no need to prompt.
|
||||||
// zh-CN: 用户取消, 无需提示.
|
// zh-CN: 用户取消, 无需提示.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
is DownloadResult.Failure -> {
|
||||||
|
val retry = showRetryDialog(context, result)
|
||||||
|
if (retry) {
|
||||||
|
attempt++
|
||||||
|
// TODO M1: 不做自动退避, 交给用户控制重试节奏; M2 可引入指数退避/网络可用性判断
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun installFromUrl(context: Context, url: String, expectedSha256: String? = null) {
|
suspend fun installFromUrl(context: Context, url: String, expectedSha256: String? = null) {
|
||||||
val result = downloadWithProgress(context, url, expectedSha256)
|
when (val result = downloadWithProgress(context, url, expectedSha256)) {
|
||||||
if (result is DownloadResult.Success) {
|
is DownloadResult.Success -> {
|
||||||
installFromFileUri(context, result.uri)
|
installFromFileUri(context, result.uri)
|
||||||
} else if (result is DownloadResult.Failure) {
|
}
|
||||||
ErrorDialogActivity.showErrorDialog(context, result.titleRes, result.message)
|
is DownloadResult.Failure -> {
|
||||||
|
MaterialDialog.Builder(context)
|
||||||
|
.title(result.titleRes)
|
||||||
|
.content(result.message)
|
||||||
|
.positiveText(R.string.dialog_button_dismiss)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showRetryDialog(context: Context, failure: DownloadResult.Failure): Boolean {
|
||||||
|
var wantRetry = false
|
||||||
|
MaterialDialog.Builder(context)
|
||||||
|
.title(failure.titleRes)
|
||||||
|
.content(failure.message)
|
||||||
|
.neutralText(R.string.dialog_button_exception_details)
|
||||||
|
.neutralColorRes(R.color.dialog_button_hint)
|
||||||
|
.onNeutral { _, _ ->
|
||||||
|
MaterialDialog.Builder(context)
|
||||||
|
.title(failure.titleRes)
|
||||||
|
.content(failure.message)
|
||||||
|
.positiveText(R.string.dialog_button_dismiss)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
.negativeText(R.string.dialog_button_quit)
|
||||||
|
.negativeColorRes(R.color.dialog_button_default)
|
||||||
|
.onNegative { d, _ -> d.dismiss() }
|
||||||
|
.positiveText(R.string.dialog_button_retry)
|
||||||
|
.positiveColorRes(R.color.dialog_button_attraction)
|
||||||
|
.onPositive { d, _ ->
|
||||||
|
wantRetry = true
|
||||||
|
d.dismiss()
|
||||||
|
}
|
||||||
|
.cancelable(false)
|
||||||
|
.autoDismiss(true)
|
||||||
|
.show()
|
||||||
|
return wantRetry
|
||||||
|
}
|
||||||
|
|
||||||
// Probe URL size (HEAD).
|
// Probe URL size (HEAD).
|
||||||
// zh-CN: 探测 URL 大小 (HEAD).
|
// zh-CN: 探测 URL 大小 (HEAD).
|
||||||
suspend fun probeContentLength(url: String): Long? = withContext(Dispatchers.IO) {
|
suspend fun probeContentLength(url: String): Long? = withContext(Dispatchers.IO) {
|
||||||
@@ -136,11 +183,29 @@ object PluginInstaller {
|
|||||||
val dialog = MaterialDialog.Builder(context)
|
val dialog = MaterialDialog.Builder(context)
|
||||||
.title(R.string.text_downloading)
|
.title(R.string.text_downloading)
|
||||||
.progress(false, 100, true)
|
.progress(false, 100, true)
|
||||||
.negativeText(R.string.text_cancel)
|
.neutralText(R.string.dialog_button_download_with_browser)
|
||||||
.negativeColorRes(R.color.dialog_button_default)
|
.neutralColorRes(R.color.dialog_button_hint)
|
||||||
.onNegative { d, _ ->
|
.onNeutral { d, _ ->
|
||||||
|
MaterialDialog.Builder(context)
|
||||||
|
.title(R.string.text_prompt)
|
||||||
|
.content(R.string.text_download_interruption_warning)
|
||||||
|
.negativeText(R.string.dialog_button_back)
|
||||||
|
.negativeColorRes(R.color.dialog_button_hint)
|
||||||
|
.positiveText(R.string.dialog_button_continue)
|
||||||
|
.positiveColorRes(R.color.dialog_button_caution)
|
||||||
|
.onPositive { _, _ ->
|
||||||
|
d.getActionButton(DialogAction.POSITIVE).performClick()
|
||||||
|
UpdateUtils.openUrl(context, url)
|
||||||
|
}
|
||||||
|
.cancelable(false)
|
||||||
|
.build()
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
.positiveText(R.string.dialog_button_cancel_download)
|
||||||
|
.positiveColorRes(R.color.dialog_button_default)
|
||||||
|
.onPositive { d, _ ->
|
||||||
cancelFlag.set(true)
|
cancelFlag.set(true)
|
||||||
d.getActionButton(DialogAction.NEGATIVE).isEnabled = false
|
d.getActionButton(DialogAction.POSITIVE).isEnabled = false
|
||||||
}
|
}
|
||||||
.cancelable(false)
|
.cancelable(false)
|
||||||
.autoDismiss(false)
|
.autoDismiss(false)
|
||||||
@@ -168,6 +233,16 @@ object PluginInstaller {
|
|||||||
if (code !in 200..299) throw HttpStatusException(code, conn.responseMessage ?: "HTTP error")
|
if (code !in 200..299) throw HttpStatusException(code, conn.responseMessage ?: "HTTP error")
|
||||||
val total = conn.contentLengthLong.takeIf { it > 0 } ?: -1L
|
val total = conn.contentLengthLong.takeIf { it > 0 } ?: -1L
|
||||||
|
|
||||||
|
if (total > 0) {
|
||||||
|
dialog.setProgressNumberFormat(
|
||||||
|
DownloadManager.getProgressMegaBytesFormat(
|
||||||
|
context,
|
||||||
|
/* downloadedMiB */ 0f,
|
||||||
|
/* totalMiB */ total / (1024f * 1024f),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
conn.inputStream.use { input ->
|
conn.inputStream.use { input ->
|
||||||
val md = MessageDigest.getInstance("SHA-256")
|
val md = MessageDigest.getInstance("SHA-256")
|
||||||
DigestInputStream(input, md).use { din ->
|
DigestInputStream(input, md).use { din ->
|
||||||
@@ -183,17 +258,26 @@ object PluginInstaller {
|
|||||||
fos.write(buf, 0, read)
|
fos.write(buf, 0, read)
|
||||||
downloaded += read
|
downloaded += read
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
|
|
||||||
if (total > 0 && (now - lastUpdateTs > 80)) {
|
if (total > 0 && (now - lastUpdateTs > 80)) {
|
||||||
val pct = ((downloaded * 100f) / total).coerceIn(0f, 100f)
|
val pct = ((downloaded * 100f) / total).coerceIn(0f, 100f)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
dialog.setProgressNumberFormat(DownloadManager.getProgressMegaBytesFormat(
|
dialog.setProgressNumberFormat(
|
||||||
context,
|
DownloadManager.getProgressMegaBytesFormat(
|
||||||
downloaded.toFloat() / pow(2, 20),
|
context,
|
||||||
total.toFloat() / pow(2, 20),
|
downloaded / (1024f * 1024f),
|
||||||
))
|
total / (1024f * 1024f),
|
||||||
|
)
|
||||||
|
)
|
||||||
dialog.setProgress(pct.roundToInt())
|
dialog.setProgress(pct.roundToInt())
|
||||||
}
|
}
|
||||||
lastUpdateTs = now
|
lastUpdateTs = now
|
||||||
|
} else if (total <= 0 && (now - lastUpdateTs > 300)) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
val cur = dialog.currentProgress
|
||||||
|
dialog.setProgress((cur + 1).coerceAtMost(99))
|
||||||
|
}
|
||||||
|
lastUpdateTs = now
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fos.flush()
|
fos.flush()
|
||||||
@@ -215,7 +299,10 @@ object PluginInstaller {
|
|||||||
} catch (he: HttpStatusException) {
|
} catch (he: HttpStatusException) {
|
||||||
return DownloadResult.Failure(R.string.text_failed_to_retrieve, "HTTP ${he.code}: ${he.message}")
|
return DownloadResult.Failure(R.string.text_failed_to_retrieve, "HTTP ${he.code}: ${he.message}")
|
||||||
} catch (me: ChecksumMismatchException) {
|
} 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))
|
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) {
|
} catch (ioe: EOFException) {
|
||||||
return DownloadResult.Failure(R.string.text_failed_to_retrieve, "Unexpected EOF: ${ioe.message}")
|
return DownloadResult.Failure(R.string.text_failed_to_retrieve, "Unexpected EOF: ${ioe.message}")
|
||||||
} catch (ioe: IOException) {
|
} catch (ioe: IOException) {
|
||||||
@@ -236,6 +323,7 @@ object PluginInstaller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private data class HttpStatusException(val code: Int, override val message: String) : RuntimeException(message)
|
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")
|
private data class ChecksumMismatchException(val expected: String, val actual: String) : RuntimeException("sha256 mismatch")
|
||||||
|
|
||||||
sealed interface DownloadResult {
|
sealed interface DownloadResult {
|
||||||
@@ -243,4 +331,5 @@ object PluginInstaller {
|
|||||||
data class Failure(val titleRes: Int, val message: String) : DownloadResult
|
data class Failure(val titleRes: Int, val message: String) : DownloadResult
|
||||||
data object Cancelled : DownloadResult
|
data object Cancelled : DownloadResult
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1118,5 +1118,8 @@
|
|||||||
<string name="text_integrity_verification_failed">فشل التحقق من السلامة</string>
|
<string name="text_integrity_verification_failed">فشل التحقق من السلامة</string>
|
||||||
<string name="text_sha256_mismatch_multiline_expected_actual">عدم تطابق SHA-256.\n\nالمتوقّع: %1$s\nالفعلي: %2$s</string>
|
<string name="text_sha256_mismatch_multiline_expected_actual">عدم تطابق SHA-256.\n\nالمتوقّع: %1$s\nالفعلي: %2$s</string>
|
||||||
<string name="error_no_available_url_provided_for_current_plugin">لم يتم توفير عنوان URL متاح للملحق الحالي</string>
|
<string name="error_no_available_url_provided_for_current_plugin">لم يتم توفير عنوان URL متاح للملحق الحالي</string>
|
||||||
|
<string name="text_failed_to_update">فشل التحديث</string>
|
||||||
|
<string name="text_confirm_to_uninstall">هل تريد بالتأكيد إلغاء التثبيت؟</string>
|
||||||
|
<string name="dialog_button_exception_details">تفاصيل</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1113,5 +1113,8 @@
|
|||||||
<string name="text_integrity_verification_failed">Integrity verification failed</string>
|
<string name="text_integrity_verification_failed">Integrity verification failed</string>
|
||||||
<string name="text_sha256_mismatch_multiline_expected_actual">SHA-256 mismatch.\n\nExpected: %1$s\nActual: %2$s</string>
|
<string name="text_sha256_mismatch_multiline_expected_actual">SHA-256 mismatch.\n\nExpected: %1$s\nActual: %2$s</string>
|
||||||
<string name="error_no_available_url_provided_for_current_plugin">No available URL provided for current plugin</string>
|
<string name="error_no_available_url_provided_for_current_plugin">No available URL provided for current plugin</string>
|
||||||
|
<string name="text_failed_to_update">Failed to update</string>
|
||||||
|
<string name="text_confirm_to_uninstall">Are you sure to uninstall?</string>
|
||||||
|
<string name="dialog_button_exception_details">Details</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1116,5 +1116,8 @@
|
|||||||
<string name="text_sha256_mismatch_multiline_expected_actual">Discordancia de SHA-256.\n\nEsperado: %1$s\nReal: %2$s</string>
|
<string name="text_sha256_mismatch_multiline_expected_actual">Discordancia de SHA-256.\n\nEsperado: %1$s\nReal: %2$s</string>
|
||||||
<string name="text_integrity_verification_failed">La verificación de integridad falló</string>
|
<string name="text_integrity_verification_failed">La verificación de integridad falló</string>
|
||||||
<string name="error_no_available_url_provided_for_current_plugin">No se proporcionó ninguna URL disponible para el complemento actual</string>
|
<string name="error_no_available_url_provided_for_current_plugin">No se proporcionó ninguna URL disponible para el complemento actual</string>
|
||||||
|
<string name="text_failed_to_update">Error al actualizar</string>
|
||||||
|
<string name="text_confirm_to_uninstall">¿Seguro que deseas desinstalar?</string>
|
||||||
|
<string name="dialog_button_exception_details">Detalles</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1116,5 +1116,8 @@
|
|||||||
<string name="text_integrity_verification_failed">Échec de la vérification de l\'intégrité</string>
|
<string name="text_integrity_verification_failed">Échec de la vérification de l\'intégrité</string>
|
||||||
<string name="text_sha256_mismatch_multiline_expected_actual">Incohérence du SHA-256.\n\nAttendu : %1$s\nObtenu : %2$s</string>
|
<string name="text_sha256_mismatch_multiline_expected_actual">Incohérence du SHA-256.\n\nAttendu : %1$s\nObtenu : %2$s</string>
|
||||||
<string name="error_no_available_url_provided_for_current_plugin">Aucune URL disponible n\'a été fournie pour le plug-in actuel</string>
|
<string name="error_no_available_url_provided_for_current_plugin">Aucune URL disponible n\'a été fournie pour le plug-in actuel</string>
|
||||||
|
<string name="text_failed_to_update">Échec de la mise à jour</string>
|
||||||
|
<string name="text_confirm_to_uninstall">Voulez-vous vraiment désinstaller ?</string>
|
||||||
|
<string name="dialog_button_exception_details">Détails</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1117,5 +1117,8 @@
|
|||||||
<string name="text_integrity_verification_failed">整合性の検証に失敗しました</string>
|
<string name="text_integrity_verification_failed">整合性の検証に失敗しました</string>
|
||||||
<string name="text_sha256_mismatch_multiline_expected_actual">SHA-256 が一致しません.\n\n期待値: %1$s\n実際: %2$s</string>
|
<string name="text_sha256_mismatch_multiline_expected_actual">SHA-256 が一致しません.\n\n期待値: %1$s\n実際: %2$s</string>
|
||||||
<string name="error_no_available_url_provided_for_current_plugin">現在のプラグインに利用可能な URL は提供されていません</string>
|
<string name="error_no_available_url_provided_for_current_plugin">現在のプラグインに利用可能な URL は提供されていません</string>
|
||||||
|
<string name="text_failed_to_update">更新に失敗しました</string>
|
||||||
|
<string name="text_confirm_to_uninstall">アンインストールしてもよろしいですか?</string>
|
||||||
|
<string name="dialog_button_exception_details">詳細</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1118,5 +1118,8 @@
|
|||||||
<string name="text_integrity_verification_failed">무결성 검증에 실패했습니다</string>
|
<string name="text_integrity_verification_failed">무결성 검증에 실패했습니다</string>
|
||||||
<string name="text_sha256_mismatch_multiline_expected_actual">SHA-256 값이 일치하지 않습니다.\n\n예상: %1$s\n실제: %2$s</string>
|
<string name="text_sha256_mismatch_multiline_expected_actual">SHA-256 값이 일치하지 않습니다.\n\n예상: %1$s\n실제: %2$s</string>
|
||||||
<string name="error_no_available_url_provided_for_current_plugin">현재 플러그인에 사용 가능한 URL 이 제공되지 않았습니다</string>
|
<string name="error_no_available_url_provided_for_current_plugin">현재 플러그인에 사용 가능한 URL 이 제공되지 않았습니다</string>
|
||||||
|
<string name="text_failed_to_update">업데이트 실패</string>
|
||||||
|
<string name="text_confirm_to_uninstall">정말 제거하시겠습니까?</string>
|
||||||
|
<string name="dialog_button_exception_details">자세히</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1116,5 +1116,8 @@
|
|||||||
<string name="text_integrity_verification_failed">Сбой проверки целостности</string>
|
<string name="text_integrity_verification_failed">Сбой проверки целостности</string>
|
||||||
<string name="text_sha256_mismatch_multiline_expected_actual">Несовпадение SHA-256.\n\nОжидалось: %1$s\nФактически: %2$s</string>
|
<string name="text_sha256_mismatch_multiline_expected_actual">Несовпадение SHA-256.\n\nОжидалось: %1$s\nФактически: %2$s</string>
|
||||||
<string name="error_no_available_url_provided_for_current_plugin">Для текущего плагина не указан доступный URL-адрес</string>
|
<string name="error_no_available_url_provided_for_current_plugin">Для текущего плагина не указан доступный URL-адрес</string>
|
||||||
|
<string name="text_failed_to_update">Не удалось обновить</string>
|
||||||
|
<string name="text_confirm_to_uninstall">Вы действительно хотите удалить?</string>
|
||||||
|
<string name="dialog_button_exception_details">Детали</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1114,5 +1114,8 @@
|
|||||||
<string name="text_integrity_verification_failed">完整性驗證失敗</string>
|
<string name="text_integrity_verification_failed">完整性驗證失敗</string>
|
||||||
<string name="text_sha256_mismatch_multiline_expected_actual">SHA-256 驗證不一致.\n\n期望值: %1$s\n實際值: %2$s</string>
|
<string name="text_sha256_mismatch_multiline_expected_actual">SHA-256 驗證不一致.\n\n期望值: %1$s\n實際值: %2$s</string>
|
||||||
<string name="error_no_available_url_provided_for_current_plugin">當前插件未提供可用的 URL</string>
|
<string name="error_no_available_url_provided_for_current_plugin">當前插件未提供可用的 URL</string>
|
||||||
|
<string name="text_failed_to_update">更新失敗</string>
|
||||||
|
<string name="text_confirm_to_uninstall">是否確定卸載</string>
|
||||||
|
<string name="dialog_button_exception_details">異常詳情</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1114,5 +1114,8 @@
|
|||||||
<string name="text_integrity_verification_failed">完整性驗證失敗</string>
|
<string name="text_integrity_verification_failed">完整性驗證失敗</string>
|
||||||
<string name="text_sha256_mismatch_multiline_expected_actual">SHA-256 驗證不一致.\n\n期望值: %1$s\n實際值: %2$s</string>
|
<string name="text_sha256_mismatch_multiline_expected_actual">SHA-256 驗證不一致.\n\n期望值: %1$s\n實際值: %2$s</string>
|
||||||
<string name="error_no_available_url_provided_for_current_plugin">當前外掛未提供可用的 URL</string>
|
<string name="error_no_available_url_provided_for_current_plugin">當前外掛未提供可用的 URL</string>
|
||||||
|
<string name="text_failed_to_update">更新失敗</string>
|
||||||
|
<string name="text_confirm_to_uninstall">是否確定解除安裝</string>
|
||||||
|
<string name="dialog_button_exception_details">異常詳情</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1114,5 +1114,8 @@
|
|||||||
<string name="text_integrity_verification_failed">完整性验证失败</string>
|
<string name="text_integrity_verification_failed">完整性验证失败</string>
|
||||||
<string name="text_sha256_mismatch_multiline_expected_actual">SHA-256 验证不一致.\n\n期望值: %1$s\n实际值: %2$s</string>
|
<string name="text_sha256_mismatch_multiline_expected_actual">SHA-256 验证不一致.\n\n期望值: %1$s\n实际值: %2$s</string>
|
||||||
<string name="error_no_available_url_provided_for_current_plugin">当前插件未提供可用的 URL</string>
|
<string name="error_no_available_url_provided_for_current_plugin">当前插件未提供可用的 URL</string>
|
||||||
|
<string name="text_failed_to_update">更新失败</string>
|
||||||
|
<string name="text_confirm_to_uninstall">是否确定卸载</string>
|
||||||
|
<string name="dialog_button_exception_details">异常详情</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -50,8 +50,8 @@
|
|||||||
<string name="entry_app_language_zh_hant_hk" translatable="false">繁體中文 (香港)</string>
|
<string name="entry_app_language_zh_hant_hk" translatable="false">繁體中文 (香港)</string>
|
||||||
<string name="entry_app_language_zh_hant_tw" translatable="false">繁體中文 (台灣)</string>
|
<string name="entry_app_language_zh_hant_tw" translatable="false">繁體中文 (台灣)</string>
|
||||||
<string name="format_default_package_name" formatted="true" translatable="false">org.autojs.autojs6.%s</string>
|
<string name="format_default_package_name" formatted="true" translatable="false">org.autojs.autojs6.%s</string>
|
||||||
<string name="format_dialog_progress_number_format_kilo_bytes" translatable="false">%.1fKB/%.1fKB</string>
|
<string name="format_dialog_progress_number_format_kilo_bytes" translatable="false">%.1fKiB/%.1fKiB</string>
|
||||||
<string name="format_dialog_progress_number_format_mega_bytes" translatable="false">%.2fMB/%.2fMB</string>
|
<string name="format_dialog_progress_number_format_mega_bytes" translatable="false">%.2fMiB/%.2fMiB</string>
|
||||||
<string name="id_launcher_shortcut_docs" translatable="false">shortcut_$_docs</string>
|
<string name="id_launcher_shortcut_docs" translatable="false">shortcut_$_docs</string>
|
||||||
<string name="id_launcher_shortcut_log" translatable="false">shortcut_$_log</string>
|
<string name="id_launcher_shortcut_log" translatable="false">shortcut_$_log</string>
|
||||||
<string name="id_launcher_shortcut_plugin_center" translatable="false">shortcut_$_plugin_center</string>
|
<string name="id_launcher_shortcut_plugin_center" translatable="false">shortcut_$_plugin_center</string>
|
||||||
@@ -1369,5 +1369,8 @@
|
|||||||
<string name="text_integrity_verification_failed">Integrity verification failed</string>
|
<string name="text_integrity_verification_failed">Integrity verification failed</string>
|
||||||
<string name="text_sha256_mismatch_multiline_expected_actual">SHA-256 mismatch.\n\nExpected: %1$s\nActual: %2$s</string>
|
<string name="text_sha256_mismatch_multiline_expected_actual">SHA-256 mismatch.\n\nExpected: %1$s\nActual: %2$s</string>
|
||||||
<string name="error_no_available_url_provided_for_current_plugin">No available URL provided for current plugin</string>
|
<string name="error_no_available_url_provided_for_current_plugin">No available URL provided for current plugin</string>
|
||||||
|
<string name="text_failed_to_update">Failed to update</string>
|
||||||
|
<string name="text_confirm_to_uninstall">Are you sure to uninstall?</string>
|
||||||
|
<string name="dialog_button_exception_details">Details</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#Mon Dec 08 20:22:09 CST 2025
|
#Tue Dec 09 11:33:43 CST 2025
|
||||||
BUILD_TIME=1765196529144
|
BUILD_TIME=1765251223946
|
||||||
COMPILE_SDK_VERSION=36
|
COMPILE_SDK_VERSION=36
|
||||||
IMAGE_QUANT_CMAKE_VERSION=3.22.1
|
IMAGE_QUANT_CMAKE_VERSION=3.22.1
|
||||||
IMAGE_QUANT_NDK_VERSION=26.1.10909125
|
IMAGE_QUANT_NDK_VERSION=26.1.10909125
|
||||||
@@ -27,6 +27,6 @@ RAPID_OCR_OPENCV_MOBILE_LABEL_VERSION=13
|
|||||||
RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3
|
RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3
|
||||||
TARGET_SDK_VERSION=36
|
TARGET_SDK_VERSION=36
|
||||||
TARGET_SDK_VERSION_INRT=29
|
TARGET_SDK_VERSION_INRT=29
|
||||||
VERSION_BUILD=3528
|
VERSION_BUILD=3530
|
||||||
VERSION_NAME=6.7.0 Alpha12
|
VERSION_NAME=6.7.0 Alpha12
|
||||||
VSCODE_EXT_REQUIRED_VERSION=1.0.8
|
VSCODE_EXT_REQUIRED_VERSION=1.0.8
|
||||||
|
|||||||
Reference in New Issue
Block a user