diff --git a/.changelog/lang_zh-Hans.json b/.changelog/lang_zh-Hans.json
index 4af83e9a..16528145 100644
--- a/.changelog/lang_zh-Hans.json
+++ b/.changelog/lang_zh-Hans.json
@@ -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))",
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8640dab0..48286556 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -349,6 +349,14 @@
+
+
+
+
+ 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
}
diff --git a/app/src/main/java/org/autojs/autojs/storage/history/HistoryDao.kt b/app/src/main/java/org/autojs/autojs/storage/history/HistoryDao.kt
index 9590d860..dedeb8db 100644
--- a/app/src/main/java/org/autojs/autojs/storage/history/HistoryDao.kt
+++ b/app/src/main/java/org/autojs/autojs/storage/history/HistoryDao.kt
@@ -40,4 +40,19 @@ interface HistoryDao {
@Query("SELECT blobRelPath FROM revision")
fun listAllBlobRelPaths(): List
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun upsertTrashItem(item: TrashEntities.TrashItem)
+
+ @Query("SELECT * FROM trash_item ORDER BY trashedAt DESC")
+ fun listTrashItemsDesc(): List
+
+ @Query("SELECT * FROM trash_item WHERE trashedAt < :expiredBefore ORDER BY trashedAt ASC")
+ fun listExpiredTrashItems(expiredBefore: Long): List
+
+ @Query("DELETE FROM trash_item WHERE trashId IN (:trashIds)")
+ fun deleteTrashItemsByIds(trashIds: List)
+
+ @Query("SELECT blobRelPath FROM trash_item")
+ fun listAllTrashBlobRelPaths(): List
}
\ No newline at end of file
diff --git a/app/src/main/java/org/autojs/autojs/storage/history/HistoryDatabase.kt b/app/src/main/java/org/autojs/autojs/storage/history/HistoryDatabase.kt
index e6067e85..c78885c6 100644
--- a/app/src/main/java/org/autojs/autojs/storage/history/HistoryDatabase.kt
+++ b/app/src/main/java/org/autojs/autojs/storage/history/HistoryDatabase.kt
@@ -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 }
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/autojs/autojs/storage/history/TrashBlobStoreUtils.kt b/app/src/main/java/org/autojs/autojs/storage/history/TrashBlobStoreUtils.kt
new file mode 100644
index 00000000..65eeda0e
--- /dev/null
+++ b/app/src/main/java/org/autojs/autojs/storage/history/TrashBlobStoreUtils.kt
@@ -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()
+ }
+}
diff --git a/app/src/main/java/org/autojs/autojs/storage/history/TrashItemEntity.kt b/app/src/main/java/org/autojs/autojs/storage/history/TrashItemEntity.kt
new file mode 100644
index 00000000..c9469bb4
--- /dev/null
+++ b/app/src/main/java/org/autojs/autojs/storage/history/TrashItemEntity.kt
@@ -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,
+ )
+}
diff --git a/app/src/main/java/org/autojs/autojs/storage/history/TrashRepository.kt b/app/src/main/java/org/autojs/autojs/storage/history/TrashRepository.kt
new file mode 100644
index 00000000..c2fddf3a
--- /dev/null
+++ b/app/src/main/java/org/autojs/autojs/storage/history/TrashRepository.kt
@@ -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()
+ }
+}
diff --git a/app/src/main/java/org/autojs/autojs/storage/history/TrashRestoreController.kt b/app/src/main/java/org/autojs/autojs/storage/history/TrashRestoreController.kt
new file mode 100644
index 00000000..b10ff518
--- /dev/null
+++ b/app/src/main/java/org/autojs/autojs/storage/history/TrashRestoreController.kt
@@ -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()
+ 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"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/autojs/autojs/storage/history/VersionHistoryController.kt b/app/src/main/java/org/autojs/autojs/storage/history/VersionHistoryController.kt
new file mode 100644
index 00000000..61e69443
--- /dev/null
+++ b/app/src/main/java/org/autojs/autojs/storage/history/VersionHistoryController.kt
@@ -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"
+ }
+}
diff --git a/app/src/main/java/org/autojs/autojs/ui/common/ScriptOperations.java b/app/src/main/java/org/autojs/autojs/ui/common/ScriptOperations.java
index 3d8c0fb9..a5408d97 100644
--- a/app/src/main/java/org/autojs/autojs/ui/common/ScriptOperations.java
+++ b/app/src/main/java/org/autojs/autojs/ui/common/ScriptOperations.java
@@ -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);
diff --git a/app/src/main/java/org/autojs/autojs/ui/edit/EditorView.kt b/app/src/main/java/org/autojs/autojs/ui/edit/EditorView.kt
index 2212ecc3..5d441532 100644
--- a/app/src/main/java/org/autojs/autojs/ui/edit/EditorView.kt
+++ b/app/src/main/java/org/autojs/autojs/ui/edit/EditorView.kt
@@ -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"
diff --git a/app/src/main/java/org/autojs/autojs/ui/explorer/ExplorerView.kt b/app/src/main/java/org/autojs/autojs/ui/explorer/ExplorerView.kt
index f4246621..36ff75de 100644
--- a/app/src/main/java/org/autojs/autojs/ui/explorer/ExplorerView.kt
+++ b/app/src/main/java/org/autojs/autojs/ui/explorer/ExplorerView.kt
@@ -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()
diff --git a/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerFragment.kt b/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerFragment.kt
index 4d405941..5f1fc23d 100644
--- a/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerFragment.kt
+++ b/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerFragment.kt
@@ -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,
)
diff --git a/app/src/main/java/org/autojs/autojs/ui/storage/TrashActivity.kt b/app/src/main/java/org/autojs/autojs/ui/storage/TrashActivity.kt
new file mode 100644
index 00000000..e45641e3
--- /dev/null
+++ b/app/src/main/java/org/autojs/autojs/ui/storage/TrashActivity.kt
@@ -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() {
+
+ private var items: List = emptyList()
+
+ fun submit(newItems: List) {
+ 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)
+ }
+ }
+}
diff --git a/app/src/main/java/org/autojs/autojs/ui/storage/VersionHistoryActivity.kt b/app/src/main/java/org/autojs/autojs/ui/storage/VersionHistoryActivity.kt
new file mode 100644
index 00000000..4c54d2c0
--- /dev/null
+++ b/app/src/main/java/org/autojs/autojs/ui/storage/VersionHistoryActivity.kt
@@ -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() {
+
+ private var items: List = emptyList()
+
+ fun submit(newItems: List) {
+ 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)
+ }
+ }
+}
diff --git a/app/src/main/res/drawable/ic_recycle_bin.png b/app/src/main/res/drawable/ic_recycle_bin.png
new file mode 100644
index 00000000..b0fb952c
Binary files /dev/null and b/app/src/main/res/drawable/ic_recycle_bin.png differ
diff --git a/app/src/main/res/drawable/ic_version_history.png b/app/src/main/res/drawable/ic_version_history.png
new file mode 100644
index 00000000..ea8f810f
Binary files /dev/null and b/app/src/main/res/drawable/ic_version_history.png differ
diff --git a/app/src/main/res/menu/menu_script_options.xml b/app/src/main/res/menu/menu_script_options.xml
index a28ec36a..9aafbad6 100644
--- a/app/src/main/res/menu/menu_script_options.xml
+++ b/app/src/main/res/menu/menu_script_options.xml
@@ -14,6 +14,9 @@
+
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index 9282c54f..6b7daa21 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -1233,4 +1233,20 @@
سجل الاصدارات
لا يوجد سجل للاصدارات
عند الاستعادة، يتم تغيير محتوى المحرر فقط. لن يتم الحفظ تلقائيا في الملف. يرجى الضغط على زر \"حفظ\" يدويا.
+ نقل الى سلة المحذوفات
+ حذف نهائيا
+ تم النقل الى سلة المحذوفات
+ اختر طريقة الحذف
+ سلة المحذوفات
+ اختر مسار الاستعادة
+ استعادة الى المسار الاصلي
+ استعادة الى مسار محدد
+ المسار: %1$s
+ اسم الملف: %1$s
+ اختيار
+ استبدال
+ اعادة تسمية تلقائيا
+ لا توجد بيانات
+ مسار غير صالح: %1$s
+ ستؤدي الاستعادة الى استبدال محتوى الملف.\nالمسار: %1$s
\ No newline at end of file
diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml
index 623a44d1..f427edf5 100644
--- a/app/src/main/res/values-en/strings.xml
+++ b/app/src/main/res/values-en/strings.xml
@@ -1228,4 +1228,20 @@
Version history
No version history
Restoring only changes the editor content. It will not automatically save to the file. Please tap the Save button manually.
+ Move to trash
+ Delete permanently
+ Moved to trash
+ Choose delete strategy
+ Trash
+ Choose restore path
+ Restore to original path
+ Restore to specified path
+ Path: %1$s
+ File name: %1$s
+ Choose
+ Overwrite
+ Auto rename
+ No data
+ Invalid path: %1$s
+ Restore will overwrite file content.\nPath: %1$s
\ No newline at end of file
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 4155ca66..13f9ed7e 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -1231,4 +1231,20 @@
Historial de versiones
Sin historial de versiones
Al restaurar, solo se cambia el contenido del editor. No se guardara automaticamente en el archivo. Debes pulsar el boton Guardar manualmente.
+ Mover a la papelera
+ Eliminar definitivamente
+ Movido a la papelera
+ Elegir metodo de eliminacion
+ Papelera
+ Elegir ruta de restauracion
+ Restaurar a la ruta original
+ Restaurar a una ruta especifica
+ Ruta: %1$s
+ Nombre de archivo: %1$s
+ Elegir
+ Sobrescribir
+ Renombrar automaticamente
+ Sin datos
+ Ruta no valida: %1$s
+ La restauracion sobrescribira el contenido del archivo.\nRuta: %1$s
\ No newline at end of file
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 0f574a72..5688b900 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -71,38 +71,38 @@
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.
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.
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.
- 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.
- 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\'auto‑dé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.
+ 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.
+ 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\'auto‑dé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.
Changer le chemin du répertoire contenant les scripts.
AutoJs6 obtient et télécharge les mises à jour depuis GitHub.
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.
- 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.
+ 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.
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.
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
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.
Permet de définir l\'affichage des extensions de fichier dans l\'explorateur AutoJs6.
- 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\".
+ 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\".
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.
Permet d\'afficher ou non les fichiers et dossiers cachés (commençant généralement par \".\") dans l\'explorateur de fichiers d\'AutoJs6.
- 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.
+ 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.
Permet de contrôler si l\'écran reste allumé lorsque AutoJs6 est au premier plan.
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\".
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.
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.
Cliquez pour afficher ou gérer les éléments de la liste.\nAppuyez longuement pour supprimer l\'élément de la liste.
- 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.
- 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].
- 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.).
+ 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.
+ 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].
+ 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.).
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.
\"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.
- 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.
+ 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.
Avec l\'accès aux médias du projet, l\'avertissement de sécurité pour l\'enregistrement d\'écran ne sera pas demandé.
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.
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 ci‑dessus 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
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.
Type binaire : non modifiable, avec l\'extension de fichier \"auto\".\nType JavaScript : peut être édité ou copié directement, avec l\'extension de fichier \"js\".
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.
- 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)
+ 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)
Shizuku vous permet d\'obtenir des privilèges ADB et d\'accéder aux API du système
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.
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.
@@ -243,7 +243,7 @@
Échec d\'ouverture de la page des paramètres
Échec de l\'octroi de la permission de Shizuku
Échec de l\'instanciation de \"%s\"
-
+
Échec du lancement du gestionnaire
Échec du lancement des paramètres système
Échec du chargement des plugins.\nRaison : %1$s.
@@ -311,10 +311,10 @@
Failed to parse GitHub release assets
Fail to parse version information
Syntaxe de motif invalide
- L\'APK du plugin ne contient pas les ressources requises pour variant=\"%1$s\": %2$s.
- L\'APK du plugin ne contient pas les bibliothèques natives requises: %1$s.
+ L\'APK du plugin ne contient pas les ressources requises pour variant=\"%1$s\" : %2$s.
+ L\'APK du plugin ne contient pas les bibliothèques natives requises : %1$s.
Le plugin %1$s a renvoyé des informations vides.
- Le plugin %1$s a renvoyé une variante invalide: %2$s.
+ Le plugin %1$s a renvoyé une variante invalide : %2$s.
Numéro de port supérieur à 65535
Le fichier de script principal du projet \"%1$s\" n\'existe pas
Cannot put value %s into JSON
@@ -603,7 +603,7 @@
Copier le fichier
Copier le dossier
Copie de la ligne
- Le chemin source est identique au chemin de destination. Continuer la copie?\n\nNouveau nom: \"%1$s\".
+ Le chemin source est identique au chemin de destination. Continuer la copie?\n\nNouveau nom : \"%1$s\".
Copier vers
Copie vers le presse-papiers
Copie de la valeur
@@ -1231,4 +1231,20 @@
Historique des versions
Aucun historique des versions
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.
+ Deplacer vers la corbeille
+ Supprimer definitivement
+ Deplace vers la corbeille
+ Choisir la methode de suppression
+ Corbeille
+ Choisir le chemin de restauration
+ Restaurer vers le chemin d\'origine
+ Restaurer vers un chemin specifique
+ Chemin : %1$s
+ Nom du fichier : %1$s
+ Choisir
+ Ecraser
+ Renommer automatiquement
+ Aucune donnee
+ Chemin invalide : %1$s
+ La restauration ecrasera le contenu du fichier.\nChemin : %1$s
\ No newline at end of file
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index 97ac6a21..1c64a81b 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -1232,4 +1232,20 @@
バージョン履歴
バージョン履歴なし
復元すると編集内容のみが変更されます. ファイルには自動保存されません. 保存ボタンを手動でタップしてください.
+ ゴミ箱に移動
+ 完全に削除
+ ゴミ箱に移動しました
+ 削除方法を選択
+ ゴミ箱
+ 復元先を選択
+ 元のパスに復元
+ 指定したパスに復元
+ パス: %1$s
+ ファイル名: %1$s
+ 選択
+ 上書き
+ 自動リネーム
+ データなし
+ 無効なパス: %1$s
+ 復元するとファイル内容が上書きされます.\nパス: %1$s
\ No newline at end of file
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index dd19d906..0e53a346 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -1233,4 +1233,20 @@
버전 기록
버전 기록 없음
복원하면 편집기 내용만 변경됩니다. 파일에 자동으로 저장되지 않습니다. 저장 버튼을 수동으로 눌러 주세요.
+ 휴지통으로 이동
+ 영구 삭제
+ 휴지통으로 이동했습니다
+ 삭제 방식 선택
+ 휴지통
+ 복원 경로 선택
+ 원래 경로로 복원
+ 지정한 경로로 복원
+ 경로: %1$s
+ 파일 이름: %1$s
+ 선택
+ 덮어쓰기
+ 자동 이름 변경
+ 데이터 없음
+ 잘못된 경로: %1$s
+ 복원하면 파일 내용이 덮어써집니다.\n경로: %1$s
\ No newline at end of file
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 220c2592..271498dd 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -1231,4 +1231,20 @@
История версий
Нет истории версий
Восстановление изменяет только содержимое редактора. Файл не будет сохранен автоматически. Нажмите кнопку \"Сохранить\" вручную.
+ Переместить в корзину
+ Удалить навсегда
+ Перемещено в корзину
+ Выберите способ удаления
+ Корзина
+ Выберите путь восстановления
+ Восстановить в исходный путь
+ Восстановить в указанный путь
+ Путь: %1$s
+ Имя файла: %1$s
+ Выбрать
+ Перезаписать
+ Автопереименование
+ Нет данных
+ Недопустимый путь: %1$s
+ Восстановление перезапишет содержимое файла.\nПуть: %1$s
\ No newline at end of file
diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml
index 18abee6d..11ac22cc 100644
--- a/app/src/main/res/values-zh-rHK/strings.xml
+++ b/app/src/main/res/values-zh-rHK/strings.xml
@@ -1229,4 +1229,20 @@
版本歷史
無版本歷史
恢復時只修改編輯器內容, 不會自動保存到文件, 需要手動點擊保存按鈕.
+ 移入回收站
+ 永久刪除
+ 已移入回收站
+ 選擇刪除策略
+ 回收站
+ 選擇恢復路徑
+ 恢復到原始路徑
+ 恢復到指定路徑
+ 路徑: %1$s
+ 文件名: %1$s
+ 選擇路徑
+ 覆蓋
+ 自動重命名
+ 無數據
+ 無效的路徑: %1$s
+ 恢復將覆蓋文件內容.\n路徑: %1$s
\ No newline at end of file
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index d3f69cdd..c1b44489 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -1229,4 +1229,20 @@
版本歷史
無版本歷史
恢復時只修改編輯器內容, 不會自動儲存到檔案, 需要手動點選儲存按鈕.
+ 移入回收站
+ 永久刪除
+ 已移入回收站
+ 選擇刪除策略
+ 回收站
+ 選擇恢復路徑
+ 恢復到原始路徑
+ 恢復到指定路徑
+ 路徑: %1$s
+ 檔名: %1$s
+ 選擇路徑
+ 覆蓋
+ 自動重新命名
+ 無資料
+ 無效的路徑: %1$s
+ 恢復將覆蓋檔案內容.\n路徑: %1$s
\ No newline at end of file
diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml
index 9d77b12e..679e522b 100644
--- a/app/src/main/res/values-zh/strings.xml
+++ b/app/src/main/res/values-zh/strings.xml
@@ -1229,4 +1229,20 @@
版本历史
无版本历史
恢复时只修改编辑器内容, 不会自动保存到文件, 需要手动点击保存按钮.
+ 移入回收站
+ 永久删除
+ 已移入回收站
+ 选择删除策略
+ 回收站
+ 选择恢复路径
+ 恢复到原始路径
+ 恢复到指定路径
+ 路径: %1$s
+ 文件名: %1$s
+ 选择路径
+ 覆盖
+ 自动重命名
+ 无数据
+ 无效的路径: %1$s
+ 恢复将覆盖文件内容.\n路径: %1$s
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index d9f59f94..9566c3ec 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1489,5 +1489,21 @@
Save as
Version history
No version history
- Restore will only change editor content; it will not save to file automatically.
+ Restoring only changes the editor content. It will not automatically save to the file. Please tap the Save button manually.
+ Move to trash
+ Delete permanently
+ Moved to trash
+ Choose delete strategy
+ Trash
+ Choose restore path
+ Restore to original path
+ Restore to specified path
+ Path: %1$s
+ File name: %1$s
+ Choose
+ Overwrite
+ Auto rename
+ No data
+ Invalid path: %1$s
+ Restore will overwrite file content.\nPath: %1$s
\ No newline at end of file
diff --git a/version.properties b/version.properties
index 1f80329f..88176de3 100644
--- a/version.properties
+++ b/version.properties
@@ -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