6.7.0 - Alpha19 - 版本历史 M3 - 新增回收站功能; 版本历史与回收站增加入口

This commit is contained in:
SuperMonster003
2026-02-04 13:00:01 +08:00
parent 275f393f5b
commit 32533afbf3
32 changed files with 1546 additions and 183 deletions

View File

@@ -1,10 +1,11 @@
{
"$data": {
"v6.7.0": {
"released_date": "2026/02/03",
"released_date": "2026/02/04",
"feature": [
"插件中心功能, 支持插件的安装/卸载/更新等操作 (入口: 主页抽屉按钮/主页标签页)",
"版本历史功能, 支持查看/恢复可编辑文件的历史版本 (入口: 代码编辑器菜单)",
"版本历史功能, 支持查看/恢复可编辑文件的历史版本 (入口: 主页抽屉按钮/文件管理器菜单/代码编辑器菜单)",
"回收站功能, 支持查看/恢复已删除的文件/文件夹 (入口: 主页抽屉按钮)",
"Paddle OCR (PP-OCRv5) 插件, 用于光学字符识别",
"cvt 模块, 用于数据单位转换 (参阅 项目文档 > [单位转换](https://docs.autojs6.com/#/cvt))",
"fmt 模块, 用于数据格式化 (参阅 项目文档 > [格式化](https://docs.autojs6.com/#/fmt))",

View File

@@ -349,6 +349,14 @@
<activity android:name="org.autojs.autojs.ui.keystore.ManageKeyStoreActivity" />
<activity
android:name="org.autojs.autojs.ui.storage.TrashActivity"
android:theme="@style/AppTheme.Settings" />
<activity
android:name="org.autojs.autojs.ui.storage.VersionHistoryActivity"
android:theme="@style/AppTheme.Settings" />
<activity
android:name="org.autojs.autojs.ui.log.LogActivity"
android:exported="true"

View File

@@ -475,4 +475,8 @@ object DialogUtils {
progressBar.setProgressBackgroundTintList(ColorStateList.valueOf(bgColor))
}
@JvmStatic
fun MaterialDialog.setActionButtonText(actionButton: DialogAction, string: String) {
getActionButton(actionButton).text = string
}
}

View File

@@ -19,7 +19,9 @@ class HistoryCleanupWorker(
override suspend fun doWork(): Result {
return runCatching {
cleanupExpiredRevisions()
cleanupExpiredTrashItems()
cleanupOrphanHistoryBlobs()
cleanupOrphanTrashBlobs()
cleanupEmergencyDrafts()
}.fold(
onSuccess = { Result.success() },
@@ -79,6 +81,58 @@ class HistoryCleanupWorker(
}
}
private fun cleanupExpiredTrashItems() {
val db = HistoryDatabase.getInstance(applicationContext)
val dao = db.historyDao()
val trashBlobs = TrashBlobStore(applicationContext)
// Remove expired trash items by days (default 30 days).
// zh-CN: 按天数删除过期回收站条目 (默认 30 天).
val now = System.currentTimeMillis()
val expiredBefore = now - TRASH_MAX_DAYS_MS
val expired = dao.listExpiredTrashItems(expiredBefore)
if (expired.isEmpty()) return
dao.deleteTrashItemsByIds(expired.map { it.trashId })
expired.forEach { trashBlobs.deleteBlobByRelPath(it.blobRelPath) }
}
private fun cleanupOrphanTrashBlobs() {
val db = HistoryDatabase.getInstance(applicationContext)
val dao = db.historyDao()
// Build the referenced trash blob set from DB.
// zh-CN: 从 DB 构建被引用的回收站 blob 集合.
val referenced = dao.listAllTrashBlobRelPaths().toHashSet()
val root = File(applicationContext.filesDir, "trash/blob")
if (!root.exists() || !root.isDirectory) return
// Delete trash blobs that are not referenced by DB.
// zh-CN: 删除 DB 未引用的回收站 blob.
root.walkTopDown()
.filter { it.isFile }
.forEach { f ->
val rel = f.relativeTo(applicationContext.filesDir).invariantSeparatorsPath
if (!referenced.contains(rel)) {
// noinspection ResultOfMethodCallIgnored
f.delete()
}
}
// Best-effort: remove empty directories.
// zh-CN: 尽力删除空目录.
root.walkBottomUp()
.filter { it.isDirectory }
.forEach { dir ->
val children = dir.listFiles()
if (children == null || children.isEmpty()) {
// noinspection ResultOfMethodCallIgnored
dir.delete()
}
}
}
private fun cleanupEmergencyDrafts() {
// Keep consistent with EditorView's local cleanup policy.
// zh-CN: 与 EditorView 的本地清理策略保持一致.
@@ -118,6 +172,7 @@ class HistoryCleanupWorker(
companion object {
private const val MAX_DAYS_MS: Long = 30L * 24L * 60L * 60L * 1000L
private const val TRASH_MAX_DAYS_MS: Long = 30L * 24L * 60L * 60L * 1000L
private const val DRAFT_MAX_DAYS_MS: Long = 7L * 24L * 60L * 60L * 1000L
private const val DRAFT_MAX_TOTAL_BYTES: Long = 200L * 1024L * 1024L
}

View File

@@ -40,4 +40,19 @@ interface HistoryDao {
@Query("SELECT blobRelPath FROM revision")
fun listAllBlobRelPaths(): List<String>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun upsertTrashItem(item: TrashEntities.TrashItem)
@Query("SELECT * FROM trash_item ORDER BY trashedAt DESC")
fun listTrashItemsDesc(): List<TrashEntities.TrashItem>
@Query("SELECT * FROM trash_item WHERE trashedAt < :expiredBefore ORDER BY trashedAt ASC")
fun listExpiredTrashItems(expiredBefore: Long): List<TrashEntities.TrashItem>
@Query("DELETE FROM trash_item WHERE trashId IN (:trashIds)")
fun deleteTrashItemsByIds(trashIds: List<String>)
@Query("SELECT blobRelPath FROM trash_item")
fun listAllTrashBlobRelPaths(): List<String>
}

View File

@@ -4,6 +4,8 @@ import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
/**
* Created by JetBrains AI Assistant (GPT-5.2) on Feb 3, 2026.
@@ -12,8 +14,9 @@ import androidx.room.RoomDatabase
entities = [
HistoryEntities.FileEntry::class,
HistoryEntities.Revision::class,
TrashEntities.TrashItem::class,
],
version = 1,
version = 2,
exportSchema = false,
)
abstract class HistoryDatabase : RoomDatabase() {
@@ -25,12 +28,37 @@ abstract class HistoryDatabase : RoomDatabase() {
@Volatile
private var instance: HistoryDatabase? = null
// Migration from v1 to v2 (add trash_item).
// zh-CN: 从 v1 到 v2 的迁移 (新增 trash_item 表).
private val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"""
CREATE TABLE IF NOT EXISTS trash_item (
trashId TEXT NOT NULL PRIMARY KEY,
originalPath TEXT NOT NULL,
fileId TEXT,
trashedAt INTEGER NOT NULL,
isDirectory INTEGER NOT NULL,
sizeBytes INTEGER NOT NULL,
sha256 TEXT,
blobRelPath TEXT NOT NULL
)
""".trimIndent()
)
db.execSQL("CREATE INDEX IF NOT EXISTS index_trash_item_originalPath ON trash_item(originalPath)")
db.execSQL("CREATE INDEX IF NOT EXISTS index_trash_item_trashedAt ON trash_item(trashedAt)")
}
}
fun getInstance(applicationContext: Context): HistoryDatabase = instance ?: synchronized(this) {
instance ?: Room.databaseBuilder(
applicationContext,
HistoryDatabase::class.java,
"history-database.db",
).build().also { instance = it }
).apply {
addMigrations(MIGRATION_1_2)
}.build().also { instance = it }
}
}
}

View File

@@ -0,0 +1,55 @@
package org.autojs.autojs.storage.history
import android.content.Context
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileInputStream
import java.util.UUID
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
/**
* Created by JetBrains AI Assistant (GPT-5.2) on Feb 4, 2026.
*/
class TrashBlobStore(private val context: Context) {
fun newTrashId(): String = UUID.randomUUID().toString()
fun writeTrashBlob(trashId: String, bytes: ByteArray): String {
val rel = "trash/blob/$trashId.bin"
val f = File(context.filesDir, rel)
f.parentFile?.mkdirs()
f.outputStream().use { it.write(bytes) }
return rel
}
fun deleteBlobByRelPath(relPath: String) {
val f = File(context.filesDir, relPath)
if (f.exists()) {
// noinspection ResultOfMethodCallIgnored
f.delete()
}
}
/**
* Pack a directory into zip bytes.
* zh-CN: 将目录打包为 zip bytes.
*/
fun zipDirectoryToBytes(dir: File): ByteArray {
require(dir.isDirectory) { "Not a directory: $dir" }
val baos = ByteArrayOutputStream()
ZipOutputStream(baos).use { zos ->
val basePathLen = dir.absolutePath.trimEnd('/').length + 1
dir.walkTopDown().forEach { f ->
if (!f.isFile) return@forEach
val relPath = f.absolutePath.substring(basePathLen).replace(File.separatorChar, '/')
val entry = ZipEntry(relPath)
zos.putNextEntry(entry)
FileInputStream(f).use { it.copyTo(zos) }
zos.closeEntry()
}
}
return baos.toByteArray()
}
}

View File

@@ -0,0 +1,45 @@
package org.autojs.autojs.storage.history
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
/**
* Created by JetBrains AI Assistant (GPT-5.2) on Feb 4, 2026.
*/
object TrashEntities {
@Entity(
tableName = "trash_item",
indices = [
Index(value = ["originalPath"]),
Index(value = ["trashedAt"]),
],
)
data class TrashItem(
@PrimaryKey
val trashId: String,
// Original logical path under /storage/emulated/0.
// zh-CN: 原始 logical path, 位于 /storage/emulated/0 下.
val originalPath: String,
// Optional: link to history fileId if we can resolve it later.
// zh-CN: 可选: 未来可关联到 history 的 fileId.
val fileId: String?,
val trashedAt: Long,
// Whether the blob represents a directory archive.
// zh-CN: blob 是否为目录打包归档.
val isDirectory: Boolean,
val sizeBytes: Long,
val sha256: String?,
// Blob relative path under filesDir.
// zh-CN: 位于 filesDir 下的相对路径.
val blobRelPath: String,
)
}

View File

@@ -0,0 +1,135 @@
package org.autojs.autojs.storage.history
import android.content.Context
import java.io.File
import java.io.FileInputStream
import java.security.MessageDigest
import java.util.Locale
import java.util.zip.ZipInputStream
/**
* Created by JetBrains AI Assistant (GPT-5.2) on Feb 4, 2026.
*/
class TrashRepository(private val context: Context) {
private val db by lazy { HistoryDatabase.getInstance(context.applicationContext) }
private val dao by lazy { db.historyDao() }
private val blobs by lazy { TrashBlobStore(context.applicationContext) }
/**
* Move a file or directory into trash.
*
* Notes:
* - Only internal storage paths are supported.
* - Directory is archived as zip bytes.
*
* zh-CN:
*
* 将文件或目录移入回收站.
*
* 注意:
* - 仅支持内部存储路径.
* - 目录会被打包为 zip bytes.
*/
fun moveToTrash(path: String): TrashEntities.TrashItem {
val src = File(path)
require(src.exists()) { "Source not exists: $path" }
val logicalPath = path.trimEnd('/')
val trashId = blobs.newTrashId()
val now = System.currentTimeMillis()
val (bytes, isDir) = if (src.isDirectory) {
blobs.zipDirectoryToBytes(src) to true
} else {
src.inputStream().use { it.readBytes() } to false
}
val sha = sha256Hex(bytes)
val rel = blobs.writeTrashBlob(trashId, bytes)
// Delete original after blob persisted.
// zh-CN: 在 blob 落盘后删除源文件/目录.
val deleted = if (src.isDirectory) {
src.deleteRecursively()
} else {
src.delete()
}
if (!deleted && src.exists()) {
throw IllegalStateException("Failed to delete source after trash: $src")
}
val item = TrashEntities.TrashItem(
trashId = trashId,
originalPath = logicalPath,
fileId = null,
trashedAt = now,
isDirectory = isDir,
sizeBytes = bytes.size.toLong(),
sha256 = sha,
blobRelPath = rel,
)
dao.upsertTrashItem(item)
return item
}
/**
* Restore a trash item to a target path.
*
* Notes:
* - For directories, dest must be a directory path.
* - Restore will remove the trash item if succeeded.
*
* zh-CN:
*
* 将回收站条目恢复到目标路径.
*
* 说明:
* - 若为目录, dest 必须是目录路径.
* - 恢复成功后会删除回收站条目.
*/
fun restoreTrashItemToPath(item: TrashEntities.TrashItem, dest: File) {
val blobFile = File(context.filesDir, item.blobRelPath)
require(blobFile.exists()) { "Trash blob not exists: ${item.blobRelPath}" }
if (item.isDirectory) {
dest.mkdirs()
ZipInputStream(FileInputStream(blobFile)).use { zis ->
while (true) {
val entry = zis.nextEntry ?: break
val outFile = File(dest, entry.name)
if (entry.isDirectory) {
outFile.mkdirs()
zis.closeEntry()
continue
}
outFile.parentFile?.mkdirs()
outFile.outputStream().use { os -> zis.copyTo(os) }
zis.closeEntry()
}
}
} else {
dest.parentFile?.mkdirs()
blobFile.inputStream().use { input ->
dest.outputStream().use { output -> input.copyTo(output) }
}
}
// Remove trash record and blob after successful restore.
// zh-CN: 恢复成功后删除回收站记录与 blob.
dao.deleteTrashItemsByIds(listOf(item.trashId))
blobs.deleteBlobByRelPath(item.blobRelPath)
}
private fun sha256Hex(bytes: ByteArray): String {
val digest = MessageDigest.getInstance("SHA-256").digest(bytes)
val sb = StringBuilder(digest.size * 2)
for (b in digest) {
sb.append(String.format(Locale.US, "%02x", b))
}
return sb.toString()
}
}

View File

@@ -0,0 +1,189 @@
package org.autojs.autojs.storage.history
import android.annotation.SuppressLint
import android.content.Context
import com.afollestad.materialdialogs.DialogAction
import com.afollestad.materialdialogs.MaterialDialog
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import org.autojs.autojs.app.DialogUtils
import org.autojs.autojs.app.DialogUtils.setActionButtonText
import org.autojs.autojs.ui.filechooser.FileChooserDialogBuilder
import org.autojs.autojs.util.MaterialDialogUtils.choiceWidgetThemeColor
import org.autojs.autojs.util.ViewUtils
import org.autojs.autojs6.R
import java.io.File
import java.util.regex.Pattern
/**
* Restore flow controller for trash items.
* zh-CN: 回收站条目的恢复流程控制器.
*
* Created by JetBrains AI Assistant (GPT-5.2) on Feb 4, 2026.
*/
class TrashRestoreController(private val context: Context) {
fun startRestoreFlow(item: TrashEntities.TrashItem) {
val originalPath = item.originalPath
val originalName = File(originalPath).name
val selected = intArrayOf(0)
DialogUtils.buildAndShowAdaptive {
MaterialDialog.Builder(context)
.title(R.string.text_choose_restore_path)
.content(context.getString(R.string.text_path_colon_value, originalPath))
.items(
context.getString(R.string.text_restore_to_original_path),
context.getString(R.string.text_restore_to_specified_path),
)
.itemsCallbackSingleChoice(0) { dialog, _, which, _ ->
selected[0] = which
// Update content and positive button text dynamically.
// zh-CN: 动态更新内容与确认按钮文本.
when (which) {
0 -> {
dialog.setContent(context.getString(R.string.text_path_colon_value, originalPath))
dialog.setActionButtonText(DialogAction.POSITIVE, context.getString(R.string.dialog_button_confirm))
}
else -> {
dialog.setContent(context.getString(R.string.text_file_name_colon_value, originalName))
dialog.setActionButtonText(DialogAction.POSITIVE, context.getString(R.string.dialog_button_choose_path))
}
}
true
}
.choiceWidgetThemeColor()
.negativeText(R.string.dialog_button_cancel)
.negativeColorRes(R.color.dialog_button_default)
.positiveText(R.string.dialog_button_confirm)
.positiveColorRes(R.color.dialog_button_attraction)
.onPositive { _, _ ->
when (selected[0]) {
0 -> restoreToOriginalPathWithConflictHandling(item)
else -> chooseDirectoryThenRestore(item, originalName)
}
}
.cancelable(true)
.build()
}
}
private fun restoreToOriginalPathWithConflictHandling(item: TrashEntities.TrashItem) {
val dest = File(item.originalPath)
val parent = dest.parentFile ?: run {
ViewUtils.showToast(context, context.getString(R.string.error_invalid_path, item.originalPath), true)
return
}
restoreWithConflictHandling(item, parent, dest.name)
}
@SuppressLint("CheckResult")
private fun chooseDirectoryThenRestore(item: TrashEntities.TrashItem, fileName: String) {
FileChooserDialogBuilder(context)
.title(R.string.dialog_button_choose_path)
.dir(INTERNAL_STORAGE_ROOT)
.chooseDir()
.singleChoice()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ dir ->
restoreWithConflictHandling(item, File(dir.path), fileName)
}, { e ->
e.printStackTrace()
ViewUtils.showToast(context, e.message, true)
})
}
private fun restoreWithConflictHandling(item: TrashEntities.TrashItem, destDir: File, fileName: String) {
val dest = File(destDir, fileName)
if (!dest.exists()) {
restoreNow(item, dest)
return
}
// Ask overwrite or auto-suffix when name conflict.
// zh-CN: 同名冲突时询问覆盖或自动后缀.
DialogUtils.buildAndShowAdaptive {
MaterialDialog.Builder(context)
.title(R.string.text_prompt)
.content(context.getString(R.string.text_path_colon_value, dest.absolutePath))
.negativeText(R.string.dialog_button_cancel)
.negativeColorRes(R.color.dialog_button_default)
.neutralText(R.string.text_auto_rename)
.neutralColorRes(R.color.dialog_button_hint)
.onNeutral { _, _ ->
val newName = generateNextIndexedName(destDir, fileName)
restoreNow(item, File(destDir, newName))
}
.positiveText(R.string.text_overwrite)
.positiveColorRes(R.color.dialog_button_caution)
.onPositive { _, _ ->
restoreNow(item, dest)
}
.cancelable(true)
.build()
}
}
private fun restoreNow(item: TrashEntities.TrashItem, dest: File) {
Schedulers.io().scheduleDirect {
runCatching {
TrashRepository(context.applicationContext).restoreTrashItemToPath(item, dest)
AndroidSchedulers.mainThread().scheduleDirect {
ViewUtils.showToast(context, context.getString(R.string.text_done), true)
}
}.onFailure {
it.printStackTrace()
AndroidSchedulers.mainThread().scheduleDirect {
ViewUtils.showToast(context, it.message, true)
}
}
}
}
/**
* Generate next indexed name by scanning existing "-n" siblings and taking the first missing n from 1.
* zh-CN: 通过扫描同级目录中已有的 "-n" 名称, 从 1 开始取第一个缺失的 n 作为新后缀.
*/
private fun generateNextIndexedName(parentDir: File, originalName: String): String {
val isDirectory = !originalName.contains('.')
val base: String
val extWithDot: String
if (isDirectory) {
base = originalName
extWithDot = ""
} else {
val dot = originalName.lastIndexOf('.')
base = if (dot > 0) originalName.substring(0, dot) else originalName
extWithDot = if (dot > 0) originalName.substring(dot) else ""
}
val files = parentDir.listFiles()?.map { it.name } ?: emptyList()
val existingIndexes = HashSet<Int>()
val pattern = if (isDirectory) {
Regex("^" + Pattern.quote(base) + "-(\\d+)$")
} else {
Regex("^" + Pattern.quote(base) + "-(\\d+)" + Pattern.quote(extWithDot) + "$")
}
for (name in files) {
val m = pattern.matchEntire(name) ?: continue
val idx = m.groups[1]?.value?.toIntOrNull() ?: continue
existingIndexes.add(idx)
}
var n = 1
while (existingIndexes.contains(n)) n++
return if (isDirectory) "$base-$n" else "$base-$n$extWithDot"
}
companion object {
private const val INTERNAL_STORAGE_ROOT: String = "/storage/emulated/0"
}
}

View File

@@ -0,0 +1,295 @@
package org.autojs.autojs.storage.history
import android.content.Context
import android.net.Uri
import com.afollestad.materialdialogs.MaterialDialog
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import org.autojs.autojs.app.DialogUtils
import org.autojs.autojs.pio.PFiles
import org.autojs.autojs.ui.common.NotAskAgainDialog
import org.autojs.autojs.util.MaterialDialogUtils.choiceWidgetThemeColor
import org.autojs.autojs.util.MaterialDialogUtils.widgetThemeColor
import org.autojs.autojs.util.StringUtils
import org.autojs.autojs.util.StringUtils.key
import org.autojs.autojs.util.ViewUtils
import org.autojs.autojs6.R
import java.io.File
import java.io.IOException
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/**
* Version history dialog controller.
* zh-CN: 版本历史对话框控制器.
*
* Created by JetBrains AI Assistant (GPT-5.2) on Feb 4, 2026.
*/
class VersionHistoryController(private val context: Context) {
/**
* Show version history for editor (restore into editor only, not auto-save).
* zh-CN: 为编辑器显示版本历史 (仅恢复到编辑器, 不自动保存).
*/
fun showForEditor(
uri: Uri,
onRestoreToEditor: (restoredText: String) -> Unit,
onRestoredUi: () -> Unit,
) {
val logicalPath = HistoryUriUtils.toLogicalPathOrNull(uri)
if (logicalPath == null) {
showNoHistoryDialog()
return
}
loadAndShow(
logicalPath = logicalPath,
mode = Mode.RESTORE_TO_EDITOR,
onRestoreToEditor = onRestoreToEditor,
onRestoredUi = onRestoredUi,
)
}
/**
* Show version history for explorer (restore to disk, overwriting the file content).
* zh-CN: 为资源管理器显示版本历史 (恢复并写回磁盘, 覆盖文件内容).
*/
fun showForFilePath(path: String) {
val normalized = path.trimEnd('/')
if (!normalized.startsWith(INTERNAL_STORAGE_ROOT)) {
showNoHistoryDialog()
return
}
loadAndShow(
logicalPath = normalized,
mode = Mode.RESTORE_TO_DISK,
onRestoreToEditor = null,
onRestoredUi = null,
)
}
private fun showNoHistoryDialog() {
DialogUtils.buildAndShowAdaptive {
MaterialDialog.Builder(context)
.title(R.string.text_version_history)
.content(R.string.text_no_version_history)
.positiveText(R.string.dialog_button_dismiss)
.positiveColorRes(R.color.dialog_button_default)
.cancelable(true)
.build()
}
}
private fun loadAndShow(
logicalPath: String,
mode: Mode,
onRestoreToEditor: ((String) -> Unit)?,
onRestoredUi: (() -> Unit)?,
) {
Schedulers.io().scheduleDirect {
runCatching {
val appCtx = context.applicationContext
val dao = HistoryDatabase.getInstance(appCtx).historyDao()
val fileEntry = dao.findFileByPath(logicalPath)
val fileId = fileEntry?.fileId
val revs = if (fileId != null) {
// Show latest first for selection.
// zh-CN: 列表按最新优先展示供选择.
dao.listRevisionsAsc(fileId).asReversed().take(HISTORY_DIALOG_MAX_ITEMS)
} else {
emptyList()
}
AndroidSchedulers.mainThread().scheduleDirect {
if (revs.isEmpty()) {
showNoHistoryDialog()
return@scheduleDirect
}
val fmt = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
val items = revs.map { rev ->
val t = fmt.format(Date(rev.createdAt))
val size = rev.sizeBytes
"$t | ${PFiles.formatSizeWithUnit(size)}"
}
val selectedIndex = intArrayOf(0)
DialogUtils.buildAndShowAdaptive {
MaterialDialog.Builder(context)
.title(R.string.text_version_history)
.items(items)
.itemsCallbackSingleChoice(0) { _, _, which, _ ->
selectedIndex[0] = which
true
}
.choiceWidgetThemeColor()
.negativeText(R.string.dialog_button_cancel)
.negativeColorRes(R.color.dialog_button_default)
.positiveText(R.string.dialog_button_retrieve)
.positiveColorRes(R.color.dialog_button_attraction)
.onPositive { _, _ ->
val chosen = revs.getOrNull(selectedIndex[0]) ?: return@onPositive
when (mode) {
Mode.RESTORE_TO_EDITOR -> {
showRestoreConfirmDialogForEditor {
restoreToEditor(chosen, onRestoreToEditor, onRestoredUi)
}
}
Mode.RESTORE_TO_DISK -> {
showRestoreConfirmDialogForDisk(logicalPath) {
restoreToDisk(chosen, logicalPath)
}
}
}
}
.cancelable(true)
.build()
}
}
}.onFailure {
it.printStackTrace()
AndroidSchedulers.mainThread().scheduleDirect {
ViewUtils.showToast(context, it.message, true)
}
}
}
}
private fun restoreToEditor(
rev: HistoryEntities.Revision,
onRestoreToEditor: ((String) -> Unit)?,
onRestoredUi: (() -> Unit)?,
) {
val appCtx = context.applicationContext
Schedulers.io().scheduleDirect {
runCatching {
val bytes = HistoryRepository(appCtx).readRevisionBytes(rev)
val restored = decodeRevisionBytes(bytes, rev.encoding, rev.hadBom)
AndroidSchedulers.mainThread().scheduleDirect {
onRestoreToEditor?.invoke(restored)
onRestoredUi?.invoke()
}
}.onFailure {
it.printStackTrace()
AndroidSchedulers.mainThread().scheduleDirect {
ViewUtils.showToast(context, it.message, true)
}
}
}
}
private fun restoreToDisk(rev: HistoryEntities.Revision, targetPath: String) {
val appCtx = context.applicationContext
Schedulers.io().scheduleDirect {
runCatching {
val bytes = HistoryRepository(appCtx).readRevisionBytes(rev)
writeBytesTransactional(File(targetPath), bytes)
AndroidSchedulers.mainThread().scheduleDirect {
ViewUtils.showToast(context, context.getString(R.string.text_done), true)
}
}.onFailure {
it.printStackTrace()
AndroidSchedulers.mainThread().scheduleDirect {
ViewUtils.showToast(context, it.message, true)
}
}
}
}
private fun writeBytesTransactional(dest: File, bytes: ByteArray) {
dest.parentFile?.mkdirs()
val tmp = File(dest.parentFile, dest.name + ".restore.tmp")
tmp.outputStream().use { it.write(bytes) }
val newHash = sha256(bytes)
val readBackHash = sha256(tmp.inputStream().use { it.readBytes() })
if (!readBackHash.contentEquals(newHash)) {
// Delete temp file when verification failed.
// zh-CN: 校验失败时删除临时文件.
// noinspection ResultOfMethodCallIgnored
tmp.delete()
throw IOException("Write verification failed (hash mismatch): ${dest.absolutePath}")
}
if (dest.exists() && !dest.delete()) {
// noinspection ResultOfMethodCallIgnored
tmp.delete()
throw IOException("Failed to replace destination: ${dest.absolutePath}")
}
if (!tmp.renameTo(dest)) {
// noinspection ResultOfMethodCallIgnored
tmp.delete()
throw IOException("Failed to commit restored file: ${dest.absolutePath}")
}
}
private fun showRestoreConfirmDialogForEditor(onConfirm: () -> Unit) {
// Use "Not ask again" for editor restoring.
// zh-CN: 编辑器恢复使用 "不再提示" 的确认方式.
DialogUtils.buildAndShowAdaptiveOrNull {
NotAskAgainDialog.Builder(
context,
key(R.string.key_version_history_restore_does_not_auto_save_to_disk),
).apply {
title(R.string.text_prompt)
content(R.string.text_version_history_restore_does_not_auto_save_to_disk)
widgetThemeColor()
negativeText(R.string.dialog_button_cancel)
negativeColorRes(R.color.dialog_button_default)
positiveText(R.string.dialog_button_confirm)
positiveColorRes(R.color.dialog_button_attraction)
onPositive { _, _ -> onConfirm() }
cancelable(false)
}.build()
} ?: onConfirm()
}
private fun showRestoreConfirmDialogForDisk(targetPath: String, onConfirm: () -> Unit) {
// Restore to disk will overwrite file content.
// zh-CN: 写回磁盘的恢复会覆盖文件内容.
DialogUtils.buildAndShowAdaptive {
MaterialDialog.Builder(context)
.title(R.string.text_prompt)
.content(context.getString(R.string.text_version_history_restore_will_overwrite_file, targetPath))
.negativeText(R.string.dialog_button_cancel)
.negativeColorRes(R.color.dialog_button_default)
.positiveText(R.string.dialog_button_confirm)
.positiveColorRes(R.color.dialog_button_caution)
.onPositive { _, _ -> onConfirm() }
.cancelable(true)
.build()
}
}
private fun decodeRevisionBytes(bytes: ByteArray, encodingName: String, hadBom: Boolean): String {
val charset = runCatching { Charset.forName(encodingName) }.getOrElse { DEFAULT_CHARSET }
val effective = if (hadBom) {
// Drop BOM before decoding because BOM presence is tracked by metadata.
// zh-CN: BOM 是否存在由元数据记录, 解码前需丢弃 BOM.
StringUtils.dropBom(bytes, charset)
} else bytes
return String(effective, charset)
}
private fun sha256(bytes: ByteArray): ByteArray {
return MessageDigest.getInstance("SHA-256").digest(bytes)
}
private enum class Mode {
RESTORE_TO_EDITOR,
RESTORE_TO_DISK,
}
companion object {
private val DEFAULT_CHARSET: Charset = StandardCharsets.UTF_8
private const val HISTORY_DIALOG_MAX_ITEMS: Int = 20
private const val INTERNAL_STORAGE_ROOT: String = "/storage/emulated/0"
}
}

View File

@@ -40,10 +40,12 @@ import org.autojs.autojs.pio.PFiles;
import org.autojs.autojs.pio.UncheckedIOException;
import org.autojs.autojs.project.ProjectConfig;
import org.autojs.autojs.storage.file.TmpScriptFiles;
import org.autojs.autojs.storage.history.TrashRepository;
import org.autojs.autojs.ui.filechooser.FileChooserDialogBuilder;
import org.autojs.autojs.ui.shortcut.ShortcutCreateActivity;
import org.autojs.autojs.ui.timing.TimedTaskSettingActivity;
import org.autojs.autojs.util.EnvironmentUtils;
import org.autojs.autojs.util.FileUtils;
import org.autojs.autojs.util.IntentUtils;
import org.autojs.autojs.util.MaterialDialogUtils;
import org.autojs.autojs.util.ShortcutUtils;
@@ -67,7 +69,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.autojs.autojs.util.FileUtils;
import static org.autojs.autojs.app.DialogUtils.fixCheckBoxGravity;
import static org.autojs.autojs.model.explorer.ExplorerFileItem.isInSampleDir;
@@ -957,14 +958,31 @@ public class ScriptOperations {
}
public void delete(final ScriptFile scriptFile) {
DialogUtils.showAdaptive(new MaterialDialog.Builder(mContext)
.title(mContext.getString(R.string.text_confirm_to_delete))
.content(scriptFile.getName())
.negativeText(R.string.text_cancel)
.positiveText(R.string.dialog_button_confirm)
.positiveColorRes(R.color.dialog_button_caution)
.onPositive((dialog, which) -> deleteWithoutConfirm(scriptFile))
.build());
DialogUtils.buildAndShowAdaptive(() -> {
MaterialDialog.Builder builder = new MaterialDialog.Builder(mContext)
.title(R.string.text_choose_delete_strategy)
.content(scriptFile.getName())
.items(
mContext.getString(R.string.item_move_to_trash),
mContext.getString(R.string.item_delete_permanently)
)
.itemsCallbackSingleChoice(0, (dialog, itemView, which, text) -> {
dialog.dismiss();
if (which == 0) {
moveToTrashWithProgress(scriptFile);
} else if (which == 1) {
deleteWithoutConfirm(scriptFile);
}
return true;
})
.negativeText(R.string.dialog_button_cancel)
.positiveText(R.string.dialog_button_confirm)
.positiveColorRes(R.color.dialog_button_caution);
MaterialDialogUtils.choiceWidgetThemeColor(builder);
return builder.build();
});
}
public void setAsWorkingDir(final ScriptFile scriptFile) {
@@ -987,6 +1005,68 @@ public class ScriptOperations {
.build());
}
private void moveToTrashWithProgress(final ScriptFile scriptFile) {
boolean isDir = scriptFile.isDirectory();
int titleRes = isDir ? R.string.text_delete_folder : R.string.text_delete_file;
Observable.fromCallable(() -> {
OperationController controller = new OperationController();
ProgressDialogSession session = new ProgressDialogSession(controller);
try {
// Indeterminate progress is acceptable for trashing.
// zh-CN: 移入回收站使用不确定进度条即可.
MaterialDialog.Builder builder = new MaterialDialog.Builder(mContext)
.title(titleRes)
.content(R.string.ellipsis_six)
.negativeText(R.string.dialog_button_abort)
.negativeColorRes(R.color.dialog_button_caution)
.onNegative((dialog, which) -> {
controller.cancel();
try {
dialog.setContent(mContext.getString(R.string.text_aborting));
} catch (Throwable ignored) {
/* Ignored. */
}
})
.progress(true, 0)
.progressIndeterminateStyle(true)
.cancelable(false)
.canceledOnTouchOutside(false);
session.scheduleShow(builder);
controller.throwIfCancelled();
// Move into trash (blob + db) then notify explorer.
// zh-CN: 移入回收站 (blob + db), 然后通知资源管理器.
new TrashRepository(mContext.getApplicationContext())
.moveToTrash(scriptFile.getPath());
return 1;
} catch (OperationAbortedException aborted) {
return -1;
} finally {
session.dismissSafely();
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
switch (result) {
case 1 -> {
showMessage(R.string.text_moved_to_trash);
notifyFileRemoved(ProjectConfig.isProject(scriptFile), scriptFile.isDirectory(), scriptFile);
}
case -1 -> showMessage(R.string.text_operation_aborted);
default -> showMessage(R.string.text_failed_to_delete);
}
}, e -> {
e.printStackTrace();
showMessage(R.string.text_failed_to_delete);
});
}
public void deleteWithoutConfirm(final ScriptFile scriptFile) {
boolean isDir = scriptFile.isDirectory();
boolean isProject = ProjectConfig.isProject(scriptFile);

View File

@@ -44,15 +44,13 @@ import org.autojs.autojs.model.script.Scripts.EXTRA_EXCEPTION_LINE_NUMBER
import org.autojs.autojs.model.script.Scripts.EXTRA_EXCEPTION_MESSAGE
import org.autojs.autojs.model.script.Scripts.openByOtherApps
import org.autojs.autojs.model.script.Scripts.runWithBroadcastSender
import org.autojs.autojs.pio.PFiles
import org.autojs.autojs.pio.PFiles.getNameWithoutExtension
import org.autojs.autojs.pio.PFiles.write
import org.autojs.autojs.storage.file.TmpScriptFiles
import org.autojs.autojs.storage.history.HistoryDatabase
import org.autojs.autojs.storage.history.HistoryRepository
import org.autojs.autojs.storage.history.HistoryUriUtils
import org.autojs.autojs.storage.history.VersionHistoryController
import org.autojs.autojs.tool.Callback
import org.autojs.autojs.ui.common.NotAskAgainDialog
import org.autojs.autojs.ui.doc.ManualDialog
import org.autojs.autojs.ui.edit.completion.CodeCompletionBar
import org.autojs.autojs.ui.edit.completion.CodeCompletionBar.OnHintClickListener
@@ -76,10 +74,8 @@ import org.autojs.autojs.util.ClipboardUtils
import org.autojs.autojs.util.DisplayUtils.pxToSp
import org.autojs.autojs.util.DocsUtils.getUrl
import org.autojs.autojs.util.MaterialDialogUtils.choiceWidgetThemeColor
import org.autojs.autojs.util.MaterialDialogUtils.widgetThemeColor
import org.autojs.autojs.util.Observers
import org.autojs.autojs.util.StringUtils
import org.autojs.autojs.util.StringUtils.key
import org.autojs.autojs.util.ViewUtils.showSnack
import org.autojs.autojs.util.ViewUtils.showToast
import org.autojs.autojs6.R
@@ -89,8 +85,6 @@ import java.io.IOException
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/**
@@ -717,140 +711,18 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
}
fun showVersionHistoryDialog() {
val logicalPath = HistoryUriUtils.toLogicalPathOrNull(uri)
if (logicalPath == null) {
// History is only tracked under internal storage root.
// zh-CN: 历史记录仅纳管内部存储根目录下的文件.
DialogUtils.buildAndShowAdaptive {
MaterialDialog.Builder(context)
.title(R.string.text_version_history)
.content(R.string.text_no_version_history)
.positiveText(R.string.dialog_button_dismiss)
.positiveColorRes(R.color.dialog_button_default)
.cancelable(false)
.build()
}
return
}
Schedulers.io().scheduleDirect {
runCatching {
val appCtx = context.applicationContext
val db = HistoryDatabase.getInstance(appCtx)
val dao = db.historyDao()
val fileEntry = dao.findFileByPath(logicalPath)
val fileId = fileEntry?.fileId
val revs = if (fileId != null) {
// Show latest first for selection.
// zh-CN: 列表按最新优先展示供选择.
dao.listRevisionsAsc(fileId).asReversed().take(HISTORY_DIALOG_MAX_ITEMS)
} else {
emptyList()
}
post {
if (revs.isEmpty()) {
DialogUtils.buildAndShowAdaptive {
MaterialDialog.Builder(context)
.title(R.string.text_version_history)
.content(R.string.text_no_version_history)
.positiveText(R.string.dialog_button_dismiss)
.positiveColorRes(R.color.dialog_button_default)
.cancelable(false)
.build()
}
return@post
}
val fmt = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
val items = revs.map { rev ->
val t = fmt.format(Date(rev.createdAt))
val size = rev.sizeBytes
"$t | ${PFiles.formatSizeWithUnit(size)}"
}
val selectedIndex = intArrayOf(0)
DialogUtils.buildAndShowAdaptive {
MaterialDialog.Builder(context)
.title(R.string.dialog_button_history)
.items(items)
.itemsCallbackSingleChoice(0) { _, _, which, _ ->
selectedIndex[0] = which
true
}
.negativeText(R.string.dialog_button_cancel)
.negativeColorRes(R.color.dialog_button_default)
.positiveText(R.string.dialog_button_retrieve)
.positiveColorRes(R.color.dialog_button_attraction)
.onPositive { _, _ ->
val chosen = revs.getOrNull(selectedIndex[0]) ?: return@onPositive
// Confirm before restoring to editor.
// zh-CN: 恢复到编辑器前先做二次确认.
showRestoreConfirmDialog() {
// Load blob and restore into editor (not auto-save).
// zh-CN: 读取 blob 并恢复到编辑器 (不自动保存).
Schedulers.io().scheduleDirect {
runCatching {
val bytes = HistoryRepository(appCtx).readRevisionBytes(chosen)
val restored = decodeRevisionBytes(
bytes = bytes,
encodingName = chosen.encoding,
hadBom = chosen.hadBom,
)
post {
editor.text = restored
setMenuItemStatus(R.id.save, true)
showSnack(this@EditorView, R.string.text_done)
}
}.onFailure {
it.printStackTrace()
post { showToast(context, it.message, true) }
}
}
}
}
.cancelable(true)
.build()
}
}
}.onFailure {
it.printStackTrace()
post { showToast(context, it.message, true) }
}
}
}
private fun showRestoreConfirmDialog(onConfirm: () -> Unit) {
DialogUtils.buildAndShowAdaptiveOrNull {
NotAskAgainDialog.Builder(
context,
key(R.string.key_version_history_restore_does_not_auto_save_to_disk)
).apply {
title(R.string.text_prompt)
content(R.string.text_version_history_restore_does_not_auto_save_to_disk)
widgetThemeColor()
negativeText(R.string.dialog_button_cancel)
negativeColorRes(R.color.dialog_button_default)
positiveText(R.string.dialog_button_confirm)
positiveColorRes(R.color.dialog_button_attraction)
onPositive { _, _ -> onConfirm() }
cancelable(false)
}.build()
} ?: onConfirm()
}
private fun decodeRevisionBytes(bytes: ByteArray, encodingName: String, hadBom: Boolean): String {
val charset = runCatching { Charset.forName(encodingName) }.getOrElse { DEFAULT_CHARSET_TO_WRITE_FILE }
val effective = if (hadBom) {
// Drop BOM before decoding because BOM presence is tracked by metadata.
// zh-CN: BOM 是否存在由元数据记录, 解码前需丢弃 BOM.
StringUtils.dropBom(bytes, charset)
} else bytes
return String(effective, charset)
// Delegate to controller to unify editor/explorer/history page behavior.
// zh-CN: 委托给 Controller, 统一 editor/explorer/history page 的行为.
VersionHistoryController(context).showForEditor(
uri = uri,
onRestoreToEditor = { restoredText ->
editor.text = restoredText
},
onRestoredUi = {
setMenuItemStatus(R.id.save, true)
showSnack(this@EditorView, R.string.text_done)
},
)
}
// A minimal emergency draft store.
@@ -1211,10 +1083,6 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
// zh-CN: 草稿总容量上限 (200MB).
private const val DRAFT_MAX_TOTAL_BYTES: Long = 200L * 1024L * 1024L
// Max items shown in history dialog.
// zh-CN: 历史对话框最多展示条数.
private const val HISTORY_DIALOG_MAX_ITEMS: Int = 20
// Internal storage root (no external SD).
// zh-CN: 内部存储根目录 (不访问外置 SD).
private const val INTERNAL_STORAGE_ROOT: String = "/storage/emulated/0"

View File

@@ -45,6 +45,7 @@ import org.autojs.autojs.model.script.Scripts
import org.autojs.autojs.pio.PFile
import org.autojs.autojs.pio.PFiles
import org.autojs.autojs.project.ProjectConfig
import org.autojs.autojs.storage.history.VersionHistoryController
import org.autojs.autojs.theme.ThemeColorHelper
import org.autojs.autojs.theme.ThemeColorManagerCompat
import org.autojs.autojs.theme.widget.ThemeColorSwipeRefreshLayout
@@ -284,6 +285,16 @@ open class ExplorerView : ThemeColorSwipeRefreshLayout, SwipeRefreshLayout.OnRef
ScriptOperations(context, this@ExplorerView, currentPage)
.delete(selectedItem!!.toScriptFile())
}
R.id.action_version_history -> {
val selected = selectedItem ?: return false
// Show version history for this file and restore to disk when chosen.
// zh-CN: 显示该文件的版本历史, 选择后写回磁盘恢复.
VersionHistoryController(context).showForFilePath(selected.path)
notifyItemOperated()
mRequestHostDialogHide?.run()
}
R.id.action_run_repeatedly -> {
ScriptLoopDialog(context, selectedItem!!.toScriptFile())
.show()

View File

@@ -48,6 +48,8 @@ import org.autojs.autojs.ui.fragment.BindingDelegates.viewBinding
import org.autojs.autojs.ui.main.MainActivity
import org.autojs.autojs.ui.settings.AboutActivity
import org.autojs.autojs.ui.settings.PreferencesActivity
import org.autojs.autojs.ui.storage.TrashActivity
import org.autojs.autojs.ui.storage.VersionHistoryActivity
import org.autojs.autojs.util.DisplayUtils
import org.autojs.autojs.util.IntentUtils.App.exit
import org.autojs.autojs.util.IntentUtils.App.restart
@@ -120,6 +122,8 @@ open class DrawerFragment : Fragment() {
private lateinit var mAutoNightModeItem: DrawerMenuToggleableItem
private lateinit var mKeepScreenOnWhenInForegroundItem: DrawerMenuToggleableItem
private lateinit var mThemeColorItem: DrawerMenuShortcutItem
private lateinit var mTrashItem: DrawerMenuShortcutItem
private lateinit var mVersionHistoryItem: DrawerMenuShortcutItem
private lateinit var mAboutAppAndDevItem: DrawerMenuShortcutItem
private lateinit var mA11yTool: AccessibilityTool
@@ -586,13 +590,35 @@ open class DrawerFragment : Fragment() {
descriptionRes = R.string.description_keep_screen_on_when_in_foreground,
)
mThemeColorItem = DrawerMenuShortcutItem(R.drawable.ic_personalize_thicker, R.string.text_theme_color)
.setAction(Runnable { ColorSelectBaseActivity.startActivity(mContext) })
.apply { subtitle = ColorSelectBaseActivity.getCurrentColorSummary(mContext) }
mThemeColorItem = DrawerMenuShortcutItem(
icon = R.drawable.ic_personalize_thicker,
title = R.string.text_theme_color,
).apply {
setAction { ColorSelectBaseActivity.startActivity(mContext) }
subtitle = ColorSelectBaseActivity.getCurrentColorSummary(mContext)
}
mAboutAppAndDevItem = DrawerMenuShortcutItem(R.drawable.ic_about, R.string.text_about_app_and_developer)
.setAction(Runnable { AboutActivity.startActivity(mContext) })
.apply { subtitle = BuildConfig.VERSION_NAME }
mTrashItem = DrawerMenuShortcutItem(
icon = R.drawable.ic_recycle_bin,
title = R.string.text_trash,
).apply {
setAction { TrashActivity.startActivity(mContext) }
}
mVersionHistoryItem = DrawerMenuShortcutItem(
icon = R.drawable.ic_version_history,
title = R.string.text_version_history,
).apply {
setAction { VersionHistoryActivity.startActivity(mContext) }
}
mAboutAppAndDevItem = DrawerMenuShortcutItem(
icon = R.drawable.ic_about,
title = R.string.text_about_app_and_developer,
).apply {
setAction { AboutActivity.startActivity(mContext) }
subtitle = BuildConfig.VERSION_NAME
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@@ -715,6 +741,9 @@ open class DrawerFragment : Fragment() {
mNightModeItem,
mKeepScreenOnWhenInForegroundItem,
mThemeColorItem,
DrawerMenuGroup(R.string.text_file),
mTrashItem,
mVersionHistoryItem,
DrawerMenuGroup(R.string.text_about),
mAboutAppAndDevItem,
)

View File

@@ -0,0 +1,184 @@
package org.autojs.autojs.ui.storage
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.afollestad.materialdialogs.MaterialDialog
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import org.autojs.autojs.app.DialogUtils
import org.autojs.autojs.storage.history.HistoryDatabase
import org.autojs.autojs.storage.history.TrashEntities
import org.autojs.autojs.storage.history.TrashRestoreController
import org.autojs.autojs.ui.BaseActivity
import org.autojs.autojs6.R
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/**
* Trash page.
* zh-CN: 回收站页面.
*
* Created by JetBrains AI Assistant (GPT-5.2) on Feb 4, 2026.
*/
class TrashActivity : BaseActivity() {
private lateinit var recyclerView: RecyclerView
private val adapter = TrashAdapter(
onClick = { item ->
TrashRestoreController(this).startRestoreFlow(item)
}
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
title = getString(R.string.text_trash)
recyclerView = RecyclerView(this).apply {
layoutManager = LinearLayoutManager(this@TrashActivity)
adapter = this@TrashActivity.adapter
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
)
}
setContentView(recyclerView)
load()
}
private fun load() {
Schedulers.io().scheduleDirect {
runCatching {
val dao = HistoryDatabase.getInstance(applicationContext).historyDao()
val items = dao.listTrashItemsDesc()
AndroidSchedulers.mainThread().scheduleDirect {
adapter.submit(items)
if (items.isEmpty()) {
DialogUtils.buildAndShowAdaptive {
MaterialDialog.Builder(this)
.title(R.string.text_prompt)
.content(R.string.text_no_data)
.positiveText(R.string.dialog_button_dismiss)
.positiveColorRes(R.color.dialog_button_default)
.cancelable(true)
.build()
}
}
}
}.onFailure {
it.printStackTrace()
AndroidSchedulers.mainThread().scheduleDirect {
DialogUtils.buildAndShowAdaptive {
MaterialDialog.Builder(this)
.title(R.string.text_prompt)
.content(it.message ?: it.toString())
.positiveText(R.string.dialog_button_dismiss)
.positiveColorRes(R.color.dialog_button_default)
.cancelable(true)
.build()
}
}
}
}
}
companion object {
fun startActivity(context: Context) {
context.startActivity(
Intent(context, TrashActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
)
}
}
}
private class TrashAdapter(
private val onClick: (TrashEntities.TrashItem) -> Unit,
) : RecyclerView.Adapter<TrashViewHolder>() {
private var items: List<TrashEntities.TrashItem> = emptyList()
fun submit(newItems: List<TrashEntities.TrashItem>) {
items = newItems
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrashViewHolder {
return TrashViewHolder.create(parent, onClick)
}
override fun onBindViewHolder(holder: TrashViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount(): Int = items.size
}
private class TrashViewHolder(
itemView: View,
private val onClick: (TrashEntities.TrashItem) -> Unit,
) : RecyclerView.ViewHolder(itemView) {
private val titleView: TextView = (itemView as LinearLayout).getChildAt(0) as TextView
private val subtitleView: TextView = (itemView as LinearLayout).getChildAt(1) as TextView
private var boundItem: TrashEntities.TrashItem? = null
fun bind(item: TrashEntities.TrashItem) {
boundItem = item
val name = File(item.originalPath).name
titleView.text = name
val fmt = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
val timeText = fmt.format(Date(item.trashedAt))
subtitleView.text = "${item.originalPath}\n$timeText"
itemView.setOnClickListener {
boundItem?.let(onClick)
}
}
companion object {
fun create(parent: ViewGroup, onClick: (TrashEntities.TrashItem) -> Unit): TrashViewHolder {
val ctx = parent.context
val root = LinearLayout(ctx).apply {
orientation = LinearLayout.VERTICAL
setPadding(32, 24, 32, 24)
layoutParams = RecyclerView.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
)
gravity = Gravity.CENTER_VERTICAL
}
val title = TextView(ctx).apply {
textSize = 16f
}
val subtitle = TextView(ctx).apply {
textSize = 12f
alpha = 0.75f
}
root.addView(title)
root.addView(subtitle)
return TrashViewHolder(root, onClick)
}
}
}

View File

@@ -0,0 +1,182 @@
package org.autojs.autojs.ui.storage
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.afollestad.materialdialogs.MaterialDialog
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import org.autojs.autojs.app.DialogUtils
import org.autojs.autojs.storage.history.HistoryDatabase
import org.autojs.autojs.storage.history.HistoryEntities
import org.autojs.autojs.storage.history.VersionHistoryController
import org.autojs.autojs.ui.BaseActivity
import org.autojs.autojs6.R
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/**
* Version history page (all files).
* zh-CN: 版本历史页面 (全部文件).
*
* Created by JetBrains AI Assistant (GPT-5.2) on Feb 4, 2026.
*/
class VersionHistoryActivity : BaseActivity() {
private lateinit var recyclerView: RecyclerView
private val adapter = VersionHistoryFileAdapter(
onClick = { entry ->
VersionHistoryController(this).showForFilePath(entry.logicalPath)
}
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
title = getString(R.string.text_version_history)
recyclerView = RecyclerView(this).apply {
layoutManager = LinearLayoutManager(this@VersionHistoryActivity)
adapter = this@VersionHistoryActivity.adapter
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
)
}
setContentView(recyclerView)
load()
}
private fun load() {
Schedulers.io().scheduleDirect {
runCatching {
val dao = HistoryDatabase.getInstance(applicationContext).historyDao()
val files = dao.listAllFiles().sortedByDescending { it.lastSeenAt }
AndroidSchedulers.mainThread().scheduleDirect {
adapter.submit(files)
if (files.isEmpty()) {
DialogUtils.buildAndShowAdaptive {
MaterialDialog.Builder(this)
.title(R.string.text_prompt)
.content(R.string.text_no_data)
.positiveText(R.string.dialog_button_dismiss)
.positiveColorRes(R.color.dialog_button_default)
.cancelable(true)
.build()
}
}
}
}.onFailure {
it.printStackTrace()
AndroidSchedulers.mainThread().scheduleDirect {
DialogUtils.buildAndShowAdaptive {
MaterialDialog.Builder(this)
.title(R.string.text_prompt)
.content(it.message ?: it.toString())
.positiveText(R.string.dialog_button_dismiss)
.positiveColorRes(R.color.dialog_button_default)
.cancelable(true)
.build()
}
}
}
}
}
companion object {
fun startActivity(context: Context) {
context.startActivity(
Intent(context, VersionHistoryActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
)
}
}
}
private class VersionHistoryFileAdapter(
private val onClick: (HistoryEntities.FileEntry) -> Unit,
) : RecyclerView.Adapter<VersionHistoryFileViewHolder>() {
private var items: List<HistoryEntities.FileEntry> = emptyList()
fun submit(newItems: List<HistoryEntities.FileEntry>) {
items = newItems
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VersionHistoryFileViewHolder {
return VersionHistoryFileViewHolder.create(parent, onClick)
}
override fun onBindViewHolder(holder: VersionHistoryFileViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount(): Int = items.size
}
private class VersionHistoryFileViewHolder(
itemView: View,
private val onClick: (HistoryEntities.FileEntry) -> Unit,
) : RecyclerView.ViewHolder(itemView) {
private val titleView: TextView = (itemView as LinearLayout).getChildAt(0) as TextView
private val subtitleView: TextView = (itemView as LinearLayout).getChildAt(1) as TextView
private var boundItem: HistoryEntities.FileEntry? = null
fun bind(item: HistoryEntities.FileEntry) {
boundItem = item
titleView.text = item.logicalPath
val fmt = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
val timeText = fmt.format(Date(item.lastSeenAt))
subtitleView.text = "${item.latestFingerprint}\n$timeText"
itemView.setOnClickListener {
boundItem?.let(onClick)
}
}
companion object {
fun create(parent: ViewGroup, onClick: (HistoryEntities.FileEntry) -> Unit): VersionHistoryFileViewHolder {
val ctx = parent.context
val root = LinearLayout(ctx).apply {
orientation = LinearLayout.VERTICAL
setPadding(32, 24, 32, 24)
layoutParams = RecyclerView.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
)
gravity = Gravity.CENTER_VERTICAL
}
val title = TextView(ctx).apply {
textSize = 15f
}
val subtitle = TextView(ctx).apply {
textSize = 11f
alpha = 0.75f
}
root.addView(title)
root.addView(subtitle)
return VersionHistoryFileViewHolder(root, onClick)
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -14,6 +14,9 @@
<item
android:id="@+id/action_delete"
android:title="@string/text_delete" />
<item
android:id="@+id/action_version_history"
android:title="@string/text_version_history" />
<item
android:id="@+id/action_timed_task"
android:title="@string/text_timed_task" />

View File

@@ -1233,4 +1233,20 @@
<string name="text_version_history">سجل الاصدارات</string>
<string name="text_no_version_history">لا يوجد سجل للاصدارات</string>
<string name="text_version_history_restore_does_not_auto_save_to_disk">عند الاستعادة، يتم تغيير محتوى المحرر فقط. لن يتم الحفظ تلقائيا في الملف. يرجى الضغط على زر \"حفظ\" يدويا.</string>
<string name="item_move_to_trash">نقل الى سلة المحذوفات</string>
<string name="item_delete_permanently">حذف نهائيا</string>
<string name="text_moved_to_trash">تم النقل الى سلة المحذوفات</string>
<string name="text_choose_delete_strategy">اختر طريقة الحذف</string>
<string name="text_trash">سلة المحذوفات</string>
<string name="text_choose_restore_path">اختر مسار الاستعادة</string>
<string name="text_restore_to_original_path">استعادة الى المسار الاصلي</string>
<string name="text_restore_to_specified_path">استعادة الى مسار محدد</string>
<string name="text_path_colon_value">المسار: %1$s</string>
<string name="text_file_name_colon_value">اسم الملف: %1$s</string>
<string name="dialog_button_choose_path">اختيار</string>
<string name="text_overwrite">استبدال</string>
<string name="text_auto_rename">اعادة تسمية تلقائيا</string>
<string name="text_no_data">لا توجد بيانات</string>
<string name="error_invalid_path">مسار غير صالح: %1$s</string>
<string name="text_version_history_restore_will_overwrite_file">ستؤدي الاستعادة الى استبدال محتوى الملف.\nالمسار: %1$s</string>
</resources>

View File

@@ -1228,4 +1228,20 @@
<string name="text_version_history">Version history</string>
<string name="text_no_version_history">No version history</string>
<string name="text_version_history_restore_does_not_auto_save_to_disk">Restoring only changes the editor content. It will not automatically save to the file. Please tap the Save button manually.</string>
<string name="item_move_to_trash">Move to trash</string>
<string name="item_delete_permanently">Delete permanently</string>
<string name="text_moved_to_trash">Moved to trash</string>
<string name="text_choose_delete_strategy">Choose delete strategy</string>
<string name="text_trash">Trash</string>
<string name="text_choose_restore_path">Choose restore path</string>
<string name="text_restore_to_original_path">Restore to original path</string>
<string name="text_restore_to_specified_path">Restore to specified path</string>
<string name="text_path_colon_value">Path: %1$s</string>
<string name="text_file_name_colon_value">File name: %1$s</string>
<string name="dialog_button_choose_path">Choose</string>
<string name="text_overwrite">Overwrite</string>
<string name="text_auto_rename">Auto rename</string>
<string name="text_no_data">No data</string>
<string name="error_invalid_path">Invalid path: %1$s</string>
<string name="text_version_history_restore_will_overwrite_file">Restore will overwrite file content.\nPath: %1$s</string>
</resources>

View File

@@ -1231,4 +1231,20 @@
<string name="text_version_history">Historial de versiones</string>
<string name="text_no_version_history">Sin historial de versiones</string>
<string name="text_version_history_restore_does_not_auto_save_to_disk">Al restaurar, solo se cambia el contenido del editor. No se guardara automaticamente en el archivo. Debes pulsar el boton Guardar manualmente.</string>
<string name="item_move_to_trash">Mover a la papelera</string>
<string name="item_delete_permanently">Eliminar definitivamente</string>
<string name="text_moved_to_trash">Movido a la papelera</string>
<string name="text_choose_delete_strategy">Elegir metodo de eliminacion</string>
<string name="text_trash">Papelera</string>
<string name="text_choose_restore_path">Elegir ruta de restauracion</string>
<string name="text_restore_to_original_path">Restaurar a la ruta original</string>
<string name="text_restore_to_specified_path">Restaurar a una ruta especifica</string>
<string name="text_path_colon_value">Ruta: %1$s</string>
<string name="text_file_name_colon_value">Nombre de archivo: %1$s</string>
<string name="dialog_button_choose_path">Elegir</string>
<string name="text_overwrite">Sobrescribir</string>
<string name="text_auto_rename">Renombrar automaticamente</string>
<string name="text_no_data">Sin datos</string>
<string name="error_invalid_path">Ruta no valida: %1$s</string>
<string name="text_version_history_restore_will_overwrite_file">La restauracion sobrescribira el contenido del archivo.\nRuta: %1$s</string>
</resources>

View File

@@ -71,38 +71,38 @@
<string name="description_accessibility_service">Le service d\'accessibilité est la capacité centrale d\'AutoJs6 pour l\'automatisation. Il sert à lire les informations des contrôles à l\'écran et à simuler des interactions telles que [ appuyer / faire glisser / saisir ].\n\nLa plupart des fonctions liées à l\'automatisation, ainsi que des outils comme l\'analyse de mise en page, nécessitent l\'activation du service d\'accessibilité pour fonctionner correctement.</string>
<string name="description_all_files_access" tools:ignore="TypographyEllipsis">L\'autorisation \"gérer tous les fichiers\" (ou \"accès à tous les fichiers\") permet à AutoJs6 d\'[ créer / lire / modifier / supprimer ] des fichiers directement via des chemins de fichiers classiques dans l\'espace de stockage partagé, afin que les scripts puissent accéder à \"Internal Storage\" et que l\'explorateur de fichiers puisse afficher et gérer les fichiers correctement.\n\nSur les appareils Android 11+, c\'est le principal moyen d\'obtenir un accès lecture/écriture à l\'ensemble du stockage.</string>
<string name="description_app_language_preference">Cette préférence permet de modifier la langue d\'affichage d\'AutoJs6, y compris les messages d\'exception des scripts en cours d\'exécution.\n\nNote : un redémarrage de l\'application peut être nécessaire pour que la langue s\'applique comme prévu.</string>
<string name="description_auto_night_mode">Lorsque le mode nuit automatique est activé, AutoJs6 bascule automatiquement en mode nuit en fonction des paramètres système.\n\nNote: l\'interrupteur de mode nuit automatique et l\'interrupteur de mode nuit sont liés et s\'influencent mutuellement.</string>
<string name="description_background_popup_permission" tools:ignore="TypographyEllipsis">L\'autorisation \"fenêtres contextuelles en arrière-plan\" (également appelée \"démarrer une interface en arrière-plan\" ou \"afficher une interface en arrière-plan\") permet à AutoJs6, même lorsque l\'application est en arrière-plan ou sans interface visible, de démarrer une Activity ou d\'ouvrir une page de paramètres spécifique. Utile dans des scénarios tels que [ ouvrir l\'UI lors du déclenchement d\'une tâche planifiée / reprendre l\'interaction après l\'écran verrouillé ou la mise en veille / ouvrir la page de configuration des scripts via une notification ou un raccourci ].\n\nNote: sur des systèmes comme Xiaomi (MIUI/HyperOS) et Vivo (OriginOS/Funtouch OS), cette autorisation peut être désactivée par défaut. Si elle n\'est pas accordée, le système peut bloquer l\'ouverture de pages depuis l\'arrière-plan par un script, ce qui peut se manifester par [ aucune réaction / exécution en arrière-plan sans interface / échec de navigation ].\n\nMême après l\'avoir accordée, d\'autres politiques système peuvent encore s\'appliquer, telles que [ optimisation de la batterie / restrictions d\'autodémarrage / gel en arrière-plan / mise en veille de l\'application ]. Il est recommandé de l\'ajuster conjointement avec les autorisations associées ou des réglages de liste blanche.</string>
<string name="description_auto_night_mode">Lorsque le mode nuit automatique est activé, AutoJs6 bascule automatiquement en mode nuit en fonction des paramètres système.\n\nNote : l\'interrupteur de mode nuit automatique et l\'interrupteur de mode nuit sont liés et s\'influencent mutuellement.</string>
<string name="description_background_popup_permission" tools:ignore="TypographyEllipsis">L\'autorisation \"fenêtres contextuelles en arrière-plan\" (également appelée \"démarrer une interface en arrière-plan\" ou \"afficher une interface en arrière-plan\") permet à AutoJs6, même lorsque l\'application est en arrière-plan ou sans interface visible, de démarrer une Activity ou d\'ouvrir une page de paramètres spécifique. Utile dans des scénarios tels que [ ouvrir l\'UI lors du déclenchement d\'une tâche planifiée / reprendre l\'interaction après l\'écran verrouillé ou la mise en veille / ouvrir la page de configuration des scripts via une notification ou un raccourci ].\n\nNote : sur des systèmes comme Xiaomi (MIUI/HyperOS) et Vivo (OriginOS/Funtouch OS), cette autorisation peut être désactivée par défaut. Si elle n\'est pas accordée, le système peut bloquer l\'ouverture de pages depuis l\'arrière-plan par un script, ce qui peut se manifester par [ aucune réaction / exécution en arrière-plan sans interface / échec de navigation ].\n\nMême après l\'avoir accordée, d\'autres politiques système peuvent encore s\'appliquer, telles que [ optimisation de la batterie / restrictions d\'autodémarrage / gel en arrière-plan / mise en veille de l\'application ]. Il est recommandé de l\'ajuster conjointement avec les autorisations associées ou des réglages de liste blanche.</string>
<string name="description_change_working_dir_preference">Changer le chemin du répertoire contenant les scripts</string>.
<string name="description_check_for_updates_preference">AutoJs6 obtient et télécharge les mises à jour depuis GitHub.</string>
<string name="description_client_mode">Le mode client permet à AutoJs6 de se connecter activement à un serveur distant afin d\'effectuer [ transfert de scripts / impression des journaux / contrôle à distance ].\n\nEn général, l\'appareil et le serveur doivent être sur le même réseau local (LAN) ou dans un environnement réseau où ils peuvent s\'atteindre mutuellement.</string>
<string name="description_display_over_other_app">L\'autorisation \"afficher par-dessus les autres applications\" permet à AutoJs6 d\'afficher des fenêtres flottantes ou un bouton flottant au-dessus des autres applications, afin d\'exécuter rapidement des actions ou de consulter des informations sur n\'importe quel écran.\n\nNote: si cette autorisation n\'est pas accordée, les fenêtres flottantes et le bouton flottant peuvent ne pas s\'afficher ou ne pas fonctionner correctement.</string>
<string name="description_display_over_other_app">L\'autorisation \"afficher par-dessus les autres applications\" permet à AutoJs6 d\'afficher des fenêtres flottantes ou un bouton flottant au-dessus des autres applications, afin d\'exécuter rapidement des actions ou de consulter des informations sur n\'importe quel écran.\n\nNote : si cette autorisation n\'est pas accordée, les fenêtres flottantes et le bouton flottant peuvent ne pas s\'afficher ou ne pas fonctionner correctement.</string>
<string name="description_documentation_source_preference">Documentation locale : Utilisation des fichiers intégrés d\'AutoJs6 comme sources de documentation, aucune connexion réseau n\'est nécessaire. Le contenu change au fur et à mesure que AutoJs6 est mis à jour.\nDocumentation en ligne : Utilisation des pages GitHub comme sources de documentation, nécessitant une connexion réseau. Le contenu est toujours à jour.\n\nRemarque : pour optimiser l\'expérience de lecture des documents en ligne, il est recommandé d\'utiliser un dispositif d\'affichage grand écran tel qu\'un moniteur de bureau.\nRemarque : à partir de la version 6.2.0, la documentation est encore dans les phases préliminaires de développement, et la plupart du contenu n\'a pas encore été écrit ou mis à jour.</string>
<string name="description_documentation_source_preference_more">Les conditions suivantes doivent être remplies pour la prise en charge du thème sombre sur les pages de la documentation :\n1. Le mode nuit d\'AutoJs6 est activé\n2. WebView du système Android (ou des navigateurs comme Google Chrome) :\n- Android API Level 29 (Android 10) [Q] et supérieur : version >= 76\n- Android API Niveau 28 (Android 9) [P] et inférieur : version >= 105</string>
<string name="description_extending_js_build_in_objects">L\'extension des objets JavaScript intégrés peut augmenter la flexibilité du code et permettre des fonctionnalités plus riches, mais elle peut provoquer des conflits et même des pannes.\nTous les scripts ont des extensions intégrées activées par défaut lorsque l\'option est activée.\nLes extensions intégrées sont souvent peu sûres et ne sont pas recommandées, sauf si les principes et les risques des extensions intégrées sont clairement compris.\nLorsque l\'option est désactivée, les extensions intégrées peuvent toujours être activées via l\'objet global plugins, comme décrit dans la documentation du projet.</string>
<string name="description_file_extensions_preference">Permet de définir l\'affichage des extensions de fichier dans l\'explorateur AutoJs6.</string>
<string name="description_floating_button">Le bouton flottant affiche un point d\'entrée rapide déplaçable au bord de l\'écran, pouvant être utilisé pour [ démarrer ou arrêter des scripts / analyse de mise en page / afficher le nom de paquet ou le nom d\'activité récents / afficher la position du pointeur ].\n\nNote: cette fonction nécessite généralement l\'autorisation \"afficher par-dessus les autres applications\".</string>
<string name="description_floating_button">Le bouton flottant affiche un point d\'entrée rapide déplaçable au bord de l\'écran, pouvant être utilisé pour [ démarrer ou arrêter des scripts / analyse de mise en page / afficher le nom de paquet ou le nom d\'activité récents / afficher la position du pointeur ].\n\nNote : cette fonction nécessite généralement l\'autorisation \"afficher par-dessus les autres applications\".</string>
<string name="description_foreground_service">Le service au premier plan permet de maintenir AutoJs6 en fonctionnement plus stable en arrière-plan, adapté à des scénarios tels que [ exécution prolongée de scripts / maintien de connexion / écoute continue ].\n\nUne fois activé, le système affiche une \"notification de service au premier plan\" pour informer l\'utilisateur qu\'AutoJs6 continue de s\'exécuter. Cette notification reste généralement affichée jusqu\'à l\'arrêt du service.\n\nDéfinir la notification sur \"Silent\" ou \"Minimize\" ne fait que réduire l\'intensité de l\'alerte et n\'affecte généralement pas le service au premier plan.\n\nSi le canal de notifications est désactivé ou si AutoJs6 est empêché d\'envoyer des notifications, cela peut affecter le démarrage normal et la stabilité du service au premier plan.</string>
<string name="description_hidden_files_preference">Permet d\'afficher ou non les fichiers et dossiers cachés (commençant généralement par \".\") dans l\'explorateur de fichiers d\'AutoJs6.</string>
<string name="description_ignore_battery_optimizations">Ignorer l\'optimisation de la batterie peut réduire les restrictions du système sur AutoJs6 en arrière-plan, ce qui aide les tâches planifiées ou les scripts de longue durée à s\'exécuter de manière relativement plus stable en veille.\n\nNote: selon le fabricant, le système peut appliquer des politiques d\'économie d\'énergie supplémentaires, telles que des restrictions d\'auto-démarrage ou un gel en arrière-plan.</string>
<string name="description_ignore_battery_optimizations">Ignorer l\'optimisation de la batterie peut réduire les restrictions du système sur AutoJs6 en arrière-plan, ce qui aide les tâches planifiées ou les scripts de longue durée à s\'exécuter de manière relativement plus stable en veille.\n\nNote : selon le fabricant, le système peut appliquer des politiques d\'économie d\'énergie supplémentaires, telles que des restrictions d\'auto-démarrage ou un gel en arrière-plan.</string>
<string name="description_keep_screen_on_when_in_foreground">Permet de contrôler si l\'écran reste allumé lorsque AutoJs6 est au premier plan.</string>
<string name="description_keep_screen_on_when_in_foreground_preference">Préférence pour que l\'écran de l\'appareil reste allumé et lumineux lorsque AutoJs6 est au premier plan.\nPour que cela se produise uniquement sur la page d\'accueil parmi toutes les pages de l\'application AutoJs6, choisissez l\'option \"page d\'accueil uniquement\".</string>
<string name="description_launcher_icon_preference">L\'icône du lanceur prend en charge les modes d\'affichage d\'icône adaptative ou d\'icône héritée à fond transparent. L\'effet d\'affichage réel peut varier selon les systèmes Android. L\'application du changement peut prendre quelques secondes après avoir basculé de mode d\'affichage.</string>
<string name="description_launcher_shortcuts">Les raccourcis permettent d\'effectuer des actions spécifiques dans AutoJs6. Les utilisateurs peuvent afficher ces raccourcis dans un lanceur pris en charge et démarrer rapidement certaines tâches ou lancer une activité, comme la lecture de la documentation de l\'application AutoJs6, le lancement de la page de configuration d\'AutoJs6, etc.</string>
<string name="description_manage_ignored_updates_preference">Cliquez pour afficher ou gérer les éléments de la liste.\nAppuyez longuement pour supprimer l\'élément de la liste.</string>
<string name="description_night_mode">Permet de contrôler et de refléter la stratégie de mode nuit d\'AutoJs6.\n\nNote: l\'interrupteur de mode nuit et l\'interrupteur de mode nuit automatique sont liés et s\'influencent mutuellement.</string>
<string name="description_night_mode_preference">Le mode nuit (également connu sous le nom de thème sombre) s\'applique à la fois à l\'interface utilisateur du système Android et aux applications exécutées sur l\'appareil, ce qui améliore la visibilité pour les utilisateurs malvoyants et ceux qui sont sensibles à la lumière vive, et facilite l\'utilisation d\'un appareil par quiconque dans un environnement à faible luminosité.\n\nSystème de suivi : AutoJs6 possède des paramètres de mode nuit identiques à ceux du système Android\nToujours activé: AutoJs6 maintient le mode Nuit activé (indépendamment des paramètres du système Android).\nToujours désactivé: AutoJs6 désactive le mode Nuit (indépendamment des paramètres du système Android).\n\nRemarque : l\'option Suivre le système n\'est disponible qu\'à partir du niveau 28 de l\'API Android (Android 9) [P].</string>
<string name="description_night_mode_preference_more">Pour activer le mode Nuit dans le système Android :\n- Niveau 29 de l\'API Android (Android 10) [Q] et supérieur : Paramètres -> Affichage -> Thème.\n- Niveau 28 de l\'API Android (Android 9) [P]: Options du développeur -> Mode nuit.\n\nLes conditions suivantes doivent être remplies pour appliquer un mode nuit (thème sombre) à un contenu Web à l\'aide d\'un composant WebView (comme la page de documentation AutoJs6) :\n1. WebView du système Android (ou des navigateurs comme Google Chrome) :\n- Android API Level 29 (Android 10) [Q] et plus : version >= 76\n- API Android Niveau 28 (Android 9) [P]: version >= 105\n2. Le contenu Web du composant WebView est adapté au thème sombre (par des ressources CSS ou Android XML, etc.).</string>
<string name="description_night_mode">Permet de contrôler et de refléter la stratégie de mode nuit d\'AutoJs6.\n\nNote : l\'interrupteur de mode nuit et l\'interrupteur de mode nuit automatique sont liés et s\'influencent mutuellement.</string>
<string name="description_night_mode_preference">Le mode nuit (également connu sous le nom de thème sombre) s\'applique à la fois à l\'interface utilisateur du système Android et aux applications exécutées sur l\'appareil, ce qui améliore la visibilité pour les utilisateurs malvoyants et ceux qui sont sensibles à la lumière vive, et facilite l\'utilisation d\'un appareil par quiconque dans un environnement à faible luminosité.\n\nSystème de suivi : AutoJs6 possède des paramètres de mode nuit identiques à ceux du système Android\nToujours activé : AutoJs6 maintient le mode Nuit activé (indépendamment des paramètres du système Android).\nToujours désactivé : AutoJs6 désactive le mode Nuit (indépendamment des paramètres du système Android).\n\nRemarque : l\'option Suivre le système n\'est disponible qu\'à partir du niveau 28 de l\'API Android (Android 9) [P].</string>
<string name="description_night_mode_preference_more">Pour activer le mode Nuit dans le système Android :\n- Niveau 29 de l\'API Android (Android 10) [Q] et supérieur : Paramètres -> Affichage -> Thème.\n- Niveau 28 de l\'API Android (Android 9) [P] : Options du développeur -> Mode nuit.\n\nLes conditions suivantes doivent être remplies pour appliquer un mode nuit (thème sombre) à un contenu Web à l\'aide d\'un composant WebView (comme la page de documentation AutoJs6) :\n1. WebView du système Android (ou des navigateurs comme Google Chrome) :\n- Android API Level 29 (Android 10) [Q] et plus : version >= 76\n- API Android Niveau 28 (Android 9) [P] : version >= 105\n2. Le contenu Web du composant WebView est adapté au thème sombre (par des ressources CSS ou Android XML, etc.).</string>
<string name="description_notification_access">L\'autorisation \"accès aux notifications\" (ou \"autorisation de lecture des notifications\") permet à AutoJs6 de lire le contenu des notifications système, afin que les scripts puissent écouter des notifications ou récupérer le texte des notifications, etc.</string>
<string name="description_pointer_location">\"Emplacement du pointeur\" est une fonctionnalité de débogage dans les options pour les développeurs d\'Android.\nUne fois activée, le système affiche à l\'écran des informations sur le(s) point(s) de contact, telles que [coordonnées/trajectoire de déplacement/nombre/taille/vitesse de déplacement/pression], ce qui facilite [l\'écriture/le débogage/la vérification] des scripts associés.</string>
<string name="description_post_notifications">L\'autorisation \"envoyer des notifications\" permet à AutoJs6 de publier des notifications dans le système, afin que les scripts puissent publier et gérer des notifications personnalisées dans le panneau de notifications.\n\nNote: sur les appareils Android 13+, si cette autorisation n\'est pas accordée, certaines notifications peuvent ne pas s\'afficher, et cela peut affecter le démarrage et la stabilité des services au premier plan.</string>
<string name="description_post_notifications">L\'autorisation \"envoyer des notifications\" permet à AutoJs6 de publier des notifications dans le système, afin que les scripts puissent publier et gérer des notifications personnalisées dans le panneau de notifications.\n\nNote : sur les appareils Android 13+, si cette autorisation n\'est pas accordée, certaines notifications peuvent ne pas s\'afficher, et cela peut affecter le démarrage et la stabilité des services au premier plan.</string>
<string name="description_project_media_access">Avec l\'accès aux médias du projet, l\'avertissement de sécurité pour l\'enregistrement d\'écran ne sera pas demandé.</string>
<string name="description_restart_strategy">La stratégie de redémarrage n\'affecte que le bouton de redémarrage dans le tiroir de la page d\'accueil.\n\nRedémarrage rapide : redémarre rapidement l\'application. Si le redémarrage échoue ou qu\'un comportement inattendu survient, essayez de passer à \"Redémarrage planifié\".\nRedémarrage planifié : configure à l\'avance une tâche de courte durée. Après l\'arrêt de l\'application, elle redémarrera selon la planification afin de relancer l\'application.</string>
<string name="description_rhino_java_primitive_wrap">Interrupteur activé (par défaut) : les valeurs retournées par les méthodes Java de type Number/Boolean/Character sont encapsulées comme objets Java et exposées au script (String exclu). typeof vaut \"object\", species vaut \"JavaObject\" ; les méthodes Java restent accessibles, ce qui permet de conserver les caractéristiques de type précises de Java et la résolution de surcharge.\n\nInterrupteur désactivé : les types cidessus ne sont plus encapsulés et sont exposés directement comme primitives JavaScript (number/boolean/chaîne à un seul caractère). typeof correspond au type JavaScript respectif, plus conforme à la sémantique/à l\'écosystème JavaScript. Il est toujours possible de déclarer explicitement un wrapper Java avec new, par ex. new java.lang.Boolean(true).\n\nVoir : http://issues.autojs6.com/435</string>
<string name="description_root_mode_preference">Si vous avez un accès exotique à la racine ou un état anormal pour l\'accès à la racine, vous pouvez forcer le réglage de la racine sur racine ou non-racine.</string>
<string name="description_root_record_out_file_type_preference">Type binaire : non modifiable, avec l\'extension de fichier \"auto\".\nType JavaScript : peut être édité ou copié directement, avec l\'extension de fichier \"js\".</string>
<string name="description_screen_capture_request_delay">Lors de la demande d\'autorisation de capture d\'écran, la fenêtre de demande affichée peut comporter une animation de fondu lors de sa disparition. Si `images.captureScreen` est appelé immédiatement, la capture obtenue peut contenir le contenu de cette fenêtre et être partiellement masquée.\n\nLa valeur de cette option ajoute un délai (en millisecondes) avant d\'effectuer la capture juste après l\'obtention de l\'autorisation, afin d\'éviter le problème de masquage ci-dessus.\n\nCette option ne s\'applique qu\'à la première capture après l\'obtention de l\'autorisation ; les captures suivantes ne seront plus affectées par cette valeur.</string>
<string name="description_server_mode">Le mode serveur permet à AutoJs6 de démarrer un service sur l\'appareil actuel et d\'attendre des connexions de clients externes afin d\'effectuer [ transfert de scripts / impression des journaux / contrôle à distance ].\n\nLe mode serveur d\'AutoJs6 prend en charge deux modes de connexion:\n1. Réseau local (LAN)\n2. Android Debug Bridge (ADB)</string>
<string name="description_server_mode">Le mode serveur permet à AutoJs6 de démarrer un service sur l\'appareil actuel et d\'attendre des connexions de clients externes afin d\'effectuer [ transfert de scripts / impression des journaux / contrôle à distance ].\n\nLe mode serveur d\'AutoJs6 prend en charge deux modes de connexion :\n1. Réseau local (LAN)\n2. Android Debug Bridge (ADB)</string>
<string name="description_shizuku_access">Shizuku vous permet d\'obtenir des privilèges ADB et d\'accéder aux API du système</string>
<string name="description_stable_mode">Le mode stable le rend plus stable lors de l\'obtention des limites de mise en page, mais certains résultats peuvent être ignorés.\nA11y service\'s restart required.</string>
<string name="description_theme_color_preference">La couleur du thème est appliquée aux widgets, y compris, mais sans s\'y limiter, aux widgets suivants :\nBarre d\'état\nBarre d\'applications\nIcône de fichier\nIcône d\'élément de tâche\nFAB\nTitre de la catégorie Paramètres\nBouton de commutation\n\nRemarque : Depuis la version 6.2.0 d\'AutoJs6, il n\'y a pas encore de différence entre la couleur primaire, la couleur primaire foncée et la couleur d\'accentuation.</string>
@@ -243,7 +243,7 @@
<string name="error_failed_to_go_to_access_settings">Échec d\'ouverture de la page des paramètres</string>
<string name="error_failed_to_grant_shizuku_access">Échec de l\'octroi de la permission de Shizuku</string>
<string name="error_failed_to_instantiate">Échec de l\'instanciation de \"%s\"</string>
<string name="error_failed_to_instantiate_with_cause"><![CDATA[Échec de l\'instanciation de \"%1$s\": [ %2$s ]]]></string>
<string name="error_failed_to_instantiate_with_cause"><![CDATA[Échec de l\'instanciation de \"%1$s\" : [ %2$s ]]]></string>
<string name="error_failed_to_launch_manager">Échec du lancement du gestionnaire</string>
<string name="error_failed_to_launch_system_settings">Échec du lancement des paramètres système</string>
<string name="error_failed_to_load_plugins_with_reason">Échec du chargement des plugins.\nRaison : %1$s.</string>
@@ -311,10 +311,10 @@
<string name="error_parse_github_release_assets">Failed to parse GitHub release assets</string>
<string name="error_parse_version_info">Fail to parse version information</string>
<string name="error_pattern_syntax">Syntaxe de motif invalide</string>
<string name="error_plugin_apk_does_not_contain_required_assets_for_variant">L\'APK du plugin ne contient pas les ressources requises pour variant=\"%1$s\": %2$s.</string>
<string name="error_plugin_apk_does_not_contain_required_native_libraries">L\'APK du plugin ne contient pas les bibliothèques natives requises: %1$s.</string>
<string name="error_plugin_apk_does_not_contain_required_assets_for_variant">L\'APK du plugin ne contient pas les ressources requises pour variant=\"%1$s\" : %2$s.</string>
<string name="error_plugin_apk_does_not_contain_required_native_libraries">L\'APK du plugin ne contient pas les bibliothèques natives requises : %1$s.</string>
<string name="error_plugin_returned_empty_info">Le plugin %1$s a renvoyé des informations vides.</string>
<string name="error_plugin_returned_invalid_variant">Le plugin %1$s a renvoyé une variante invalide: %2$s.</string>
<string name="error_plugin_returned_invalid_variant">Le plugin %1$s a renvoyé une variante invalide : %2$s.</string>
<string name="error_port_num_over_65535">Numéro de port supérieur à 65535</string>
<string name="error_project_main_script_file_with_abs_path_does_not_exist">Le fichier de script principal du projet \"%1$s\" n\'existe pas</string>
<string name="error_put_value_into_json">Cannot put value %s into JSON</string>
@@ -603,7 +603,7 @@
<string name="text_copy_file">Copier le fichier</string>
<string name="text_copy_folder">Copier le dossier</string>
<string name="text_copy_line">Copie de la ligne</string>
<string name="text_copy_same_path_confirm">Le chemin source est identique au chemin de destination. Continuer la copie?\n\nNouveau nom: \"%1$s\".</string>
<string name="text_copy_same_path_confirm">Le chemin source est identique au chemin de destination. Continuer la copie?\n\nNouveau nom : \"%1$s\".</string>
<string name="text_copy_to">Copier vers</string>
<string name="text_copy_to_clip">Copie vers le presse-papiers</string>
<string name="text_copy_value">Copie de la valeur</string>
@@ -1231,4 +1231,20 @@
<string name="text_version_history">Historique des versions</string>
<string name="text_no_version_history">Aucun historique des versions</string>
<string name="text_version_history_restore_does_not_auto_save_to_disk">La restauration ne modifie que le contenu de l\'editeur. Elle n\'enregistre pas automatiquement dans le fichier. Veuillez appuyer manuellement sur le bouton Enregistrer.</string>
<string name="item_move_to_trash">Deplacer vers la corbeille</string>
<string name="item_delete_permanently">Supprimer definitivement</string>
<string name="text_moved_to_trash">Deplace vers la corbeille</string>
<string name="text_choose_delete_strategy">Choisir la methode de suppression</string>
<string name="text_trash">Corbeille</string>
<string name="text_choose_restore_path">Choisir le chemin de restauration</string>
<string name="text_restore_to_original_path">Restaurer vers le chemin d\'origine</string>
<string name="text_restore_to_specified_path">Restaurer vers un chemin specifique</string>
<string name="text_path_colon_value">Chemin : %1$s</string>
<string name="text_file_name_colon_value">Nom du fichier : %1$s</string>
<string name="dialog_button_choose_path">Choisir</string>
<string name="text_overwrite">Ecraser</string>
<string name="text_auto_rename">Renommer automatiquement</string>
<string name="text_no_data">Aucune donnee</string>
<string name="error_invalid_path">Chemin invalide : %1$s</string>
<string name="text_version_history_restore_will_overwrite_file">La restauration ecrasera le contenu du fichier.\nChemin : %1$s</string>
</resources>

View File

@@ -1232,4 +1232,20 @@
<string name="text_version_history">バージョン履歴</string>
<string name="text_no_version_history">バージョン履歴なし</string>
<string name="text_version_history_restore_does_not_auto_save_to_disk">復元すると編集内容のみが変更されます. ファイルには自動保存されません. 保存ボタンを手動でタップしてください.</string>
<string name="item_move_to_trash">ゴミ箱に移動</string>
<string name="item_delete_permanently">完全に削除</string>
<string name="text_moved_to_trash">ゴミ箱に移動しました</string>
<string name="text_choose_delete_strategy">削除方法を選択</string>
<string name="text_trash">ゴミ箱</string>
<string name="text_choose_restore_path">復元先を選択</string>
<string name="text_restore_to_original_path">元のパスに復元</string>
<string name="text_restore_to_specified_path">指定したパスに復元</string>
<string name="text_path_colon_value">パス: %1$s</string>
<string name="text_file_name_colon_value">ファイル名: %1$s</string>
<string name="dialog_button_choose_path">選択</string>
<string name="text_overwrite">上書き</string>
<string name="text_auto_rename">自動リネーム</string>
<string name="text_no_data">データなし</string>
<string name="error_invalid_path">無効なパス: %1$s</string>
<string name="text_version_history_restore_will_overwrite_file">復元するとファイル内容が上書きされます.\nパス: %1$s</string>
</resources>

View File

@@ -1233,4 +1233,20 @@
<string name="text_version_history">버전 기록</string>
<string name="text_no_version_history">버전 기록 없음</string>
<string name="text_version_history_restore_does_not_auto_save_to_disk">복원하면 편집기 내용만 변경됩니다. 파일에 자동으로 저장되지 않습니다. 저장 버튼을 수동으로 눌러 주세요.</string>
<string name="item_move_to_trash">휴지통으로 이동</string>
<string name="item_delete_permanently">영구 삭제</string>
<string name="text_moved_to_trash">휴지통으로 이동했습니다</string>
<string name="text_choose_delete_strategy">삭제 방식 선택</string>
<string name="text_trash">휴지통</string>
<string name="text_choose_restore_path">복원 경로 선택</string>
<string name="text_restore_to_original_path">원래 경로로 복원</string>
<string name="text_restore_to_specified_path">지정한 경로로 복원</string>
<string name="text_path_colon_value">경로: %1$s</string>
<string name="text_file_name_colon_value">파일 이름: %1$s</string>
<string name="dialog_button_choose_path">선택</string>
<string name="text_overwrite">덮어쓰기</string>
<string name="text_auto_rename">자동 이름 변경</string>
<string name="text_no_data">데이터 없음</string>
<string name="error_invalid_path">잘못된 경로: %1$s</string>
<string name="text_version_history_restore_will_overwrite_file">복원하면 파일 내용이 덮어써집니다.\n경로: %1$s</string>
</resources>

View File

@@ -1231,4 +1231,20 @@
<string name="text_version_history">История версий</string>
<string name="text_no_version_history">Нет истории версий</string>
<string name="text_version_history_restore_does_not_auto_save_to_disk">Восстановление изменяет только содержимое редактора. Файл не будет сохранен автоматически. Нажмите кнопку \"Сохранить\" вручную.</string>
<string name="item_move_to_trash">Переместить в корзину</string>
<string name="item_delete_permanently">Удалить навсегда</string>
<string name="text_moved_to_trash">Перемещено в корзину</string>
<string name="text_choose_delete_strategy">Выберите способ удаления</string>
<string name="text_trash">Корзина</string>
<string name="text_choose_restore_path">Выберите путь восстановления</string>
<string name="text_restore_to_original_path">Восстановить в исходный путь</string>
<string name="text_restore_to_specified_path">Восстановить в указанный путь</string>
<string name="text_path_colon_value">Путь: %1$s</string>
<string name="text_file_name_colon_value">Имя файла: %1$s</string>
<string name="dialog_button_choose_path">Выбрать</string>
<string name="text_overwrite">Перезаписать</string>
<string name="text_auto_rename">Автопереименование</string>
<string name="text_no_data">Нет данных</string>
<string name="error_invalid_path">Недопустимый путь: %1$s</string>
<string name="text_version_history_restore_will_overwrite_file">Восстановление перезапишет содержимое файла.\nПуть: %1$s</string>
</resources>

View File

@@ -1229,4 +1229,20 @@
<string name="text_version_history">版本歷史</string>
<string name="text_no_version_history">無版本歷史</string>
<string name="text_version_history_restore_does_not_auto_save_to_disk">恢復時只修改編輯器內容, 不會自動保存到文件, 需要手動點擊保存按鈕.</string>
<string name="item_move_to_trash">移入回收站</string>
<string name="item_delete_permanently">永久刪除</string>
<string name="text_moved_to_trash">已移入回收站</string>
<string name="text_choose_delete_strategy">選擇刪除策略</string>
<string name="text_trash">回收站</string>
<string name="text_choose_restore_path">選擇恢復路徑</string>
<string name="text_restore_to_original_path">恢復到原始路徑</string>
<string name="text_restore_to_specified_path">恢復到指定路徑</string>
<string name="text_path_colon_value">路徑: %1$s</string>
<string name="text_file_name_colon_value">文件名: %1$s</string>
<string name="dialog_button_choose_path">選擇路徑</string>
<string name="text_overwrite">覆蓋</string>
<string name="text_auto_rename">自動重命名</string>
<string name="text_no_data">無數據</string>
<string name="error_invalid_path">無效的路徑: %1$s</string>
<string name="text_version_history_restore_will_overwrite_file">恢復將覆蓋文件內容.\n路徑: %1$s</string>
</resources>

View File

@@ -1229,4 +1229,20 @@
<string name="text_version_history">版本歷史</string>
<string name="text_no_version_history">無版本歷史</string>
<string name="text_version_history_restore_does_not_auto_save_to_disk">恢復時只修改編輯器內容, 不會自動儲存到檔案, 需要手動點選儲存按鈕.</string>
<string name="item_move_to_trash">移入回收站</string>
<string name="item_delete_permanently">永久刪除</string>
<string name="text_moved_to_trash">已移入回收站</string>
<string name="text_choose_delete_strategy">選擇刪除策略</string>
<string name="text_trash">回收站</string>
<string name="text_choose_restore_path">選擇恢復路徑</string>
<string name="text_restore_to_original_path">恢復到原始路徑</string>
<string name="text_restore_to_specified_path">恢復到指定路徑</string>
<string name="text_path_colon_value">路徑: %1$s</string>
<string name="text_file_name_colon_value">檔名: %1$s</string>
<string name="dialog_button_choose_path">選擇路徑</string>
<string name="text_overwrite">覆蓋</string>
<string name="text_auto_rename">自動重新命名</string>
<string name="text_no_data">無資料</string>
<string name="error_invalid_path">無效的路徑: %1$s</string>
<string name="text_version_history_restore_will_overwrite_file">恢復將覆蓋檔案內容.\n路徑: %1$s</string>
</resources>

View File

@@ -1229,4 +1229,20 @@
<string name="text_version_history">版本历史</string>
<string name="text_no_version_history">无版本历史</string>
<string name="text_version_history_restore_does_not_auto_save_to_disk">恢复时只修改编辑器内容, 不会自动保存到文件, 需要手动点击保存按钮.</string>
<string name="item_move_to_trash">移入回收站</string>
<string name="item_delete_permanently">永久删除</string>
<string name="text_moved_to_trash">已移入回收站</string>
<string name="text_choose_delete_strategy">选择删除策略</string>
<string name="text_trash">回收站</string>
<string name="text_choose_restore_path">选择恢复路径</string>
<string name="text_restore_to_original_path">恢复到原始路径</string>
<string name="text_restore_to_specified_path">恢复到指定路径</string>
<string name="text_path_colon_value">路径: %1$s</string>
<string name="text_file_name_colon_value">文件名: %1$s</string>
<string name="dialog_button_choose_path">选择路径</string>
<string name="text_overwrite">覆盖</string>
<string name="text_auto_rename">自动重命名</string>
<string name="text_no_data">无数据</string>
<string name="error_invalid_path">无效的路径: %1$s</string>
<string name="text_version_history_restore_will_overwrite_file">恢复将覆盖文件内容.\n路径: %1$s</string>
</resources>

View File

@@ -1489,5 +1489,21 @@
<string name="dialog_button_save_as">Save as</string>
<string name="text_version_history">Version history</string>
<string name="text_no_version_history">No version history</string>
<string name="text_version_history_restore_does_not_auto_save_to_disk">Restore will only change editor content; it will not save to file automatically.</string>
<string name="text_version_history_restore_does_not_auto_save_to_disk">Restoring only changes the editor content. It will not automatically save to the file. Please tap the Save button manually.</string>
<string name="item_move_to_trash">Move to trash</string>
<string name="item_delete_permanently">Delete permanently</string>
<string name="text_moved_to_trash">Moved to trash</string>
<string name="text_choose_delete_strategy">Choose delete strategy</string>
<string name="text_trash">Trash</string>
<string name="text_choose_restore_path">Choose restore path</string>
<string name="text_restore_to_original_path">Restore to original path</string>
<string name="text_restore_to_specified_path">Restore to specified path</string>
<string name="text_path_colon_value">Path: %1$s</string>
<string name="text_file_name_colon_value">File name: %1$s</string>
<string name="dialog_button_choose_path">Choose</string>
<string name="text_overwrite">Overwrite</string>
<string name="text_auto_rename">Auto rename</string>
<string name="text_no_data">No data</string>
<string name="error_invalid_path">Invalid path: %1$s</string>
<string name="text_version_history_restore_will_overwrite_file">Restore will overwrite file content.\nPath: %1$s</string>
</resources>

View File

@@ -1,5 +1,5 @@
#Tue Feb 03 21:34:34 CST 2026
BUILD_TIME=1770125674544
#Wed Feb 04 12:36:35 CST 2026
BUILD_TIME=1770179795823
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=3691
VERSION_BUILD=3693
VERSION_NAME=6.7.0 Alpha19
VSCODE_EXT_REQUIRED_VERSION=1.0.13