6.7.0 - Alpha19 - 版本历史 M2 - 增加代码编辑器历史版本功能, 支持查看和恢复历史版本
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
"released_date": "2026/02/03",
|
"released_date": "2026/02/03",
|
||||||
"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))",
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import org.autojs.autojs.event.GlobalKeyObserver
|
|||||||
import org.autojs.autojs.external.receiver.DynamicBroadcastReceivers
|
import org.autojs.autojs.external.receiver.DynamicBroadcastReceivers
|
||||||
import org.autojs.autojs.ipc.InAppEventBus
|
import org.autojs.autojs.ipc.InAppEventBus
|
||||||
import org.autojs.autojs.leakcanary.LeakCanarySetup
|
import org.autojs.autojs.leakcanary.LeakCanarySetup
|
||||||
|
import org.autojs.autojs.storage.history.HistoryCleanupScheduler
|
||||||
import org.autojs.autojs.theme.ThemeColorManager
|
import org.autojs.autojs.theme.ThemeColorManager
|
||||||
import org.autojs.autojs.timing.TimedTaskManager
|
import org.autojs.autojs.timing.TimedTaskManager
|
||||||
import org.autojs.autojs.timing.TimedTaskScheduler
|
import org.autojs.autojs.timing.TimedTaskScheduler
|
||||||
@@ -39,7 +40,7 @@ import java.lang.reflect.Method
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by Stardust on Jan 27, 2017.
|
* 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() {
|
class App : MultiDexApplication() {
|
||||||
|
|
||||||
@@ -71,6 +72,8 @@ class App : MultiDexApplication() {
|
|||||||
|
|
||||||
ThemeColorManager.init()
|
ThemeColorManager.init()
|
||||||
setUpDefaultNightMode()
|
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.content.Context;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import com.afollestad.materialdialogs.MaterialDialog;
|
import com.afollestad.materialdialogs.MaterialDialog;
|
||||||
|
|
||||||
import org.autojs.autojs.util.MD5Utils;
|
import org.autojs.autojs.util.MD5Utils;
|
||||||
import org.autojs.autojs6.R;
|
import org.autojs.autojs6.R;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by Stardust on Jan 30, 2017.
|
* Created by Stardust on Jan 30, 2017.
|
||||||
|
* Modified by SuperMonster003 as of Feb 3, 2026.
|
||||||
*/
|
*/
|
||||||
public class NotAskAgainDialog extends MaterialDialog {
|
public class NotAskAgainDialog extends MaterialDialog {
|
||||||
|
|
||||||
@@ -38,6 +36,13 @@ public class NotAskAgainDialog extends MaterialDialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public MaterialDialog build() {
|
||||||
|
return mRemind ? super.build() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
public MaterialDialog show() {
|
public MaterialDialog show() {
|
||||||
return mRemind ? super.show() : null;
|
return mRemind ? super.show() : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,6 +141,10 @@ public class EditorMenu {
|
|||||||
showFileDetails();
|
showFileDetails();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (itemId == R.id.action_version_history) {
|
||||||
|
showHistory();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (itemId == R.id.action_build_apk) {
|
if (itemId == R.id.action_build_apk) {
|
||||||
startBuildApkActivity();
|
startBuildApkActivity();
|
||||||
return true;
|
return true;
|
||||||
@@ -148,6 +152,10 @@ public class EditorMenu {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void showHistory() {
|
||||||
|
mEditorView.showVersionHistoryDialog();
|
||||||
|
}
|
||||||
|
|
||||||
private void importJavaPackageOrClass() {
|
private void importJavaPackageOrClass() {
|
||||||
mEditor.getSelection()
|
mEditor.getSelection()
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.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.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.HistoryUriUtils
|
||||||
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
|
||||||
@@ -71,8 +76,10 @@ 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
|
||||||
@@ -82,6 +89,8 @@ 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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -497,14 +506,19 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
|
|||||||
resolver.openInputStream(uri)?.use { it.readBytes() }
|
resolver.openInputStream(uri)?.use { it.readBytes() }
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
|
|
||||||
// Track history only when within size guardrails.
|
// Track history only when within size guardrails and inside internal storage.
|
||||||
// zh-CN: 仅在满足大小护栏时纳管历史.
|
// zh-CN: 仅在满足大小护栏且位于内部存储时纳管历史.
|
||||||
if (shouldTrackHistory(oldBytes, newBytes)) {
|
val logicalPath = HistoryUriUtils.toLogicalPathOrNull(uri)
|
||||||
// TODO Hook HistoryRepository here.
|
if (logicalPath != null && shouldTrackHistory(oldBytes, newBytes) && oldBytes != null) {
|
||||||
// zh-CN: 在这里接入 HistoryRepository.
|
runCatching {
|
||||||
//
|
HistoryRepository(context.applicationContext).recordSavePre(
|
||||||
// Example:
|
uri = uri,
|
||||||
// historyRepo.recordPreSave(uri = uri, oldBytes = oldBytes, meta = ...)
|
logicalPath = logicalPath,
|
||||||
|
oldBytes = oldBytes,
|
||||||
|
encodingName = targetCharset.name(),
|
||||||
|
hadBom = needBom,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val newHash = sha256(newBytes)
|
val newHash = sha256(newBytes)
|
||||||
@@ -574,7 +588,7 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
|
|||||||
|
|
||||||
// Prompt user on main thread.
|
// Prompt user on main thread.
|
||||||
// zh-CN: 在主线程提示用户.
|
// zh-CN: 在主线程提示用户.
|
||||||
DialogUtils.buildAndShowAdaptive(
|
DialogUtils.buildAndShowAdaptive {
|
||||||
MaterialDialog.Builder(context)
|
MaterialDialog.Builder(context)
|
||||||
.title(R.string.error_save_failed)
|
.title(R.string.error_save_failed)
|
||||||
.content(
|
.content(
|
||||||
@@ -658,7 +672,8 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
|
|||||||
}
|
}
|
||||||
.autoDismiss(false)
|
.autoDismiss(false)
|
||||||
.cancelable(false)
|
.cancelable(false)
|
||||||
)
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
if (error is IOException) {
|
if (error is IOException) {
|
||||||
throw error
|
throw error
|
||||||
@@ -701,6 +716,143 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
|
|||||||
return if (niceExt.isBlank()) sanitized else "$sanitized.$niceExt"
|
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.
|
// A minimal emergency draft store.
|
||||||
// zh-CN: 一个最小实现的草稿存储器.
|
// zh-CN: 一个最小实现的草稿存储器.
|
||||||
private class EmergencyDraftStore(private val context: Context) {
|
private class EmergencyDraftStore(private val context: Context) {
|
||||||
@@ -1059,6 +1211,10 @@ 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"
|
||||||
|
|||||||
@@ -2,16 +2,13 @@
|
|||||||
<menu
|
<menu
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_force_stop"
|
android:id="@+id/action_force_stop"
|
||||||
android:title="@string/text_force_stop"
|
android:title="@string/text_force_stop"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
||||||
<item android:title="@string/text_edit">
|
<item android:title="@string/text_edit">
|
||||||
|
|
||||||
<menu>
|
<menu>
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_find_or_replace"
|
android:id="@+id/action_find_or_replace"
|
||||||
android:title="@string/text_find_or_replace"
|
android:title="@string/text_find_or_replace"
|
||||||
@@ -51,17 +48,13 @@
|
|||||||
android:id="@+id/action_beautify"
|
android:id="@+id/action_beautify"
|
||||||
android:title="@string/text_code_beautify"
|
android:title="@string/text_code_beautify"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
||||||
</menu>
|
</menu>
|
||||||
|
|
||||||
</item>
|
</item>
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:title="@string/text_jump"
|
android:title="@string/text_jump"
|
||||||
app:showAsAction="never">
|
app:showAsAction="never">
|
||||||
|
|
||||||
<menu>
|
<menu>
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_jump_to_line"
|
android:id="@+id/action_jump_to_line"
|
||||||
android:title="@string/text_jump_to_line"
|
android:title="@string/text_jump_to_line"
|
||||||
@@ -86,17 +79,13 @@
|
|||||||
android:id="@+id/action_jump_to_line_end"
|
android:id="@+id/action_jump_to_line_end"
|
||||||
android:title="@string/text_jump_to_line_end"
|
android:title="@string/text_jump_to_line_end"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
||||||
</menu>
|
</menu>
|
||||||
|
|
||||||
</item>
|
</item>
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:title="@string/text_debug"
|
android:title="@string/text_debug"
|
||||||
app:showAsAction="never">
|
app:showAsAction="never">
|
||||||
|
|
||||||
<menu>
|
<menu>
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_breakpoint"
|
android:id="@+id/action_breakpoint"
|
||||||
android:title="@string/text_set_breakpoint"
|
android:title="@string/text_set_breakpoint"
|
||||||
@@ -111,9 +100,7 @@
|
|||||||
android:id="@+id/action_remove_all_breakpoints"
|
android:id="@+id/action_remove_all_breakpoints"
|
||||||
android:title="@string/text_remove_all_breakpoints"
|
android:title="@string/text_remove_all_breakpoints"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
||||||
</menu>
|
</menu>
|
||||||
|
|
||||||
</item>
|
</item>
|
||||||
|
|
||||||
<item
|
<item
|
||||||
@@ -127,9 +114,7 @@
|
|||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
||||||
<item android:title="@string/text_more">
|
<item android:title="@string/text_more">
|
||||||
|
|
||||||
<menu>
|
<menu>
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_console"
|
android:id="@+id/action_console"
|
||||||
android:title="@string/text_console"
|
android:title="@string/text_console"
|
||||||
@@ -145,6 +130,11 @@
|
|||||||
android:title="@string/text_file_details"
|
android:title="@string/text_file_details"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_version_history"
|
||||||
|
android:title="@string/text_version_history"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_editor_text_size"
|
android:id="@+id/action_editor_text_size"
|
||||||
android:title="@string/text_text_size"
|
android:title="@string/text_text_size"
|
||||||
@@ -156,16 +146,12 @@
|
|||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
||||||
<item android:title="@string/text_fx_keyboard">
|
<item android:title="@string/text_fx_keyboard">
|
||||||
|
|
||||||
<menu>
|
<menu>
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_editor_fx_symbols_settings"
|
android:id="@+id/action_editor_fx_symbols_settings"
|
||||||
android:title="@string/text_symbols_settings"
|
android:title="@string/text_symbols_settings"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
||||||
</menu>
|
</menu>
|
||||||
|
|
||||||
</item>
|
</item>
|
||||||
|
|
||||||
<item
|
<item
|
||||||
@@ -177,9 +163,6 @@
|
|||||||
android:id="@+id/action_open_by_other_apps"
|
android:id="@+id/action_open_by_other_apps"
|
||||||
android:title="@string/text_open_by_other_apps"
|
android:title="@string/text_open_by_other_apps"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
||||||
</menu>
|
</menu>
|
||||||
|
|
||||||
</item>
|
</item>
|
||||||
|
|
||||||
</menu>
|
</menu>
|
||||||
@@ -1230,4 +1230,7 @@
|
|||||||
<string name="text_draft_file_path">مسار المسودة</string>
|
<string name="text_draft_file_path">مسار المسودة</string>
|
||||||
<string name="dialog_button_copy_path">نسخ</string>
|
<string name="dialog_button_copy_path">نسخ</string>
|
||||||
<string name="dialog_button_save_as">حفظ باسم</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>
|
</resources>
|
||||||
@@ -1225,4 +1225,7 @@
|
|||||||
<string name="text_draft_file_path">Draft file path</string>
|
<string name="text_draft_file_path">Draft file path</string>
|
||||||
<string name="dialog_button_copy_path">Copy path</string>
|
<string name="dialog_button_copy_path">Copy path</string>
|
||||||
<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_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>
|
</resources>
|
||||||
@@ -1228,4 +1228,7 @@
|
|||||||
<string name="text_draft_file_path">Ruta del borrador</string>
|
<string name="text_draft_file_path">Ruta del borrador</string>
|
||||||
<string name="dialog_button_copy_path">Copiar</string>
|
<string name="dialog_button_copy_path">Copiar</string>
|
||||||
<string name="dialog_button_save_as">Guardar como</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>
|
</resources>
|
||||||
@@ -1228,4 +1228,7 @@
|
|||||||
<string name="text_draft_file_path">Chemin du brouillon</string>
|
<string name="text_draft_file_path">Chemin du brouillon</string>
|
||||||
<string name="dialog_button_copy_path">Copier</string>
|
<string name="dialog_button_copy_path">Copier</string>
|
||||||
<string name="dialog_button_save_as">Enregistrer sous</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>
|
</resources>
|
||||||
@@ -1229,4 +1229,7 @@
|
|||||||
<string name="text_draft_file_path">下書きのパス</string>
|
<string name="text_draft_file_path">下書きのパス</string>
|
||||||
<string name="dialog_button_copy_path">コピー</string>
|
<string name="dialog_button_copy_path">コピー</string>
|
||||||
<string name="dialog_button_save_as">名前を付けて保存</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>
|
</resources>
|
||||||
@@ -1230,4 +1230,7 @@
|
|||||||
<string name="text_draft_file_path">초안 경로</string>
|
<string name="text_draft_file_path">초안 경로</string>
|
||||||
<string name="dialog_button_copy_path">복사</string>
|
<string name="dialog_button_copy_path">복사</string>
|
||||||
<string name="dialog_button_save_as">다른 이름으로 저장</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>
|
</resources>
|
||||||
@@ -1228,4 +1228,7 @@
|
|||||||
<string name="text_draft_file_path">Путь к черновику</string>
|
<string name="text_draft_file_path">Путь к черновику</string>
|
||||||
<string name="dialog_button_copy_path">Копировать</string>
|
<string name="dialog_button_copy_path">Копировать</string>
|
||||||
<string name="dialog_button_save_as">Сохранить как</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>
|
</resources>
|
||||||
@@ -1226,4 +1226,7 @@
|
|||||||
<string name="text_draft_file_path">草稿文件路徑</string>
|
<string name="text_draft_file_path">草稿文件路徑</string>
|
||||||
<string name="dialog_button_copy_path">複製路徑</string>
|
<string name="dialog_button_copy_path">複製路徑</string>
|
||||||
<string name="dialog_button_save_as">另存為</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>
|
</resources>
|
||||||
@@ -1226,4 +1226,7 @@
|
|||||||
<string name="text_draft_file_path">草稿檔案路徑</string>
|
<string name="text_draft_file_path">草稿檔案路徑</string>
|
||||||
<string name="dialog_button_copy_path">複製路徑</string>
|
<string name="dialog_button_copy_path">複製路徑</string>
|
||||||
<string name="dialog_button_save_as">另存為</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>
|
</resources>
|
||||||
@@ -1226,4 +1226,7 @@
|
|||||||
<string name="text_draft_file_path">草稿文件路径</string>
|
<string name="text_draft_file_path">草稿文件路径</string>
|
||||||
<string name="dialog_button_copy_path">复制路径</string>
|
<string name="dialog_button_copy_path">复制路径</string>
|
||||||
<string name="dialog_button_save_as">另存为</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>
|
</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_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_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_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" translatable="false">key_$_working_directory</string>
|
||||||
<string name="key_working_directory_histories" translatable="false">key_$_working_directory_histories</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>
|
<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="text_draft_file_path">Draft file path</string>
|
||||||
<string name="dialog_button_copy_path">Copy path</string>
|
<string name="dialog_button_copy_path">Copy path</string>
|
||||||
<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_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>
|
</resources>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#Tue Feb 03 15:47:53 CST 2026
|
#Tue Feb 03 21:34:34 CST 2026
|
||||||
BUILD_TIME=1770104873073
|
BUILD_TIME=1770125674544
|
||||||
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=3689
|
VERSION_BUILD=3691
|
||||||
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