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

View File

@@ -475,4 +475,8 @@ object DialogUtils {
progressBar.setProgressBackgroundTintList(ColorStateList.valueOf(bgColor)) 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 { override suspend fun doWork(): Result {
return runCatching { return runCatching {
cleanupExpiredRevisions() cleanupExpiredRevisions()
cleanupExpiredTrashItems()
cleanupOrphanHistoryBlobs() cleanupOrphanHistoryBlobs()
cleanupOrphanTrashBlobs()
cleanupEmergencyDrafts() cleanupEmergencyDrafts()
}.fold( }.fold(
onSuccess = { Result.success() }, 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() { private fun cleanupEmergencyDrafts() {
// Keep consistent with EditorView's local cleanup policy. // Keep consistent with EditorView's local cleanup policy.
// zh-CN: 与 EditorView 的本地清理策略保持一致. // zh-CN: 与 EditorView 的本地清理策略保持一致.
@@ -118,6 +172,7 @@ class HistoryCleanupWorker(
companion object { companion object {
private const val MAX_DAYS_MS: Long = 30L * 24L * 60L * 60L * 1000L 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_DAYS_MS: Long = 7L * 24L * 60L * 60L * 1000L
private const val DRAFT_MAX_TOTAL_BYTES: Long = 200L * 1024L * 1024L private const val DRAFT_MAX_TOTAL_BYTES: Long = 200L * 1024L * 1024L
} }

View File

@@ -40,4 +40,19 @@ interface HistoryDao {
@Query("SELECT blobRelPath FROM revision") @Query("SELECT blobRelPath FROM revision")
fun listAllBlobRelPaths(): List<String> 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.Database
import androidx.room.Room import androidx.room.Room
import androidx.room.RoomDatabase 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. * Created by JetBrains AI Assistant (GPT-5.2) on Feb 3, 2026.
@@ -12,8 +14,9 @@ import androidx.room.RoomDatabase
entities = [ entities = [
HistoryEntities.FileEntry::class, HistoryEntities.FileEntry::class,
HistoryEntities.Revision::class, HistoryEntities.Revision::class,
TrashEntities.TrashItem::class,
], ],
version = 1, version = 2,
exportSchema = false, exportSchema = false,
) )
abstract class HistoryDatabase : RoomDatabase() { abstract class HistoryDatabase : RoomDatabase() {
@@ -25,12 +28,37 @@ abstract class HistoryDatabase : RoomDatabase() {
@Volatile @Volatile
private var instance: HistoryDatabase? = null 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) { fun getInstance(applicationContext: Context): HistoryDatabase = instance ?: synchronized(this) {
instance ?: Room.databaseBuilder( instance ?: Room.databaseBuilder(
applicationContext, applicationContext,
HistoryDatabase::class.java, HistoryDatabase::class.java,
"history-database.db", "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.pio.UncheckedIOException;
import org.autojs.autojs.project.ProjectConfig; import org.autojs.autojs.project.ProjectConfig;
import org.autojs.autojs.storage.file.TmpScriptFiles; 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.filechooser.FileChooserDialogBuilder;
import org.autojs.autojs.ui.shortcut.ShortcutCreateActivity; import org.autojs.autojs.ui.shortcut.ShortcutCreateActivity;
import org.autojs.autojs.ui.timing.TimedTaskSettingActivity; import org.autojs.autojs.ui.timing.TimedTaskSettingActivity;
import org.autojs.autojs.util.EnvironmentUtils; import org.autojs.autojs.util.EnvironmentUtils;
import org.autojs.autojs.util.FileUtils;
import org.autojs.autojs.util.IntentUtils; import org.autojs.autojs.util.IntentUtils;
import org.autojs.autojs.util.MaterialDialogUtils; import org.autojs.autojs.util.MaterialDialogUtils;
import org.autojs.autojs.util.ShortcutUtils; 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.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.autojs.autojs.util.FileUtils;
import static org.autojs.autojs.app.DialogUtils.fixCheckBoxGravity; import static org.autojs.autojs.app.DialogUtils.fixCheckBoxGravity;
import static org.autojs.autojs.model.explorer.ExplorerFileItem.isInSampleDir; import static org.autojs.autojs.model.explorer.ExplorerFileItem.isInSampleDir;
@@ -957,14 +958,31 @@ public class ScriptOperations {
} }
public void delete(final ScriptFile scriptFile) { public void delete(final ScriptFile scriptFile) {
DialogUtils.showAdaptive(new MaterialDialog.Builder(mContext) DialogUtils.buildAndShowAdaptive(() -> {
.title(mContext.getString(R.string.text_confirm_to_delete)) MaterialDialog.Builder builder = new MaterialDialog.Builder(mContext)
.title(R.string.text_choose_delete_strategy)
.content(scriptFile.getName()) .content(scriptFile.getName())
.negativeText(R.string.text_cancel) .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) .positiveText(R.string.dialog_button_confirm)
.positiveColorRes(R.color.dialog_button_caution) .positiveColorRes(R.color.dialog_button_caution);
.onPositive((dialog, which) -> deleteWithoutConfirm(scriptFile)) MaterialDialogUtils.choiceWidgetThemeColor(builder);
.build()); return builder.build();
});
} }
public void setAsWorkingDir(final ScriptFile scriptFile) { public void setAsWorkingDir(final ScriptFile scriptFile) {
@@ -987,6 +1005,68 @@ public class ScriptOperations {
.build()); .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) { public void deleteWithoutConfirm(final ScriptFile scriptFile) {
boolean isDir = scriptFile.isDirectory(); boolean isDir = scriptFile.isDirectory();
boolean isProject = ProjectConfig.isProject(scriptFile); 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.EXTRA_EXCEPTION_MESSAGE
import org.autojs.autojs.model.script.Scripts.openByOtherApps import org.autojs.autojs.model.script.Scripts.openByOtherApps
import org.autojs.autojs.model.script.Scripts.runWithBroadcastSender 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.getNameWithoutExtension
import org.autojs.autojs.pio.PFiles.write import org.autojs.autojs.pio.PFiles.write
import org.autojs.autojs.storage.file.TmpScriptFiles 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.HistoryRepository
import org.autojs.autojs.storage.history.HistoryUriUtils import org.autojs.autojs.storage.history.HistoryUriUtils
import org.autojs.autojs.storage.history.VersionHistoryController
import org.autojs.autojs.tool.Callback 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.doc.ManualDialog
import org.autojs.autojs.ui.edit.completion.CodeCompletionBar import org.autojs.autojs.ui.edit.completion.CodeCompletionBar
import org.autojs.autojs.ui.edit.completion.CodeCompletionBar.OnHintClickListener 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.DisplayUtils.pxToSp
import org.autojs.autojs.util.DocsUtils.getUrl import org.autojs.autojs.util.DocsUtils.getUrl
import org.autojs.autojs.util.MaterialDialogUtils.choiceWidgetThemeColor 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.Observers
import org.autojs.autojs.util.StringUtils 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.showSnack
import org.autojs.autojs.util.ViewUtils.showToast import org.autojs.autojs.util.ViewUtils.showToast
import org.autojs.autojs6.R import org.autojs.autojs6.R
@@ -89,8 +85,6 @@ import java.io.IOException
import java.nio.charset.Charset import java.nio.charset.Charset
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.security.MessageDigest import java.security.MessageDigest
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale import java.util.Locale
/** /**
@@ -717,140 +711,18 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
} }
fun showVersionHistoryDialog() { fun showVersionHistoryDialog() {
val logicalPath = HistoryUriUtils.toLogicalPathOrNull(uri) // Delegate to controller to unify editor/explorer/history page behavior.
if (logicalPath == null) { // zh-CN: 委托给 Controller, 统一 editor/explorer/history page 的行为.
// History is only tracked under internal storage root. VersionHistoryController(context).showForEditor(
// zh-CN: 历史记录仅纳管内部存储根目录下的文件. uri = uri,
DialogUtils.buildAndShowAdaptive { onRestoreToEditor = { restoredText ->
MaterialDialog.Builder(context) editor.text = restoredText
.title(R.string.text_version_history) },
.content(R.string.text_no_version_history) onRestoredUi = {
.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) setMenuItemStatus(R.id.save, true)
showSnack(this@EditorView, R.string.text_done) 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)
} }
// A minimal emergency draft store. // A minimal emergency draft store.
@@ -1211,10 +1083,6 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
// zh-CN: 草稿总容量上限 (200MB). // zh-CN: 草稿总容量上限 (200MB).
private const val DRAFT_MAX_TOTAL_BYTES: Long = 200L * 1024L * 1024L 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). // Internal storage root (no external SD).
// zh-CN: 内部存储根目录 (不访问外置 SD). // zh-CN: 内部存储根目录 (不访问外置 SD).
private const val INTERNAL_STORAGE_ROOT: String = "/storage/emulated/0" 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.PFile
import org.autojs.autojs.pio.PFiles import org.autojs.autojs.pio.PFiles
import org.autojs.autojs.project.ProjectConfig import org.autojs.autojs.project.ProjectConfig
import org.autojs.autojs.storage.history.VersionHistoryController
import org.autojs.autojs.theme.ThemeColorHelper import org.autojs.autojs.theme.ThemeColorHelper
import org.autojs.autojs.theme.ThemeColorManagerCompat import org.autojs.autojs.theme.ThemeColorManagerCompat
import org.autojs.autojs.theme.widget.ThemeColorSwipeRefreshLayout import org.autojs.autojs.theme.widget.ThemeColorSwipeRefreshLayout
@@ -284,6 +285,16 @@ open class ExplorerView : ThemeColorSwipeRefreshLayout, SwipeRefreshLayout.OnRef
ScriptOperations(context, this@ExplorerView, currentPage) ScriptOperations(context, this@ExplorerView, currentPage)
.delete(selectedItem!!.toScriptFile()) .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 -> { R.id.action_run_repeatedly -> {
ScriptLoopDialog(context, selectedItem!!.toScriptFile()) ScriptLoopDialog(context, selectedItem!!.toScriptFile())
.show() .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.main.MainActivity
import org.autojs.autojs.ui.settings.AboutActivity import org.autojs.autojs.ui.settings.AboutActivity
import org.autojs.autojs.ui.settings.PreferencesActivity 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.DisplayUtils
import org.autojs.autojs.util.IntentUtils.App.exit import org.autojs.autojs.util.IntentUtils.App.exit
import org.autojs.autojs.util.IntentUtils.App.restart import org.autojs.autojs.util.IntentUtils.App.restart
@@ -120,6 +122,8 @@ open class DrawerFragment : Fragment() {
private lateinit var mAutoNightModeItem: DrawerMenuToggleableItem private lateinit var mAutoNightModeItem: DrawerMenuToggleableItem
private lateinit var mKeepScreenOnWhenInForegroundItem: DrawerMenuToggleableItem private lateinit var mKeepScreenOnWhenInForegroundItem: DrawerMenuToggleableItem
private lateinit var mThemeColorItem: DrawerMenuShortcutItem private lateinit var mThemeColorItem: DrawerMenuShortcutItem
private lateinit var mTrashItem: DrawerMenuShortcutItem
private lateinit var mVersionHistoryItem: DrawerMenuShortcutItem
private lateinit var mAboutAppAndDevItem: DrawerMenuShortcutItem private lateinit var mAboutAppAndDevItem: DrawerMenuShortcutItem
private lateinit var mA11yTool: AccessibilityTool private lateinit var mA11yTool: AccessibilityTool
@@ -586,13 +590,35 @@ open class DrawerFragment : Fragment() {
descriptionRes = R.string.description_keep_screen_on_when_in_foreground, descriptionRes = R.string.description_keep_screen_on_when_in_foreground,
) )
mThemeColorItem = DrawerMenuShortcutItem(R.drawable.ic_personalize_thicker, R.string.text_theme_color) mThemeColorItem = DrawerMenuShortcutItem(
.setAction(Runnable { ColorSelectBaseActivity.startActivity(mContext) }) icon = R.drawable.ic_personalize_thicker,
.apply { subtitle = ColorSelectBaseActivity.getCurrentColorSummary(mContext) } 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) mTrashItem = DrawerMenuShortcutItem(
.setAction(Runnable { AboutActivity.startActivity(mContext) }) icon = R.drawable.ic_recycle_bin,
.apply { subtitle = BuildConfig.VERSION_NAME } 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? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@@ -715,6 +741,9 @@ open class DrawerFragment : Fragment() {
mNightModeItem, mNightModeItem,
mKeepScreenOnWhenInForegroundItem, mKeepScreenOnWhenInForegroundItem,
mThemeColorItem, mThemeColorItem,
DrawerMenuGroup(R.string.text_file),
mTrashItem,
mVersionHistoryItem,
DrawerMenuGroup(R.string.text_about), DrawerMenuGroup(R.string.text_about),
mAboutAppAndDevItem, 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 <item
android:id="@+id/action_delete" android:id="@+id/action_delete"
android:title="@string/text_delete" /> android:title="@string/text_delete" />
<item
android:id="@+id/action_version_history"
android:title="@string/text_version_history" />
<item <item
android:id="@+id/action_timed_task" android:id="@+id/action_timed_task"
android:title="@string/text_timed_task" /> android:title="@string/text_timed_task" />

View File

@@ -1233,4 +1233,20 @@
<string name="text_version_history">سجل الاصدارات</string> <string name="text_version_history">سجل الاصدارات</string>
<string name="text_no_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="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> </resources>

View File

@@ -1228,4 +1228,20 @@
<string name="text_version_history">Version history</string> <string name="text_version_history">Version history</string>
<string name="text_no_version_history">No 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="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> </resources>

View File

@@ -1231,4 +1231,20 @@
<string name="text_version_history">Historial de versiones</string> <string name="text_version_history">Historial de versiones</string>
<string name="text_no_version_history">Sin 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="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> </resources>

View File

@@ -1231,4 +1231,20 @@
<string name="text_version_history">Historique des versions</string> <string name="text_version_history">Historique des versions</string>
<string name="text_no_version_history">Aucun 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="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> </resources>

View File

@@ -1232,4 +1232,20 @@
<string name="text_version_history">バージョン履歴</string> <string name="text_version_history">バージョン履歴</string>
<string name="text_no_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="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> </resources>

View File

@@ -1233,4 +1233,20 @@
<string name="text_version_history">버전 기록</string> <string name="text_version_history">버전 기록</string>
<string name="text_no_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="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> </resources>

View File

@@ -1231,4 +1231,20 @@
<string name="text_version_history">История версий</string> <string name="text_version_history">История версий</string>
<string name="text_no_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="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> </resources>

View File

@@ -1229,4 +1229,20 @@
<string name="text_version_history">版本歷史</string> <string name="text_version_history">版本歷史</string>
<string name="text_no_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="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> </resources>

View File

@@ -1229,4 +1229,20 @@
<string name="text_version_history">版本歷史</string> <string name="text_version_history">版本歷史</string>
<string name="text_no_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="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> </resources>

View File

@@ -1229,4 +1229,20 @@
<string name="text_version_history">版本历史</string> <string name="text_version_history">版本历史</string>
<string name="text_no_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="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> </resources>

View File

@@ -1489,5 +1489,21 @@
<string name="dialog_button_save_as">Save as</string> <string name="dialog_button_save_as">Save as</string>
<string name="text_version_history">Version history</string> <string name="text_version_history">Version history</string>
<string name="text_no_version_history">No 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> </resources>

View File

@@ -1,5 +1,5 @@
#Tue Feb 03 21:34:34 CST 2026 #Wed Feb 04 12:36:35 CST 2026
BUILD_TIME=1770125674544 BUILD_TIME=1770179795823
COMPILE_SDK_VERSION=36 COMPILE_SDK_VERSION=36
IMAGE_QUANT_CMAKE_VERSION=3.22.1 IMAGE_QUANT_CMAKE_VERSION=3.22.1
IMAGE_QUANT_NDK_VERSION=26.1.10909125 IMAGE_QUANT_NDK_VERSION=26.1.10909125
@@ -27,6 +27,6 @@ RAPID_OCR_OPENCV_MOBILE_LABEL_VERSION=13
RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3 RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3
TARGET_SDK_VERSION=36 TARGET_SDK_VERSION=36
TARGET_SDK_VERSION_INRT=29 TARGET_SDK_VERSION_INRT=29
VERSION_BUILD=3691 VERSION_BUILD=3693
VERSION_NAME=6.7.0 Alpha19 VERSION_NAME=6.7.0 Alpha19
VSCODE_EXT_REQUIRED_VERSION=1.0.13 VSCODE_EXT_REQUIRED_VERSION=1.0.13