6.7.0 - Alpha19 - 版本历史 M3 - 新增回收站功能; 版本历史与回收站增加入口
This commit is contained in:
@@ -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))",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
@@ -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 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
184
app/src/main/java/org/autojs/autojs/ui/storage/TrashActivity.kt
Normal file
184
app/src/main/java/org/autojs/autojs/ui/storage/TrashActivity.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
app/src/main/res/drawable/ic_recycle_bin.png
Normal file
BIN
app/src/main/res/drawable/ic_recycle_bin.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
BIN
app/src/main/res/drawable/ic_version_history.png
Normal file
BIN
app/src/main/res/drawable/ic_version_history.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
@@ -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" />
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user