6.7.0 - Alpha19 - 版本历史 M2 - 增加代码编辑器历史版本功能, 支持查看和恢复历史版本

This commit is contained in:
SuperMonster003
2026-02-03 23:55:14 +08:00
parent 6d964b7949
commit 275f393f5b
26 changed files with 784 additions and 39 deletions

View File

@@ -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))",

View File

@@ -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)
}
}
}

View File

@@ -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()
}

View File

@@ -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,
)
}
}

View File

@@ -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
}
}

View File

@@ -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>
}

View File

@@ -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 }
}
}
}

View File

@@ -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,
)
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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;
}

View File

@@ -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())

View File

@@ -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"

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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