Skip to content

Commit c73e75b

Browse files
committed
Update
1 parent 0a85269 commit c73e75b

2 files changed

Lines changed: 117 additions & 22 deletions

File tree

app/src/main/java/com/tool/tree/TextEditorActivity.kt

Lines changed: 116 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ import com.tool.tree.ui.SyntaxHighlighterFactory
3535
import kotlinx.coroutines.Dispatchers
3636
import kotlinx.coroutines.launch
3737
import kotlinx.coroutines.withContext
38+
import org.json.JSONArray
39+
import org.json.JSONObject
3840
import java.io.File
41+
import java.security.MessageDigest
3942

4043
class TextEditorActivity : AppCompatActivity() {
4144

@@ -47,6 +50,8 @@ class TextEditorActivity : AppCompatActivity() {
4750
private const val EXTRA_DIR = "dir"
4851
private const val UNDO_HISTORY_LIMIT = 200
4952
private const val UNDO_DEBOUNCE_MS = 700L
53+
private const val UNDO_CACHE_DEBOUNCE_MS = 400L
54+
private const val UNDO_CACHE_DIR = "editor_undo_cache"
5055

5156
private const val EXTRA_PLACEHOLDER = "placeholder"
5257

@@ -56,12 +61,6 @@ class TextEditorActivity : AppCompatActivity() {
5661
"py" to "python"
5762
)
5863

59-
// Cho phép dòng đầu tiên của nội dung khai báo ngôn ngữ thực tế của file, ví dụ:
60-
// "# python", "#!/usr/bin/env python", hoặc shebang trỏ tới binary Termux
61-
// được bundle riêng trong thư mục dữ liệu của app (com.tool.tree), ví dụ:
62-
// #!/data/data/com.tool.tree/files/home/termux/bin/python
63-
// #!/data/data/com.tool.tree/files/home/bin/bash
64-
// (vẫn hỗ trợ cả dạng /data/user/<id>/... phòng khi thiết bị multi-user)
6564
private val FIRST_LINE_LANG_PATTERN = Regex(
6665
"""^#!?\s*(?:/usr/bin/env\s+)?(?:/data/(?:data|user/\d+)/com\.tool\.tree/\S*/)?(python3?|py|bash|sh|shell)\b""",
6766
RegexOption.IGNORE_CASE
@@ -120,6 +119,7 @@ class TextEditorActivity : AppCompatActivity() {
120119
private var redoButton: ImageButton? = null
121120
private val undoHandler = Handler(Looper.getMainLooper())
122121
private val commitPendingUndoRunnable = Runnable { commitPendingUndoSnapshot() }
122+
private val persistUndoCacheRunnable = Runnable { persistUndoCacheToDisk() }
123123

124124
override fun onCreate(savedInstanceState: Bundle?) {
125125
super.onCreate(savedInstanceState)
@@ -159,12 +159,107 @@ class TextEditorActivity : AppCompatActivity() {
159159
loadFileContent()
160160
}
161161

162+
override fun onPause() {
163+
commitPendingUndoSnapshot()
164+
persistUndoCacheToDisk()
165+
super.onPause()
166+
}
167+
162168
override fun onDestroy() {
163169
syntaxHighlighter?.detach()
164170
undoHandler.removeCallbacksAndMessages(null)
165171
super.onDestroy()
166172
}
167173

174+
// ---------------------------------------------------------------------
175+
// Cache undo/redo xuống đĩa: cho phép khôi phục lịch sử undo/redo nếu
176+
// Activity bị hệ thống hủy (xoay màn hình, thiếu RAM) hoặc người dùng
177+
// rời màn hình soạn thảo mà chưa lưu, rồi quay lại mở đúng file đó.
178+
// Cache chỉ được khôi phục khi nội dung file tại thời điểm mở lại trùng
179+
// khớp với nội dung đã có lúc ghi cache (tránh áp lịch sử undo sai lệch
180+
// lên một nội dung file đã thay đổi bởi nơi khác).
181+
// ---------------------------------------------------------------------
182+
183+
private fun undoCacheFile(): File {
184+
val digest = MessageDigest.getInstance("SHA-256")
185+
.digest(absoluteFilePath.toByteArray(Charsets.UTF_8))
186+
.joinToString("") { "%02x".format(it) }
187+
val dir = File(cacheDir, UNDO_CACHE_DIR).apply { mkdirs() }
188+
return File(dir, "$digest.json")
189+
}
190+
191+
private fun snapshotToJson(snapshot: EditorSnapshot) = JSONObject().apply {
192+
put("text", snapshot.text)
193+
put("cursor", snapshot.cursor)
194+
}
195+
196+
private fun jsonToSnapshot(json: JSONObject) =
197+
EditorSnapshot(json.optString("text", ""), json.optInt("cursor", 0))
198+
199+
private fun scheduleUndoCachePersist() {
200+
undoHandler.removeCallbacks(persistUndoCacheRunnable)
201+
undoHandler.postDelayed(persistUndoCacheRunnable, UNDO_CACHE_DEBOUNCE_MS)
202+
}
203+
204+
private fun persistUndoCacheToDisk() {
205+
undoHandler.removeCallbacks(persistUndoCacheRunnable)
206+
if (!::binding.isInitialized) return
207+
val baseContent = savedContent
208+
val undoSnapshot = undoStack.toList()
209+
val redoSnapshot = redoStack.toList()
210+
val pending = pendingUndoSnapshot
211+
lifecycleScope.launch(Dispatchers.IO) {
212+
try {
213+
if (undoSnapshot.isEmpty() && redoSnapshot.isEmpty() && pending == null) {
214+
undoCacheFile().delete()
215+
return@launch
216+
}
217+
val root = JSONObject().apply {
218+
put("baseContent", baseContent)
219+
put("undo", JSONArray().apply { undoSnapshot.forEach { put(snapshotToJson(it)) } })
220+
put("redo", JSONArray().apply { redoSnapshot.forEach { put(snapshotToJson(it)) } })
221+
pending?.let { put("pending", snapshotToJson(it)) }
222+
}
223+
undoCacheFile().writeText(root.toString())
224+
} catch (_: Exception) {
225+
}
226+
}
227+
}
228+
229+
/** Phải được gọi từ Main thread, sau khi [savedContent] đã được thiết lập. */
230+
private fun restoreUndoCacheFromDisk() {
231+
try {
232+
val file = undoCacheFile()
233+
if (!file.exists()) return
234+
val root = JSONObject(file.readText())
235+
val baseContent = if (root.has("baseContent")) root.getString("baseContent") else null
236+
if (baseContent == null || baseContent != savedContent) {
237+
file.delete()
238+
return
239+
}
240+
241+
undoStack.clear()
242+
redoStack.clear()
243+
root.optJSONArray("undo")?.let { arr ->
244+
for (i in 0 until arr.length()) undoStack.addLast(jsonToSnapshot(arr.getJSONObject(i)))
245+
}
246+
root.optJSONArray("redo")?.let { arr ->
247+
for (i in 0 until arr.length()) redoStack.addLast(jsonToSnapshot(arr.getJSONObject(i)))
248+
}
249+
pendingUndoSnapshot = root.optJSONObject("pending")?.let { jsonToSnapshot(it) }
250+
refreshUndoRedoButtons()
251+
} catch (_: Exception) {
252+
}
253+
}
254+
255+
private fun clearUndoCache() {
256+
undoHandler.removeCallbacks(persistUndoCacheRunnable)
257+
try {
258+
undoCacheFile().delete()
259+
} catch (_: Exception) {
260+
}
261+
}
262+
168263
private fun setupKeyboardInsets() {
169264
mainListBaseBottomMargin = (binding.mainList.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin
170265
val extraGapPx = (24 * resources.displayMetrics.density).toInt()
@@ -224,12 +319,6 @@ class TextEditorActivity : AppCompatActivity() {
224319

225320
private fun fileExtension() = File(absoluteFilePath).name.substringAfterLast('.', "").lowercase()
226321

227-
/**
228-
* Đọc dòng đầu tiên của nội dung đang soạn thảo để xem có khai báo ngôn ngữ
229-
* dạng "# python", "# bash", "#!/usr/bin/env python"... hay không. Nếu có,
230-
* ngôn ngữ này sẽ được ưu tiên hơn phần mở rộng file thực tế (vd: file.sh
231-
* nhưng dòng đầu ghi "# python" thì vẫn coi là python).
232-
*/
233322
private fun detectFirstLineLanguageOverride(): String? {
234323
val firstLine = binding.editorContent.text?.toString()
235324
?.lineSequence()?.firstOrNull()?.trim()
@@ -246,10 +335,6 @@ class TextEditorActivity : AppCompatActivity() {
246335
syntaxHighlighter?.detach()
247336
val override = detectFirstLineLanguageOverride()
248337
lastLanguageOverride = override
249-
// SyntaxHighlighterFactory chọn bộ tô màu dựa trên phần mở rộng của đường dẫn
250-
// truyền vào. Khi có override từ dòng đầu, ta đưa vào một "đường dẫn ảo" có
251-
// đúng phần mở rộng tương ứng để factory chọn đúng ngôn ngữ, còn việc đọc/ghi
252-
// file vẫn dùng absoluteFilePath thật như bình thường.
253338
val highlightPath = when (override) {
254339
"python" -> "$absoluteFilePath.__lang_override.py"
255340
"bash" -> "$absoluteFilePath.__lang_override.bash"
@@ -262,10 +347,6 @@ class TextEditorActivity : AppCompatActivity() {
262347
)?.apply { attach() }
263348
}
264349

265-
/**
266-
* Gọi lại khi nội dung dòng đầu tiên thay đổi trong lúc soạn thảo, để cập nhật
267-
* lại màu cú pháp và trạng thái nút "Run" (test) cho phù hợp ngôn ngữ mới.
268-
*/
269350
private fun refreshLanguageOverrideIfChanged() {
270351
val current = detectFirstLineLanguageOverride()
271352
if (current != lastLanguageOverride) {
@@ -344,6 +425,7 @@ class TextEditorActivity : AppCompatActivity() {
344425
undoHandler.postDelayed(commitPendingUndoRunnable, UNDO_DEBOUNCE_MS)
345426
refreshUndoRedoButtons()
346427
refreshLanguageOverrideIfChanged()
428+
scheduleUndoCachePersist()
347429
}
348430
})
349431
}
@@ -357,6 +439,7 @@ class TextEditorActivity : AppCompatActivity() {
357439
if (undoStack.size >= UNDO_HISTORY_LIMIT) undoStack.removeFirst()
358440
undoStack.addLast(snapshot)
359441
refreshUndoRedoButtons()
442+
scheduleUndoCachePersist()
360443
}
361444

362445
private inline fun withHighlightSuspended(block: () -> Unit) {
@@ -380,6 +463,7 @@ class TextEditorActivity : AppCompatActivity() {
380463
redoStack.addLast(EditorSnapshot(currentText, currentCursor))
381464
applyHistorySnapshot(target)
382465
refreshUndoRedoButtons()
466+
persistUndoCacheToDisk()
383467
}
384468

385469
private fun performRedo() {
@@ -395,6 +479,7 @@ class TextEditorActivity : AppCompatActivity() {
395479
undoStack.addLast(EditorSnapshot(currentText, currentCursor))
396480
applyHistorySnapshot(target)
397481
refreshUndoRedoButtons()
482+
persistUndoCacheToDisk()
398483
}
399484

400485
private fun applyHistorySnapshot(snapshot: EditorSnapshot) {
@@ -454,6 +539,10 @@ class TextEditorActivity : AppCompatActivity() {
454539
pendingUndoSnapshot = null
455540
undoStack.clear()
456541
redoStack.clear()
542+
543+
// Nếu có cache undo/redo được lưu từ phiên trước cho đúng nội dung
544+
// file này, khôi phục lại để người dùng có thể undo/redo tiếp.
545+
restoreUndoCacheFromDisk()
457546
refreshUndoRedoButtons()
458547

459548
binding.editorContent.hint = when {
@@ -518,6 +607,7 @@ class TextEditorActivity : AppCompatActivity() {
518607
commitPendingUndoSnapshot()
519608

520609
if (!hasUnsavedChanges()) {
610+
clearUndoCache()
521611
finish()
522612
return
523613
}
@@ -532,6 +622,7 @@ class TextEditorActivity : AppCompatActivity() {
532622
}
533623
}),
534624
onCancel = DialogHelper.DialogButton(getString(R.string.editor_discard), Runnable {
625+
clearUndoCache()
535626
finish()
536627
})
537628
)
@@ -584,6 +675,10 @@ class TextEditorActivity : AppCompatActivity() {
584675
isSaving = false
585676
if (success) {
586677
savedContent = content
678+
// Cập nhật lại "baseContent" của cache theo nội dung vừa lưu,
679+
// để lịch sử undo/redo hiện có vẫn còn dùng được nếu quay
680+
// lại chỉnh sửa tiếp mà chưa đóng màn hình.
681+
persistUndoCacheToDisk()
587682
}
588683
if (showToast) {
589684
Toast.makeText(
@@ -660,4 +755,4 @@ class TextEditorActivity : AppCompatActivity() {
660755
).apply { isCancelable = false }
661756
.show(supportFragmentManager, "editor-run-test")
662757
}
663-
}
758+
}

app/src/main/res/drawable/ic_wrap_text.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
android:viewportHeight="24">
66
<path
77
android:fillColor="#FF000000"
8-
android:pathData="M4,19h6v-2L4,17v2zM20,5L4,5v2h16L20,5zM4,13h12v-2L4,11v2zM17.75,11c0,-1.24 -1.01,-2.25 -2.25,-2.25L4,8.75v1.5h11.5c0.41,0 0.75,0.34 0.75,0.75s-0.34,0.75 -0.75,0.75L13,11.75V9.5l-3,3 3,3v-2.25h2.5c1.24,0 2.25,-1.01 2.25,-2.25z" />
8+
android:pathData="M588,828 L440,680l148,-148 56,58 -50,50h96q29,0 49.5,-20.5T760,570q0,-29 -20.5,-49.5T690,500L160,500v-80h530q63,0 106.5,43.5T840,570q0,63 -43.5,106.5T690,720h-96l50,50 -56,58ZM160,720v-80h200v80L160,720ZM160,280v-80h640v80L160,280Z" />
99
</vector>

0 commit comments

Comments
 (0)