6.7.0 - Alpha19 - 版本历史 M2 - 增加代码编辑器历史版本功能, 支持查看和恢复历史版本
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
"released_date": "2026/02/03",
|
||||
"feature": [
|
||||
"插件中心功能, 支持插件的安装/卸载/更新等操作 (入口: 主页抽屉按钮/主页标签页)",
|
||||
"版本历史功能, 支持查看/恢复可编辑文件的历史版本 (入口: 代码编辑器菜单)",
|
||||
"Paddle OCR (PP-OCRv5) 插件, 用于光学字符识别",
|
||||
"cvt 模块, 用于数据单位转换 (参阅 项目文档 > [单位转换](https://docs.autojs6.com/#/cvt))",
|
||||
"fmt 模块, 用于数据格式化 (参阅 项目文档 > [格式化](https://docs.autojs6.com/#/fmt))",
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.autojs.autojs.event.GlobalKeyObserver
|
||||
import org.autojs.autojs.external.receiver.DynamicBroadcastReceivers
|
||||
import org.autojs.autojs.ipc.InAppEventBus
|
||||
import org.autojs.autojs.leakcanary.LeakCanarySetup
|
||||
import org.autojs.autojs.storage.history.HistoryCleanupScheduler
|
||||
import org.autojs.autojs.theme.ThemeColorManager
|
||||
import org.autojs.autojs.timing.TimedTaskManager
|
||||
import org.autojs.autojs.timing.TimedTaskScheduler
|
||||
@@ -39,7 +40,7 @@ import java.lang.reflect.Method
|
||||
|
||||
/**
|
||||
* Created by Stardust on Jan 27, 2017.
|
||||
* Modified by SuperMonster003 as of Jan 17, 2026.
|
||||
* Modified by SuperMonster003 as of Feb 3, 2026.
|
||||
*/
|
||||
class App : MultiDexApplication() {
|
||||
|
||||
@@ -71,6 +72,8 @@ class App : MultiDexApplication() {
|
||||
|
||||
ThemeColorManager.init()
|
||||
setUpDefaultNightMode()
|
||||
|
||||
HistoryCleanupScheduler.schedule(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.autojs.autojs.storage.history
|
||||
|
||||
import android.content.Context
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Created by JetBrains AI Assistant (GPT-5.2) on Feb 3, 2026.
|
||||
*/
|
||||
class HistoryBlobStore(private val context: Context) {
|
||||
|
||||
fun writeRevisionBlob(fileId: String, revId: String, bytes: ByteArray): String {
|
||||
val rel = "history/blob/$fileId/$revId.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()
|
||||
}
|
||||
}
|
||||
|
||||
fun newFileId(): String = UUID.randomUUID().toString()
|
||||
|
||||
fun newRevId(): String = UUID.randomUUID().toString()
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.autojs.autojs.storage.history
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Scheduler for HistoryCleanupWorker.
|
||||
* zh-CN: HistoryCleanupWorker 的调度器.
|
||||
*
|
||||
* Created by JetBrains AI Assistant (GPT-5.2) on Feb 3, 2026.
|
||||
*/
|
||||
object HistoryCleanupScheduler {
|
||||
|
||||
private const val UNIQUE_WORK_NAME = "history_cleanup_periodic"
|
||||
|
||||
/**
|
||||
* Schedule periodic cleanup (once per day).
|
||||
* zh-CN: 调度周期性清理 (每天一次).
|
||||
*/
|
||||
fun schedule(context: Context) {
|
||||
val appContext = context.applicationContext
|
||||
|
||||
// No network is required; keep it light and battery-friendly.
|
||||
// zh-CN: 不需要网络; 尽量轻量/省电.
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.NOT_REQUIRED)
|
||||
.build()
|
||||
|
||||
val request = PeriodicWorkRequestBuilder<HistoryCleanupWorker>(1, TimeUnit.DAYS)
|
||||
.setConstraints(constraints)
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance(appContext).enqueueUniquePeriodicWork(
|
||||
UNIQUE_WORK_NAME,
|
||||
ExistingPeriodicWorkPolicy.KEEP,
|
||||
request,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package org.autojs.autojs.storage.history
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Periodic cleanup worker for history/trash/drafts.
|
||||
* zh-CN: 用于 history/trash/drafts 的周期性清理 Worker.
|
||||
*
|
||||
* Created by JetBrains AI Assistant (GPT-5.2) on Feb 3, 2026.
|
||||
*/
|
||||
class HistoryCleanupWorker(
|
||||
appContext: Context,
|
||||
params: WorkerParameters,
|
||||
) : CoroutineWorker(appContext, params) {
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
return runCatching {
|
||||
cleanupExpiredRevisions()
|
||||
cleanupOrphanHistoryBlobs()
|
||||
cleanupEmergencyDrafts()
|
||||
}.fold(
|
||||
onSuccess = { Result.success() },
|
||||
onFailure = { Result.retry() },
|
||||
)
|
||||
}
|
||||
|
||||
private fun cleanupExpiredRevisions() {
|
||||
val db = HistoryDatabase.getInstance(applicationContext)
|
||||
val dao = db.historyDao()
|
||||
val blobs = HistoryBlobStore(applicationContext)
|
||||
|
||||
// Remove expired revisions by days (30 days).
|
||||
// zh-CN: 按天数删除过期 revision (30 天).
|
||||
val now = System.currentTimeMillis()
|
||||
val expiredBefore = now - MAX_DAYS_MS
|
||||
val expired = dao.listExpiredRevisions(expiredBefore)
|
||||
if (expired.isEmpty()) return
|
||||
|
||||
dao.deleteRevisionsByIds(expired.map { it.revId })
|
||||
expired.forEach { blobs.deleteBlobByRelPath(it.blobRelPath) }
|
||||
}
|
||||
|
||||
private fun cleanupOrphanHistoryBlobs() {
|
||||
val db = HistoryDatabase.getInstance(applicationContext)
|
||||
val dao = db.historyDao()
|
||||
|
||||
// Build the referenced blob set from DB.
|
||||
// zh-CN: 从 DB 构建被引用的 blob 集合.
|
||||
val referenced = dao.listAllBlobRelPaths().toHashSet()
|
||||
|
||||
val root = File(applicationContext.filesDir, "history/blob")
|
||||
if (!root.exists() || !root.isDirectory) return
|
||||
|
||||
// Delete blobs that are not referenced by DB.
|
||||
// zh-CN: 删除 DB 未引用的 blob.
|
||||
root.walkTopDown()
|
||||
.filter { it.isFile }
|
||||
.forEach { f ->
|
||||
val rel = f.relativeTo(applicationContext.filesDir).invariantSeparatorsPath
|
||||
if (!referenced.contains(rel)) {
|
||||
// noinspection ResultOfMethodCallIgnored
|
||||
f.delete()
|
||||
}
|
||||
}
|
||||
|
||||
// Best-effort: remove empty directories.
|
||||
// zh-CN: 尽力删除空目录.
|
||||
root.walkBottomUp()
|
||||
.filter { it.isDirectory }
|
||||
.forEach { dir ->
|
||||
val children = dir.listFiles()
|
||||
if (children == null || children.isEmpty()) {
|
||||
// noinspection ResultOfMethodCallIgnored
|
||||
dir.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun cleanupEmergencyDrafts() {
|
||||
// Keep consistent with EditorView's local cleanup policy.
|
||||
// zh-CN: 与 EditorView 的本地清理策略保持一致.
|
||||
val draftsDir = File(applicationContext.filesDir, "drafts")
|
||||
if (!draftsDir.exists() || !draftsDir.isDirectory) return
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
// 1) Remove expired (older than 7 days).
|
||||
// zh-CN: 1) 删除过期草稿 (超过 7 天).
|
||||
val expiredBefore = now - DRAFT_MAX_DAYS_MS
|
||||
draftsDir.listFiles()?.forEach { f ->
|
||||
if (f.isFile && f.lastModified() < expiredBefore) {
|
||||
// noinspection ResultOfMethodCallIgnored
|
||||
f.delete()
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Enforce total bytes limit (keep newest first).
|
||||
// zh-CN: 2) 约束总容量上限 (优先保留最新).
|
||||
val remained = draftsDir.listFiles()
|
||||
?.filter { it.isFile }
|
||||
?.sortedByDescending { it.lastModified() }
|
||||
?: return
|
||||
|
||||
var total = remained.sumOf { it.length().coerceAtLeast(0L) }
|
||||
if (total <= DRAFT_MAX_TOTAL_BYTES) return
|
||||
|
||||
for (f in remained.asReversed()) {
|
||||
if (total <= DRAFT_MAX_TOTAL_BYTES) break
|
||||
val len = f.length().coerceAtLeast(0L)
|
||||
if (f.delete()) {
|
||||
total -= len
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_DAYS_MS: Long = 30L * 24L * 60L * 60L * 1000L
|
||||
private const val DRAFT_MAX_DAYS_MS: Long = 7L * 24L * 60L * 60L * 1000L
|
||||
private const val DRAFT_MAX_TOTAL_BYTES: Long = 200L * 1024L * 1024L
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package org.autojs.autojs.storage.history
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
|
||||
/**
|
||||
* Created by JetBrains AI Assistant (GPT-5.2) on Feb 3, 2026.
|
||||
*/
|
||||
@Dao
|
||||
interface HistoryDao {
|
||||
|
||||
@Query("SELECT * FROM file_entry WHERE logicalPath = :logicalPath LIMIT 1")
|
||||
fun findFileByPath(logicalPath: String): HistoryEntities.FileEntry?
|
||||
|
||||
@Query("SELECT * FROM file_entry WHERE latestFingerprint = :fingerprint ORDER BY lastSeenAt DESC LIMIT 1")
|
||||
fun findFileByLatestFingerprint(fingerprint: String): HistoryEntities.FileEntry?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun upsertFile(entry: HistoryEntities.FileEntry)
|
||||
|
||||
@Insert
|
||||
fun insertRevision(rev: HistoryEntities.Revision)
|
||||
|
||||
@Query("SELECT * FROM revision WHERE fileId = :fileId ORDER BY createdAt ASC")
|
||||
fun listRevisionsAsc(fileId: String): List<HistoryEntities.Revision>
|
||||
|
||||
@Query("DELETE FROM revision WHERE revId IN (:revIds)")
|
||||
fun deleteRevisionsByIds(revIds: List<String>)
|
||||
|
||||
@Query("SELECT SUM(sizeBytes) FROM revision WHERE fileId = :fileId")
|
||||
fun sumBytesByFileId(fileId: String): Long?
|
||||
|
||||
@Query("SELECT * FROM file_entry")
|
||||
fun listAllFiles(): List<HistoryEntities.FileEntry>
|
||||
|
||||
@Query("SELECT * FROM revision WHERE createdAt < :expiredBefore ORDER BY createdAt ASC")
|
||||
fun listExpiredRevisions(expiredBefore: Long): List<HistoryEntities.Revision>
|
||||
|
||||
@Query("SELECT blobRelPath FROM revision")
|
||||
fun listAllBlobRelPaths(): List<String>
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.autojs.autojs.storage.history
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
|
||||
/**
|
||||
* Created by JetBrains AI Assistant (GPT-5.2) on Feb 3, 2026.
|
||||
*/
|
||||
@Database(
|
||||
entities = [
|
||||
HistoryEntities.FileEntry::class,
|
||||
HistoryEntities.Revision::class,
|
||||
],
|
||||
version = 1,
|
||||
exportSchema = false,
|
||||
)
|
||||
abstract class HistoryDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun historyDao(): HistoryDao
|
||||
|
||||
companion object {
|
||||
|
||||
@Volatile
|
||||
private var instance: HistoryDatabase? = null
|
||||
|
||||
fun getInstance(applicationContext: Context): HistoryDatabase = instance ?: synchronized(this) {
|
||||
instance ?: Room.databaseBuilder(
|
||||
applicationContext,
|
||||
HistoryDatabase::class.java,
|
||||
"history-database.db",
|
||||
).build().also { instance = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
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 3, 2026.
|
||||
*/
|
||||
object HistoryEntities {
|
||||
|
||||
@Entity(
|
||||
tableName = "file_entry",
|
||||
indices = [
|
||||
Index(value = ["logicalPath"], unique = true),
|
||||
Index(value = ["latestFingerprint"]),
|
||||
Index(value = ["lastSeenAt"]),
|
||||
],
|
||||
)
|
||||
data class FileEntry(
|
||||
@PrimaryKey
|
||||
val fileId: String,
|
||||
|
||||
val logicalPath: String,
|
||||
|
||||
val createdAt: Long,
|
||||
val lastSeenAt: Long,
|
||||
|
||||
// Latest content fingerprint for "path lost but content same" matching.
|
||||
// zh-CN: 用于 "路径丢失但内容相同" 匹配的最新内容指纹.
|
||||
val latestFingerprint: String,
|
||||
)
|
||||
|
||||
@Entity(
|
||||
tableName = "revision",
|
||||
indices = [
|
||||
Index(value = ["fileId"]),
|
||||
Index(value = ["createdAt"]),
|
||||
],
|
||||
)
|
||||
data class Revision(
|
||||
@PrimaryKey
|
||||
val revId: String,
|
||||
|
||||
val fileId: String,
|
||||
|
||||
// Operation type for future extension (SAVE_PRE / RESTORE / TRASH etc.).
|
||||
// zh-CN: 操作类型, 供未来扩展 (SAVE_PRE / RESTORE / TRASH 等).
|
||||
val op: String,
|
||||
|
||||
val createdAt: Long,
|
||||
|
||||
val logicalPathAtThatTime: String,
|
||||
|
||||
val encoding: String,
|
||||
val hadBom: Boolean,
|
||||
|
||||
val sizeBytes: Long,
|
||||
|
||||
val sha256: String,
|
||||
|
||||
// Blob relative path under filesDir.
|
||||
// zh-CN: 位于 filesDir 下的相对路径.
|
||||
val blobRelPath: String,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package org.autojs.autojs.storage.history
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import java.io.File
|
||||
import java.security.MessageDigest
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Created by JetBrains AI Assistant (GPT-5.2) on Feb 3, 2026.
|
||||
*/
|
||||
class HistoryRepository(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 { HistoryBlobStore(context.applicationContext) }
|
||||
|
||||
/**
|
||||
* Read revision blob bytes.
|
||||
* zh-CN: 读取 revision 对应的 blob bytes.
|
||||
*/
|
||||
fun readRevisionBytes(rev: HistoryEntities.Revision): ByteArray {
|
||||
val f = File(context.filesDir, rev.blobRelPath)
|
||||
return f.inputStream().use { it.readBytes() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a SAVE_PRE snapshot (old content bytes).
|
||||
*
|
||||
* Rules:
|
||||
* - Only tracks files under internal storage root.
|
||||
* - Uses logicalPath first; fallback by latestFingerprint matching.
|
||||
* - Applies per-file retention after inserting.
|
||||
*
|
||||
* zh-CN:
|
||||
*
|
||||
* 记录一次 SAVE_PRE 快照 (旧内容 bytes).
|
||||
*
|
||||
* 规则:
|
||||
* - 仅纳管内部存储根目录下的文件.
|
||||
* - 优先按 logicalPath 匹配; 找不到时用 latestFingerprint 回退匹配.
|
||||
* - 插入后执行单文件保留策略清理.
|
||||
*/
|
||||
fun recordSavePre(
|
||||
uri: Uri,
|
||||
logicalPath: String,
|
||||
oldBytes: ByteArray,
|
||||
encodingName: String,
|
||||
hadBom: Boolean,
|
||||
) {
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
val oldSha = sha256Hex(oldBytes)
|
||||
val fileEntry = getOrCreateFileEntry(
|
||||
logicalPath = logicalPath,
|
||||
latestFingerprint = oldSha,
|
||||
now = now,
|
||||
)
|
||||
|
||||
val revId = blobs.newRevId()
|
||||
val blobRel = blobs.writeRevisionBlob(fileEntry.fileId, revId, oldBytes)
|
||||
|
||||
dao.insertRevision(
|
||||
HistoryEntities.Revision(
|
||||
revId = revId,
|
||||
fileId = fileEntry.fileId,
|
||||
op = "SAVE_PRE",
|
||||
createdAt = now,
|
||||
logicalPathAtThatTime = logicalPath,
|
||||
encoding = encodingName,
|
||||
hadBom = hadBom,
|
||||
sizeBytes = oldBytes.size.toLong(),
|
||||
sha256 = oldSha,
|
||||
blobRelPath = blobRel,
|
||||
)
|
||||
)
|
||||
|
||||
cleanupPerFile(fileEntry.fileId, now)
|
||||
}
|
||||
|
||||
private fun getOrCreateFileEntry(logicalPath: String, latestFingerprint: String, now: Long): HistoryEntities.FileEntry {
|
||||
val byPath = dao.findFileByPath(logicalPath)
|
||||
if (byPath != null) {
|
||||
val updated = byPath.copy(
|
||||
lastSeenAt = now,
|
||||
latestFingerprint = latestFingerprint,
|
||||
)
|
||||
dao.upsertFile(updated)
|
||||
return updated
|
||||
}
|
||||
|
||||
val byFingerprint = dao.findFileByLatestFingerprint(latestFingerprint)
|
||||
if (byFingerprint != null) {
|
||||
// Re-attach to new path.
|
||||
// zh-CN: 重新绑定到新路径.
|
||||
val updated = byFingerprint.copy(
|
||||
logicalPath = logicalPath,
|
||||
lastSeenAt = now,
|
||||
latestFingerprint = latestFingerprint,
|
||||
)
|
||||
dao.upsertFile(updated)
|
||||
return updated
|
||||
}
|
||||
|
||||
val created = HistoryEntities.FileEntry(
|
||||
fileId = blobs.newFileId(),
|
||||
logicalPath = logicalPath,
|
||||
createdAt = now,
|
||||
lastSeenAt = now,
|
||||
latestFingerprint = latestFingerprint,
|
||||
)
|
||||
dao.upsertFile(created)
|
||||
return created
|
||||
}
|
||||
|
||||
private fun cleanupPerFile(fileId: String, now: Long) {
|
||||
val revisions = dao.listRevisionsAsc(fileId).toMutableList()
|
||||
if (revisions.isEmpty()) return
|
||||
|
||||
// 1) Remove expired by days.
|
||||
// zh-CN: 1) 按天数删除过期版本.
|
||||
val expiredBefore = now - MAX_DAYS_MS
|
||||
val expired = revisions.filter { it.createdAt < expiredBefore }
|
||||
if (expired.isNotEmpty()) {
|
||||
deleteRevisionsAndBlobs(expired)
|
||||
revisions.removeAll(expired.toSet())
|
||||
}
|
||||
|
||||
// 2) Enforce max count (remove oldest).
|
||||
// zh-CN: 2) 约束最大版本数 (删除最旧).
|
||||
if (revisions.size > MAX_VERSIONS) {
|
||||
val toRemove = revisions.take(revisions.size - MAX_VERSIONS)
|
||||
deleteRevisionsAndBlobs(toRemove)
|
||||
revisions.removeAll(toRemove.toSet())
|
||||
}
|
||||
|
||||
// 3) Enforce max total bytes per file (remove oldest).
|
||||
// zh-CN: 3) 约束单文件历史总容量 (删除最旧).
|
||||
var totalBytes = (dao.sumBytesByFileId(fileId) ?: 0L).coerceAtLeast(0L)
|
||||
if (totalBytes > MAX_TOTAL_BYTES_PER_FILE) {
|
||||
for (rev in revisions.toList()) {
|
||||
if (totalBytes <= MAX_TOTAL_BYTES_PER_FILE) break
|
||||
deleteRevisionsAndBlobs(listOf(rev))
|
||||
totalBytes -= rev.sizeBytes.coerceAtLeast(0L)
|
||||
revisions.remove(rev)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteRevisionsAndBlobs(revs: List<HistoryEntities.Revision>) {
|
||||
if (revs.isEmpty()) return
|
||||
dao.deleteRevisionsByIds(revs.map { it.revId })
|
||||
revs.forEach { blobs.deleteBlobByRelPath(it.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()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_DAYS_MS: Long = 30L * 24L * 60L * 60L * 1000L
|
||||
private const val MAX_VERSIONS: Int = 50
|
||||
private const val MAX_TOTAL_BYTES_PER_FILE: Long = 200L * 1024L * 1024L
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package org.autojs.autojs.storage.history
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
/**
|
||||
* Created by JetBrains AI Assistant (GPT-5.2) on Feb 3, 2026.
|
||||
*/
|
||||
object HistoryUriUtils {
|
||||
|
||||
private const val INTERNAL_ROOT = "/storage/emulated/0"
|
||||
|
||||
/**
|
||||
* Resolve a logical path for history indexing.
|
||||
*
|
||||
* Strategy:
|
||||
* - Prefer extracting "/storage/emulated/0/..." from uri.toString() for third-party providers.
|
||||
* - Fallback to uri.path.
|
||||
* - Return null if not under internal storage root.
|
||||
*
|
||||
* zh-CN:
|
||||
*
|
||||
* 为历史索引解析 logical path.
|
||||
*
|
||||
* 策略:
|
||||
* - 对第三方 provider 优先从 uri.toString() 中提取 "/storage/emulated/0/..." 子串.
|
||||
* - 回退到 uri.path.
|
||||
* - 若不在内部存储根目录下则返回 null.
|
||||
*/
|
||||
fun toLogicalPathOrNull(uri: Uri): String? {
|
||||
val raw = uri.toString()
|
||||
val idx = raw.indexOf(INTERNAL_ROOT)
|
||||
val extracted = if (idx >= 0) {
|
||||
raw.substring(idx)
|
||||
} else {
|
||||
uri.path
|
||||
} ?: return null
|
||||
|
||||
val normalized = extracted.trimEnd('/')
|
||||
return if (normalized.startsWith(INTERNAL_ROOT)) normalized else null
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,16 @@ package org.autojs.autojs.ui.common;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.afollestad.materialdialogs.MaterialDialog;
|
||||
|
||||
import org.autojs.autojs.util.MD5Utils;
|
||||
import org.autojs.autojs6.R;
|
||||
|
||||
/**
|
||||
* Created by Stardust on Jan 30, 2017.
|
||||
* Modified by SuperMonster003 as of Feb 3, 2026.
|
||||
*/
|
||||
public class NotAskAgainDialog extends MaterialDialog {
|
||||
|
||||
@@ -38,6 +36,13 @@ public class NotAskAgainDialog extends MaterialDialog {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public MaterialDialog build() {
|
||||
return mRemind ? super.build() : null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public MaterialDialog show() {
|
||||
return mRemind ? super.show() : null;
|
||||
}
|
||||
|
||||
@@ -141,6 +141,10 @@ public class EditorMenu {
|
||||
showFileDetails();
|
||||
return true;
|
||||
}
|
||||
if (itemId == R.id.action_version_history) {
|
||||
showHistory();
|
||||
return true;
|
||||
}
|
||||
if (itemId == R.id.action_build_apk) {
|
||||
startBuildApkActivity();
|
||||
return true;
|
||||
@@ -148,6 +152,10 @@ public class EditorMenu {
|
||||
return false;
|
||||
}
|
||||
|
||||
private void showHistory() {
|
||||
mEditorView.showVersionHistoryDialog();
|
||||
}
|
||||
|
||||
private void importJavaPackageOrClass() {
|
||||
mEditor.getSelection()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
|
||||
@@ -44,10 +44,15 @@ import org.autojs.autojs.model.script.Scripts.EXTRA_EXCEPTION_LINE_NUMBER
|
||||
import org.autojs.autojs.model.script.Scripts.EXTRA_EXCEPTION_MESSAGE
|
||||
import org.autojs.autojs.model.script.Scripts.openByOtherApps
|
||||
import org.autojs.autojs.model.script.Scripts.runWithBroadcastSender
|
||||
import org.autojs.autojs.pio.PFiles
|
||||
import org.autojs.autojs.pio.PFiles.getNameWithoutExtension
|
||||
import org.autojs.autojs.pio.PFiles.write
|
||||
import org.autojs.autojs.storage.file.TmpScriptFiles
|
||||
import org.autojs.autojs.storage.history.HistoryDatabase
|
||||
import org.autojs.autojs.storage.history.HistoryRepository
|
||||
import org.autojs.autojs.storage.history.HistoryUriUtils
|
||||
import org.autojs.autojs.tool.Callback
|
||||
import org.autojs.autojs.ui.common.NotAskAgainDialog
|
||||
import org.autojs.autojs.ui.doc.ManualDialog
|
||||
import org.autojs.autojs.ui.edit.completion.CodeCompletionBar
|
||||
import org.autojs.autojs.ui.edit.completion.CodeCompletionBar.OnHintClickListener
|
||||
@@ -71,8 +76,10 @@ import org.autojs.autojs.util.ClipboardUtils
|
||||
import org.autojs.autojs.util.DisplayUtils.pxToSp
|
||||
import org.autojs.autojs.util.DocsUtils.getUrl
|
||||
import org.autojs.autojs.util.MaterialDialogUtils.choiceWidgetThemeColor
|
||||
import org.autojs.autojs.util.MaterialDialogUtils.widgetThemeColor
|
||||
import org.autojs.autojs.util.Observers
|
||||
import org.autojs.autojs.util.StringUtils
|
||||
import org.autojs.autojs.util.StringUtils.key
|
||||
import org.autojs.autojs.util.ViewUtils.showSnack
|
||||
import org.autojs.autojs.util.ViewUtils.showToast
|
||||
import org.autojs.autojs6.R
|
||||
@@ -82,6 +89,8 @@ 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
|
||||
|
||||
/**
|
||||
@@ -497,14 +506,19 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
|
||||
resolver.openInputStream(uri)?.use { it.readBytes() }
|
||||
}.getOrNull()
|
||||
|
||||
// Track history only when within size guardrails.
|
||||
// zh-CN: 仅在满足大小护栏时纳管历史.
|
||||
if (shouldTrackHistory(oldBytes, newBytes)) {
|
||||
// TODO Hook HistoryRepository here.
|
||||
// zh-CN: 在这里接入 HistoryRepository.
|
||||
//
|
||||
// Example:
|
||||
// historyRepo.recordPreSave(uri = uri, oldBytes = oldBytes, meta = ...)
|
||||
// Track history only when within size guardrails and inside internal storage.
|
||||
// zh-CN: 仅在满足大小护栏且位于内部存储时纳管历史.
|
||||
val logicalPath = HistoryUriUtils.toLogicalPathOrNull(uri)
|
||||
if (logicalPath != null && shouldTrackHistory(oldBytes, newBytes) && oldBytes != null) {
|
||||
runCatching {
|
||||
HistoryRepository(context.applicationContext).recordSavePre(
|
||||
uri = uri,
|
||||
logicalPath = logicalPath,
|
||||
oldBytes = oldBytes,
|
||||
encodingName = targetCharset.name(),
|
||||
hadBom = needBom,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val newHash = sha256(newBytes)
|
||||
@@ -574,7 +588,7 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
|
||||
|
||||
// Prompt user on main thread.
|
||||
// zh-CN: 在主线程提示用户.
|
||||
DialogUtils.buildAndShowAdaptive(
|
||||
DialogUtils.buildAndShowAdaptive {
|
||||
MaterialDialog.Builder(context)
|
||||
.title(R.string.error_save_failed)
|
||||
.content(
|
||||
@@ -658,7 +672,8 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
|
||||
}
|
||||
.autoDismiss(false)
|
||||
.cancelable(false)
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
if (error is IOException) {
|
||||
throw error
|
||||
@@ -701,6 +716,143 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
|
||||
return if (niceExt.isBlank()) sanitized else "$sanitized.$niceExt"
|
||||
}
|
||||
|
||||
fun showVersionHistoryDialog() {
|
||||
val logicalPath = HistoryUriUtils.toLogicalPathOrNull(uri)
|
||||
if (logicalPath == null) {
|
||||
// History is only tracked under internal storage root.
|
||||
// zh-CN: 历史记录仅纳管内部存储根目录下的文件.
|
||||
DialogUtils.buildAndShowAdaptive {
|
||||
MaterialDialog.Builder(context)
|
||||
.title(R.string.text_version_history)
|
||||
.content(R.string.text_no_version_history)
|
||||
.positiveText(R.string.dialog_button_dismiss)
|
||||
.positiveColorRes(R.color.dialog_button_default)
|
||||
.cancelable(false)
|
||||
.build()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Schedulers.io().scheduleDirect {
|
||||
runCatching {
|
||||
val appCtx = context.applicationContext
|
||||
val db = HistoryDatabase.getInstance(appCtx)
|
||||
val dao = db.historyDao()
|
||||
|
||||
val fileEntry = dao.findFileByPath(logicalPath)
|
||||
val fileId = fileEntry?.fileId
|
||||
|
||||
val revs = if (fileId != null) {
|
||||
// Show latest first for selection.
|
||||
// zh-CN: 列表按最新优先展示供选择.
|
||||
dao.listRevisionsAsc(fileId).asReversed().take(HISTORY_DIALOG_MAX_ITEMS)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
post {
|
||||
if (revs.isEmpty()) {
|
||||
DialogUtils.buildAndShowAdaptive {
|
||||
MaterialDialog.Builder(context)
|
||||
.title(R.string.text_version_history)
|
||||
.content(R.string.text_no_version_history)
|
||||
.positiveText(R.string.dialog_button_dismiss)
|
||||
.positiveColorRes(R.color.dialog_button_default)
|
||||
.cancelable(false)
|
||||
.build()
|
||||
}
|
||||
return@post
|
||||
}
|
||||
|
||||
val fmt = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
|
||||
val items = revs.map { rev ->
|
||||
val t = fmt.format(Date(rev.createdAt))
|
||||
val size = rev.sizeBytes
|
||||
"$t | ${PFiles.formatSizeWithUnit(size)}"
|
||||
}
|
||||
|
||||
val selectedIndex = intArrayOf(0)
|
||||
|
||||
DialogUtils.buildAndShowAdaptive {
|
||||
MaterialDialog.Builder(context)
|
||||
.title(R.string.dialog_button_history)
|
||||
.items(items)
|
||||
.itemsCallbackSingleChoice(0) { _, _, which, _ ->
|
||||
selectedIndex[0] = which
|
||||
true
|
||||
}
|
||||
.negativeText(R.string.dialog_button_cancel)
|
||||
.negativeColorRes(R.color.dialog_button_default)
|
||||
.positiveText(R.string.dialog_button_retrieve)
|
||||
.positiveColorRes(R.color.dialog_button_attraction)
|
||||
.onPositive { _, _ ->
|
||||
val chosen = revs.getOrNull(selectedIndex[0]) ?: return@onPositive
|
||||
|
||||
// Confirm before restoring to editor.
|
||||
// zh-CN: 恢复到编辑器前先做二次确认.
|
||||
showRestoreConfirmDialog() {
|
||||
// Load blob and restore into editor (not auto-save).
|
||||
// zh-CN: 读取 blob 并恢复到编辑器 (不自动保存).
|
||||
Schedulers.io().scheduleDirect {
|
||||
runCatching {
|
||||
val bytes = HistoryRepository(appCtx).readRevisionBytes(chosen)
|
||||
val restored = decodeRevisionBytes(
|
||||
bytes = bytes,
|
||||
encodingName = chosen.encoding,
|
||||
hadBom = chosen.hadBom,
|
||||
)
|
||||
post {
|
||||
editor.text = restored
|
||||
setMenuItemStatus(R.id.save, true)
|
||||
showSnack(this@EditorView, R.string.text_done)
|
||||
}
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
post { showToast(context, it.message, true) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.cancelable(true)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
post { showToast(context, it.message, true) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showRestoreConfirmDialog(onConfirm: () -> Unit) {
|
||||
DialogUtils.buildAndShowAdaptiveOrNull {
|
||||
NotAskAgainDialog.Builder(
|
||||
context,
|
||||
key(R.string.key_version_history_restore_does_not_auto_save_to_disk)
|
||||
).apply {
|
||||
title(R.string.text_prompt)
|
||||
content(R.string.text_version_history_restore_does_not_auto_save_to_disk)
|
||||
widgetThemeColor()
|
||||
negativeText(R.string.dialog_button_cancel)
|
||||
negativeColorRes(R.color.dialog_button_default)
|
||||
positiveText(R.string.dialog_button_confirm)
|
||||
positiveColorRes(R.color.dialog_button_attraction)
|
||||
onPositive { _, _ -> onConfirm() }
|
||||
cancelable(false)
|
||||
}.build()
|
||||
} ?: onConfirm()
|
||||
}
|
||||
|
||||
private fun decodeRevisionBytes(bytes: ByteArray, encodingName: String, hadBom: Boolean): String {
|
||||
val charset = runCatching { Charset.forName(encodingName) }.getOrElse { DEFAULT_CHARSET_TO_WRITE_FILE }
|
||||
val effective = if (hadBom) {
|
||||
// Drop BOM before decoding because BOM presence is tracked by metadata.
|
||||
// zh-CN: BOM 是否存在由元数据记录, 解码前需丢弃 BOM.
|
||||
StringUtils.dropBom(bytes, charset)
|
||||
} else bytes
|
||||
return String(effective, charset)
|
||||
}
|
||||
|
||||
// A minimal emergency draft store.
|
||||
// zh-CN: 一个最小实现的草稿存储器.
|
||||
private class EmergencyDraftStore(private val context: Context) {
|
||||
@@ -1059,6 +1211,10 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
|
||||
// zh-CN: 草稿总容量上限 (200MB).
|
||||
private const val DRAFT_MAX_TOTAL_BYTES: Long = 200L * 1024L * 1024L
|
||||
|
||||
// Max items shown in history dialog.
|
||||
// zh-CN: 历史对话框最多展示条数.
|
||||
private const val HISTORY_DIALOG_MAX_ITEMS: Int = 20
|
||||
|
||||
// Internal storage root (no external SD).
|
||||
// zh-CN: 内部存储根目录 (不访问外置 SD).
|
||||
private const val INTERNAL_STORAGE_ROOT: String = "/storage/emulated/0"
|
||||
|
||||
@@ -2,16 +2,13 @@
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_force_stop"
|
||||
android:title="@string/text_force_stop"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item android:title="@string/text_edit">
|
||||
|
||||
<menu>
|
||||
|
||||
<item
|
||||
android:id="@+id/action_find_or_replace"
|
||||
android:title="@string/text_find_or_replace"
|
||||
@@ -51,17 +48,13 @@
|
||||
android:id="@+id/action_beautify"
|
||||
android:title="@string/text_code_beautify"
|
||||
app:showAsAction="never" />
|
||||
|
||||
</menu>
|
||||
|
||||
</item>
|
||||
|
||||
<item
|
||||
android:title="@string/text_jump"
|
||||
app:showAsAction="never">
|
||||
|
||||
<menu>
|
||||
|
||||
<item
|
||||
android:id="@+id/action_jump_to_line"
|
||||
android:title="@string/text_jump_to_line"
|
||||
@@ -86,17 +79,13 @@
|
||||
android:id="@+id/action_jump_to_line_end"
|
||||
android:title="@string/text_jump_to_line_end"
|
||||
app:showAsAction="never" />
|
||||
|
||||
</menu>
|
||||
|
||||
</item>
|
||||
|
||||
<item
|
||||
android:title="@string/text_debug"
|
||||
app:showAsAction="never">
|
||||
|
||||
<menu>
|
||||
|
||||
<item
|
||||
android:id="@+id/action_breakpoint"
|
||||
android:title="@string/text_set_breakpoint"
|
||||
@@ -111,9 +100,7 @@
|
||||
android:id="@+id/action_remove_all_breakpoints"
|
||||
android:title="@string/text_remove_all_breakpoints"
|
||||
app:showAsAction="never" />
|
||||
|
||||
</menu>
|
||||
|
||||
</item>
|
||||
|
||||
<item
|
||||
@@ -127,9 +114,7 @@
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item android:title="@string/text_more">
|
||||
|
||||
<menu>
|
||||
|
||||
<item
|
||||
android:id="@+id/action_console"
|
||||
android:title="@string/text_console"
|
||||
@@ -145,6 +130,11 @@
|
||||
android:title="@string/text_file_details"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_version_history"
|
||||
android:title="@string/text_version_history"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_editor_text_size"
|
||||
android:title="@string/text_text_size"
|
||||
@@ -156,16 +146,12 @@
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item android:title="@string/text_fx_keyboard">
|
||||
|
||||
<menu>
|
||||
|
||||
<item
|
||||
android:id="@+id/action_editor_fx_symbols_settings"
|
||||
android:title="@string/text_symbols_settings"
|
||||
app:showAsAction="never" />
|
||||
|
||||
</menu>
|
||||
|
||||
</item>
|
||||
|
||||
<item
|
||||
@@ -177,9 +163,6 @@
|
||||
android:id="@+id/action_open_by_other_apps"
|
||||
android:title="@string/text_open_by_other_apps"
|
||||
app:showAsAction="never" />
|
||||
|
||||
</menu>
|
||||
|
||||
</item>
|
||||
|
||||
</menu>
|
||||
@@ -1230,4 +1230,7 @@
|
||||
<string name="text_draft_file_path">مسار المسودة</string>
|
||||
<string name="dialog_button_copy_path">نسخ</string>
|
||||
<string name="dialog_button_save_as">حفظ باسم</string>
|
||||
<string name="text_version_history">سجل الاصدارات</string>
|
||||
<string name="text_no_version_history">لا يوجد سجل للاصدارات</string>
|
||||
<string name="text_version_history_restore_does_not_auto_save_to_disk">عند الاستعادة، يتم تغيير محتوى المحرر فقط. لن يتم الحفظ تلقائيا في الملف. يرجى الضغط على زر \"حفظ\" يدويا.</string>
|
||||
</resources>
|
||||
@@ -1225,4 +1225,7 @@
|
||||
<string name="text_draft_file_path">Draft file path</string>
|
||||
<string name="dialog_button_copy_path">Copy path</string>
|
||||
<string name="dialog_button_save_as">Save as</string>
|
||||
<string name="text_version_history">Version history</string>
|
||||
<string name="text_no_version_history">No version history</string>
|
||||
<string name="text_version_history_restore_does_not_auto_save_to_disk">Restoring only changes the editor content. It will not automatically save to the file. Please tap the Save button manually.</string>
|
||||
</resources>
|
||||
@@ -1228,4 +1228,7 @@
|
||||
<string name="text_draft_file_path">Ruta del borrador</string>
|
||||
<string name="dialog_button_copy_path">Copiar</string>
|
||||
<string name="dialog_button_save_as">Guardar como</string>
|
||||
<string name="text_version_history">Historial de versiones</string>
|
||||
<string name="text_no_version_history">Sin historial de versiones</string>
|
||||
<string name="text_version_history_restore_does_not_auto_save_to_disk">Al restaurar, solo se cambia el contenido del editor. No se guardara automaticamente en el archivo. Debes pulsar el boton Guardar manualmente.</string>
|
||||
</resources>
|
||||
@@ -1228,4 +1228,7 @@
|
||||
<string name="text_draft_file_path">Chemin du brouillon</string>
|
||||
<string name="dialog_button_copy_path">Copier</string>
|
||||
<string name="dialog_button_save_as">Enregistrer sous</string>
|
||||
<string name="text_version_history">Historique des versions</string>
|
||||
<string name="text_no_version_history">Aucun historique des versions</string>
|
||||
<string name="text_version_history_restore_does_not_auto_save_to_disk">La restauration ne modifie que le contenu de l\'editeur. Elle n\'enregistre pas automatiquement dans le fichier. Veuillez appuyer manuellement sur le bouton Enregistrer.</string>
|
||||
</resources>
|
||||
@@ -1229,4 +1229,7 @@
|
||||
<string name="text_draft_file_path">下書きのパス</string>
|
||||
<string name="dialog_button_copy_path">コピー</string>
|
||||
<string name="dialog_button_save_as">名前を付けて保存</string>
|
||||
<string name="text_version_history">バージョン履歴</string>
|
||||
<string name="text_no_version_history">バージョン履歴なし</string>
|
||||
<string name="text_version_history_restore_does_not_auto_save_to_disk">復元すると編集内容のみが変更されます. ファイルには自動保存されません. 保存ボタンを手動でタップしてください.</string>
|
||||
</resources>
|
||||
@@ -1230,4 +1230,7 @@
|
||||
<string name="text_draft_file_path">초안 경로</string>
|
||||
<string name="dialog_button_copy_path">복사</string>
|
||||
<string name="dialog_button_save_as">다른 이름으로 저장</string>
|
||||
<string name="text_version_history">버전 기록</string>
|
||||
<string name="text_no_version_history">버전 기록 없음</string>
|
||||
<string name="text_version_history_restore_does_not_auto_save_to_disk">복원하면 편집기 내용만 변경됩니다. 파일에 자동으로 저장되지 않습니다. 저장 버튼을 수동으로 눌러 주세요.</string>
|
||||
</resources>
|
||||
@@ -1228,4 +1228,7 @@
|
||||
<string name="text_draft_file_path">Путь к черновику</string>
|
||||
<string name="dialog_button_copy_path">Копировать</string>
|
||||
<string name="dialog_button_save_as">Сохранить как</string>
|
||||
<string name="text_version_history">История версий</string>
|
||||
<string name="text_no_version_history">Нет истории версий</string>
|
||||
<string name="text_version_history_restore_does_not_auto_save_to_disk">Восстановление изменяет только содержимое редактора. Файл не будет сохранен автоматически. Нажмите кнопку \"Сохранить\" вручную.</string>
|
||||
</resources>
|
||||
@@ -1226,4 +1226,7 @@
|
||||
<string name="text_draft_file_path">草稿文件路徑</string>
|
||||
<string name="dialog_button_copy_path">複製路徑</string>
|
||||
<string name="dialog_button_save_as">另存為</string>
|
||||
<string name="text_version_history">版本歷史</string>
|
||||
<string name="text_no_version_history">無版本歷史</string>
|
||||
<string name="text_version_history_restore_does_not_auto_save_to_disk">恢復時只修改編輯器內容, 不會自動保存到文件, 需要手動點擊保存按鈕.</string>
|
||||
</resources>
|
||||
@@ -1226,4 +1226,7 @@
|
||||
<string name="text_draft_file_path">草稿檔案路徑</string>
|
||||
<string name="dialog_button_copy_path">複製路徑</string>
|
||||
<string name="dialog_button_save_as">另存為</string>
|
||||
<string name="text_version_history">版本歷史</string>
|
||||
<string name="text_no_version_history">無版本歷史</string>
|
||||
<string name="text_version_history_restore_does_not_auto_save_to_disk">恢復時只修改編輯器內容, 不會自動儲存到檔案, 需要手動點選儲存按鈕.</string>
|
||||
</resources>
|
||||
@@ -1226,4 +1226,7 @@
|
||||
<string name="text_draft_file_path">草稿文件路径</string>
|
||||
<string name="dialog_button_copy_path">复制路径</string>
|
||||
<string name="dialog_button_save_as">另存为</string>
|
||||
<string name="text_version_history">版本历史</string>
|
||||
<string name="text_no_version_history">无版本历史</string>
|
||||
<string name="text_version_history_restore_does_not_auto_save_to_disk">恢复时只修改编辑器内容, 不会自动保存到文件, 需要手动点击保存按钮.</string>
|
||||
</resources>
|
||||
@@ -175,6 +175,7 @@
|
||||
<string name="key_use_volume_control_record" translatable="false">key_$_use_volume_control_record</string>
|
||||
<string name="key_use_volume_control_running" translatable="false">key_$_use_volume_control_running</string>
|
||||
<string name="key_version_histories" translatable="false">key_$_version_histories</string>
|
||||
<string name="key_version_history_restore_does_not_auto_save_to_disk" translatable="false">key_$_version_history_restore_does_not_auto_save_to_disk</string>
|
||||
<string name="key_working_directory" translatable="false">key_$_working_directory</string>
|
||||
<string name="key_working_directory_histories" translatable="false">key_$_working_directory_histories</string>
|
||||
<string name="key_working_directory_initialized" translatable="false">key_$_working_directory_initialized</string>
|
||||
@@ -1486,4 +1487,7 @@
|
||||
<string name="text_draft_file_path">Draft file path</string>
|
||||
<string name="dialog_button_copy_path">Copy path</string>
|
||||
<string name="dialog_button_save_as">Save as</string>
|
||||
<string name="text_version_history">Version history</string>
|
||||
<string name="text_no_version_history">No version history</string>
|
||||
<string name="text_version_history_restore_does_not_auto_save_to_disk">Restore will only change editor content; it will not save to file automatically.</string>
|
||||
</resources>
|
||||
@@ -1,5 +1,5 @@
|
||||
#Tue Feb 03 15:47:53 CST 2026
|
||||
BUILD_TIME=1770104873073
|
||||
#Tue Feb 03 21:34:34 CST 2026
|
||||
BUILD_TIME=1770125674544
|
||||
COMPILE_SDK_VERSION=36
|
||||
IMAGE_QUANT_CMAKE_VERSION=3.22.1
|
||||
IMAGE_QUANT_NDK_VERSION=26.1.10909125
|
||||
@@ -27,6 +27,6 @@ RAPID_OCR_OPENCV_MOBILE_LABEL_VERSION=13
|
||||
RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3
|
||||
TARGET_SDK_VERSION=36
|
||||
TARGET_SDK_VERSION_INRT=29
|
||||
VERSION_BUILD=3689
|
||||
VERSION_BUILD=3691
|
||||
VERSION_NAME=6.7.0 Alpha19
|
||||
VSCODE_EXT_REQUIRED_VERSION=1.0.13
|
||||
|
||||
Reference in New Issue
Block a user