feat: add explicit Root Mode button, restore backup dialog, and fix Beta category parity

This commit is contained in:
CAtlas0784 committed 2026-10-07 23:54:00 +07:00
1 parent e7ca8d26af
commit fefa57678d
4 files changed
+291 -89

No files matched your search

@@ -14,10 +14,28 @@ object SceneRepository {
suspend fun loadScenes(context: Context): List<PlanetItem> = withContext(Dispatchers.IO) { suspend fun loadScenes(context: Context): List<PlanetItem> = withContext(Dispatchers.IO) {
val cachedFile = File(context.filesDir, CACHED_SCENES_FILENAME) val cachedFile = File(context.filesDir, CACHED_SCENES_FILENAME)
val defaultJson = context.assets.open("scenes.json").bufferedReader().use { it.readText() }
val jsonContent = if (cachedFile.exists()) { val jsonContent = if (cachedFile.exists()) {
cachedFile.readText() try {
val cachedText = cachedFile.readText()
val cachedObj = JSONObject(cachedText)
val defaultObj = JSONObject(defaultJson)
val defaultBeta = defaultObj.optInt("total_beta_scenes", 0)
val cachedBeta = cachedObj.optInt("total_beta_scenes", 0)
// Discard stale cache if bundled asset has more beta scene definitions
if (defaultBeta > cachedBeta) {
cachedFile.delete()
defaultJson
} else {
cachedText
}
} catch (e: Exception) {
cachedFile.delete()
defaultJson
}
} else { } else {
context.assets.open("scenes.json").bufferedReader().use { it.readText() } defaultJson
} }
parseJson(jsonContent) parseJson(jsonContent)
} }
@@ -81,7 +99,10 @@ object SceneRepository {
val calyxPropId = sObj.optLong("calyx_prop_id", 808) val calyxPropId = sObj.optLong("calyx_prop_id", 808)
val combatReady = sObj.optBoolean("calyx_supported", false) val combatReady = sObj.optBoolean("calyx_supported", false)
val isBeta = sObj.optBoolean("is_beta", false) val isBeta = sObj.optBoolean("is_beta", false) ||
sObj.optString("channel", "").equals("beta", ignoreCase = true) ||
sObj.optString("client_version", "").contains("Beta", ignoreCase = true) ||
planeId == 20571L || planeId == 20572L
val descEn = sObj.optString("desc", "") val descEn = sObj.optString("desc", "")
val descTh = sObj.optString("desc_th", descEn) val descTh = sObj.optString("desc_th", descEn)
@@ -76,6 +76,38 @@ object StorageManager {
} }
} }
suspend fun connectRootMode(): Result<String> = withContext(Dispatchers.IO) {
try {
val hasRoot = checkRootAccess()
if (!hasRoot) {
return@withContext Result.failure(Exception("Root permission denied / กรุณากดอนุญาต Superuser ใน Magisk/KernelSU"))
}
// Ensure ppSR directories exist
val dirCmd = "mkdir -p \"/data/data/$PPSR_PACKAGE_NAME/files/server/build/build/misc\""
Runtime.getRuntime().exec(arrayOf("su", "-c", dirCmd)).waitFor()
// Check if persistent.json exists; create default template if not found
val checkCmd = "[ -f \"$PPSR_INTERNAL_PERSISTENT_PATH\" ] && echo 'EXISTS'"
val p = Runtime.getRuntime().exec(arrayOf("su", "-c", checkCmd))
val output = p.inputStream.bufferedReader().use { it.readText().trim() }
p.waitFor()
if (output != "EXISTS") {
val initJson = "{\n \"scene\": {\n \"plane_id\": 20001,\n \"floor_id\": 20001001,\n \"entry_id\": 2000101,\n \"pos_x\": 0,\n \"pos_y\": 0,\n \"pos_z\": 0\n }\n}"
val createCmd = "echo '$initJson' > \"$PPSR_INTERNAL_PERSISTENT_PATH\" && chmod 666 \"$PPSR_INTERNAL_PERSISTENT_PATH\""
Runtime.getRuntime().exec(arrayOf("su", "-c", createCmd)).waitFor()
} else {
val permCmd = "chmod 666 \"$PPSR_INTERNAL_PERSISTENT_PATH\""
Runtime.getRuntime().exec(arrayOf("su", "-c", permCmd)).waitFor()
}
Result.success(PPSR_INTERNAL_PERSISTENT_PATH)
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun readConfigViaRoot(): SceneConfig? = withContext(Dispatchers.IO) { suspend fun readConfigViaRoot(): SceneConfig? = withContext(Dispatchers.IO) {
try { try {
val cmd = "cat \"$PPSR_INTERNAL_PERSISTENT_PATH\"" val cmd = "cat \"$PPSR_INTERNAL_PERSISTENT_PATH\""
@@ -96,13 +128,17 @@ object StorageManager {
suspend fun writeConfigViaRoot(context: Context, newConfig: SceneConfig): Result<Unit> = suspend fun writeConfigViaRoot(context: Context, newConfig: SceneConfig): Result<Unit> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
try { try {
// Ensure directory exists
val dirCmd = "mkdir -p \"/data/data/$PPSR_PACKAGE_NAME/files/server/build/build/misc\""
Runtime.getRuntime().exec(arrayOf("su", "-c", dirCmd)).waitFor()
// Read original content to preserve avatar, key, send_hotfix, etc. // Read original content to preserve avatar, key, send_hotfix, etc.
val readCmd = "cat \"$PPSR_INTERNAL_PERSISTENT_PATH\"" val readCmd = "cat \"$PPSR_INTERNAL_PERSISTENT_PATH\""
val readProcess = Runtime.getRuntime().exec(arrayOf("su", "-c", readCmd)) val readProcess = Runtime.getRuntime().exec(arrayOf("su", "-c", readCmd))
val originalContent = readProcess.inputStream.bufferedReader().use { it.readText().trim() } val originalContent = readProcess.inputStream.bufferedReader().use { it.readText().trim() }
readProcess.waitFor() readProcess.waitFor()
// Save backup both inside ppSR folder and in local app cache // Save backup both inside ppSR folder (.bak) and in local app cache
if (originalContent.isNotEmpty()) { if (originalContent.isNotEmpty()) {
try { try {
val bakCmd = "cp \"$PPSR_INTERNAL_PERSISTENT_PATH\" \"$PPSR_INTERNAL_BACKUP_PATH\"" val bakCmd = "cp \"$PPSR_INTERNAL_PERSISTENT_PATH\" \"$PPSR_INTERNAL_BACKUP_PATH\""
@@ -30,6 +30,8 @@ object UpdateManager {
"https://api.github.com/repos/CAtlas0784/Escape-penacony/releases/latest" "https://api.github.com/repos/CAtlas0784/Escape-penacony/releases/latest"
private const val GITHUB_RAW_SCENES_URL = private const val GITHUB_RAW_SCENES_URL =
"https://raw.githubusercontent.com/CAtlas0784/Escape-penacony/testing/scenes.json"
private const val GITHUB_RAW_SCENES_FALLBACK =
"https://raw.githubusercontent.com/CAtlas0784/Escape-penacony/main/scenes.json" "https://raw.githubusercontent.com/CAtlas0784/Escape-penacony/main/scenes.json"
suspend fun checkForAppUpdate(currentVersion: String = BuildConfig.VERSION_NAME): Result<ReleaseInfo?> = suspend fun checkForAppUpdate(currentVersion: String = BuildConfig.VERSION_NAME): Result<ReleaseInfo?> =
@@ -151,12 +153,20 @@ object UpdateManager {
suspend fun fetchLatestScenesJson(): Result<String> = withContext(Dispatchers.IO) { suspend fun fetchLatestScenesJson(): Result<String> = withContext(Dispatchers.IO) {
try { try {
val url = URL(GITHUB_RAW_SCENES_URL) val primaryUrl = URL(GITHUB_RAW_SCENES_URL)
val conn = url.openConnection() as HttpURLConnection var conn = primaryUrl.openConnection() as HttpURLConnection
conn.connectTimeout = 8000 conn.connectTimeout = 8000
conn.readTimeout = 8000 conn.readTimeout = 8000
conn.setRequestProperty("User-Agent", "Escape-Penacony-Android-App") conn.setRequestProperty("User-Agent", "Escape-Penacony-Android-App")
if (conn.responseCode != 200) {
val fallbackUrl = URL(GITHUB_RAW_SCENES_FALLBACK)
conn = fallbackUrl.openConnection() as HttpURLConnection
conn.connectTimeout = 8000
conn.readTimeout = 8000
conn.setRequestProperty("User-Agent", "Escape-Penacony-Android-App")
}
if (conn.responseCode == 200) { if (conn.responseCode == 200) {
val content = conn.inputStream.bufferedReader().use { it.readText() } val content = conn.inputStream.bufferedReader().use { it.readText() }
Result.success(content) Result.success(content)
@@ -55,6 +55,8 @@ fun MainScreen(
var isRootMode by remember { mutableStateOf(false) } var isRootMode by remember { mutableStateOf(false) }
var currentActiveConfig by remember { mutableStateOf<SceneConfig?>(null) } var currentActiveConfig by remember { mutableStateOf<SceneConfig?>(null) }
var showCustomDialog by remember { mutableStateOf(false) } var showCustomDialog by remember { mutableStateOf(false) }
var showRestoreDialog by remember { mutableStateOf(false) }
var isConnectingRoot by remember { mutableStateOf(false) }
// Update States // Update States
var showUpdateDialog by remember { mutableStateOf(false) } var showUpdateDialog by remember { mutableStateOf(false) }
@@ -69,7 +71,7 @@ fun MainScreen(
language = AppLanguage.values().firstOrNull { it.code == savedLang } ?: AppLanguage.EN language = AppLanguage.values().firstOrNull { it.code == savedLang } ?: AppLanguage.EN
planets = SceneRepository.loadScenes(context) planets = SceneRepository.loadScenes(context)
// Check Root Mode // Check Root Mode silently if su permission already granted
val hasRoot = StorageManager.checkRootAccess() val hasRoot = StorageManager.checkRootAccess()
val hasPpsr = if (hasRoot) StorageManager.checkPpsrInternalFileExists() else false val hasPpsr = if (hasRoot) StorageManager.checkPpsrInternalFileExists() else false
isRootMode = hasRoot && hasPpsr isRootMode = hasRoot && hasPpsr
@@ -108,7 +110,13 @@ fun MainScreen(
// Filter scenes // Filter scenes
// When BETA category is selected, search globally across all planets so Starforster & Fleeting Joy always appear! // When BETA category is selected, search globally across all planets so Starforster & Fleeting Joy always appear!
val displayedScenes = remember(planetTabs, activePlanet, selectedCategory, searchQuery, language) { val displayedScenes = remember(planetTabs, activePlanet, selectedCategory, searchQuery, language) {
val baseScenes = if (selectedCategory == FilterCategory.BETA || selectedPlanetId == PLANET_ID_ALL) { val baseScenes = if (selectedCategory == FilterCategory.BETA) {
if (activePlanet != null && activePlanet.id != PLANET_ID_ALL && activePlanet.scenes.any { it.isBeta }) {
activePlanet.scenes
} else {
planets.flatMap { it.scenes }
}
} else if (selectedPlanetId == PLANET_ID_ALL) {
planets.flatMap { it.scenes } planets.flatMap { it.scenes }
} else { } else {
activePlanet?.scenes ?: emptyList() activePlanet?.scenes ?: emptyList()
@@ -117,7 +125,7 @@ fun MainScreen(
baseScenes.filter { scene -> baseScenes.filter { scene ->
val matchCategory = when (selectedCategory) { val matchCategory = when (selectedCategory) {
FilterCategory.ALL -> true FilterCategory.ALL -> true
FilterCategory.COMBAT -> scene.combatReady FilterCategory.COMBAT -> scene.combatReady && !scene.isBeta
FilterCategory.EXPLORE -> !scene.combatReady && !scene.isBeta FilterCategory.EXPLORE -> !scene.combatReady && !scene.isBeta
FilterCategory.BETA -> scene.isBeta FilterCategory.BETA -> scene.isBeta
} }
@@ -139,9 +147,9 @@ fun MainScreen(
val list = if (selectedPlanetId == PLANET_ID_ALL) planets.flatMap { it.scenes } else activePlanet?.scenes ?: emptyList() val list = if (selectedPlanetId == PLANET_ID_ALL) planets.flatMap { it.scenes } else activePlanet?.scenes ?: emptyList()
mapOf( mapOf(
FilterCategory.ALL to list.size, FilterCategory.ALL to list.size,
FilterCategory.COMBAT to list.count { it.combatReady }, FilterCategory.COMBAT to list.count { it.combatReady && !it.isBeta },
FilterCategory.EXPLORE to list.count { !it.combatReady && !it.isBeta }, FilterCategory.EXPLORE to list.count { !it.combatReady && !it.isBeta },
FilterCategory.BETA to globalBetaCount FilterCategory.BETA to if (selectedPlanetId == PLANET_ID_ALL) globalBetaCount else list.count { it.isBeta }
) )
} }
@@ -248,121 +256,188 @@ fun MainScreen(
shape = RoundedCornerShape(10.dp), shape = RoundedCornerShape(10.dp),
color = DarkSurface color = DarkSurface
) { ) {
Row( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(12.dp), .padding(12.dp)
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) { ) {
Column(modifier = Modifier.weight(1f)) { // Top Row: Status Badge & Target Info
if (isRootMode) { Row(
Row(verticalAlignment = Alignment.CenterVertically) { modifier = Modifier.fillMaxWidth(),
Badge(text = "⚡ ROOT DIRECT", color = CombatGreen) verticalAlignment = Alignment.CenterVertically,
Spacer(modifier = Modifier.width(6.dp)) horizontalArrangement = Arrangement.SpaceBetween
Text( ) {
text = "ppSR Auto-Synced", Column(modifier = Modifier.weight(1f)) {
fontWeight = FontWeight.Bold, if (isRootMode) {
fontSize = 13.sp, Row(verticalAlignment = Alignment.CenterVertically) {
color = CombatGreen Badge(text = "⚡ ROOT DIRECT", color = CombatGreen)
) Spacer(modifier = Modifier.width(6.dp))
} Text(
if (currentActiveConfig != null) { text = "ppSR Auto-Synced",
Text( fontWeight = FontWeight.Bold,
text = "Current: Plane ${currentActiveConfig?.planeId} (Layer ${currentActiveConfig?.mapLayer})", fontSize = 13.sp,
fontSize = 11.sp, color = CombatGreen
color = NeonCyan )
) }
Spacer(modifier = Modifier.height(2.dp))
if (currentActiveConfig != null) {
Text(
text = "Current: Plane ${currentActiveConfig?.planeId} (Layer ${currentActiveConfig?.mapLayer})",
fontSize = 11.sp,
color = NeonCyan
)
} else {
Text(
text = "/data/data/com.afn.ppSR/files/server/build/build/misc/persistent.json",
fontSize = 10.sp,
color = TextMuted,
maxLines = 1
)
}
} else { } else {
Text( Row(verticalAlignment = Alignment.CenterVertically) {
text = "Connected to com.afn.ppSR internal storage", Badge(text = "📁 SAF MODE", color = ExploreAmber)
fontSize = 11.sp, Spacer(modifier = Modifier.width(6.dp))
color = TextMuted Text(
) text = if (currentPersistentUri != null) "persistent.json (Selected)" else "Target: Not Selected",
fontWeight = FontWeight.Bold,
fontSize = 13.sp,
color = if (currentPersistentUri != null) CombatGreen else TextMuted
)
}
Spacer(modifier = Modifier.height(2.dp))
if (currentActiveConfig != null) {
Text(
text = "Current Plane: ${currentActiveConfig?.planeId} (Layer ${currentActiveConfig?.mapLayer})",
fontSize = 11.sp,
color = NeonCyan
)
} else {
Text(
text = "เลือก persistent.json หรือกดเปิด '⚡ Root Mode' เพื่อซิงก์ ppSR อัตโนมัติ",
fontSize = 11.sp,
color = TextMuted
)
}
} }
} else { }
Text(
text = if (currentPersistentUri != null) "Target: persistent.json" else "Target: Not Selected (SAF Mode)", // Restore / Revert Backup Button (Always available at top right!)
fontWeight = FontWeight.Bold, IconButton(
fontSize = 13.sp, onClick = { showRestoreDialog = true },
color = if (currentPersistentUri != null) CombatGreen else TextMuted modifier = Modifier.size(36.dp)
) {
Icon(
Icons.Default.Restore,
contentDescription = "Restore Backup",
tint = StarGold,
modifier = Modifier.size(22.dp)
) )
if (currentActiveConfig != null) {
Text(
text = "Current Plane: ${currentActiveConfig?.planeId} (Layer ${currentActiveConfig?.mapLayer})",
fontSize = 11.sp,
color = NeonCyan
)
} else {
Text(
text = "Select persistent.json or grant Root for 1-tap teleport",
fontSize = 11.sp,
color = TextMuted
)
}
} }
} }
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) { Spacer(modifier = Modifier.height(10.dp))
// Restore / Revert Backup Button
IconButton(
onClick = {
coroutineScope.launch {
val res = StorageManager.restoreBackup(context, isRootMode, currentPersistentUri)
res.onSuccess {
if (isRootMode) {
currentActiveConfig = StorageManager.readConfigViaRoot()
} else if (currentPersistentUri != null) {
currentActiveConfig = StorageManager.readCurrentConfig(context, currentPersistentUri)
}
Toast.makeText(context, "Restored backup persistent.json successfully!", Toast.LENGTH_SHORT).show()
}.onFailure { err ->
Toast.makeText(context, "Restore failed: ${err.message}", Toast.LENGTH_SHORT).show()
}
}
},
modifier = Modifier.size(34.dp)
) {
Icon(Icons.Default.Restore, contentDescription = "Restore Backup", tint = StarGold, modifier = Modifier.size(20.dp))
}
// Bottom Row: Action Buttons
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
if (isRootMode) { if (isRootMode) {
// Restart ppSR Server button // Restart ppSR Server button
OutlinedButton( Button(
onClick = { onClick = {
coroutineScope.launch { coroutineScope.launch {
Toast.makeText(context, "Restarting ppSR Server...", Toast.LENGTH_SHORT).show() Toast.makeText(context, "Restarting ppSR Server...", Toast.LENGTH_SHORT).show()
val res = StorageManager.restartPpsrServerViaRoot() val res = StorageManager.restartPpsrServerViaRoot()
res.onSuccess { res.onSuccess {
Toast.makeText(context, "ppSR Server restarted successfully!", Toast.LENGTH_SHORT).show() Toast.makeText(context, "รีสตาร์ท ppSR Server สำเร็จ!", Toast.LENGTH_SHORT).show()
}.onFailure { }.onFailure {
Toast.makeText(context, "Restart failed: ${it.message}", Toast.LENGTH_SHORT).show() Toast.makeText(context, "Restart failed: ${it.message}", Toast.LENGTH_SHORT).show()
} }
} }
}, },
shape = RoundedCornerShape(8.dp), shape = RoundedCornerShape(8.dp),
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 2.dp), colors = ButtonDefaults.buttonColors(
modifier = Modifier.height(32.dp) containerColor = DarkSurfaceElevated
),
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 4.dp),
modifier = Modifier.height(34.dp).weight(1f)
) { ) {
Icon(Icons.Default.Refresh, contentDescription = "Restart", modifier = Modifier.size(13.dp), tint = NeonCyan) Icon(Icons.Default.Refresh, contentDescription = "Restart", modifier = Modifier.size(14.dp), tint = NeonCyan)
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
Text("Restart ppSR", fontSize = 11.sp, color = NeonCyan) Text("Restart ppSR", fontSize = 11.sp, color = NeonCyan)
} }
// Switch to SAF button
OutlinedButton(
onClick = {
isRootMode = false
coroutineScope.launch {
if (currentPersistentUri != null) {
currentActiveConfig = StorageManager.readCurrentConfig(context, currentPersistentUri)
} else {
currentActiveConfig = null
}
}
Toast.makeText(context, "สลับเป็นโหมด SAF", Toast.LENGTH_SHORT).show()
},
shape = RoundedCornerShape(8.dp),
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 4.dp),
modifier = Modifier.height(34.dp)
) {
Text("Switch to SAF", fontSize = 11.sp, color = TextMuted)
}
} else { } else {
// ⚡ Explicit Root Mode Button
Button( Button(
onClick = onPickPersistentFile, onClick = {
coroutineScope.launch {
isConnectingRoot = true
val res = StorageManager.connectRootMode()
isConnectingRoot = false
res.onSuccess {
isRootMode = true
currentActiveConfig = StorageManager.readConfigViaRoot()
Toast.makeText(context, "⚡ เชื่อมต่อ Root Direct (ppSR) สำเร็จ!", Toast.LENGTH_SHORT).show()
}.onFailure { err ->
Toast.makeText(context, "เชื่อมต่อ Root ไม่สำเร็จ: ${err.message}", Toast.LENGTH_LONG).show()
}
}
},
shape = RoundedCornerShape(8.dp), shape = RoundedCornerShape(8.dp),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = if (currentPersistentUri != null) DarkSurfaceElevated else PurplePrimary containerColor = CombatGreen
), ),
enabled = !isConnectingRoot,
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp), contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp),
modifier = Modifier.height(32.dp) modifier = Modifier.height(34.dp).weight(1f)
) { ) {
Icon(Icons.Default.Bolt, contentDescription = "Root", modifier = Modifier.size(16.dp), tint = Color.Black)
Spacer(modifier = Modifier.width(4.dp))
Text( Text(
text = if (currentPersistentUri != null) "Change" else "Choose File", if (isConnectingRoot) "Connecting..." else "⚡ Root Mode (ppSR)",
fontSize = 11.sp fontSize = 12.sp,
fontWeight = FontWeight.Bold,
color = Color.Black
) )
} }
// SAF File Chooser
OutlinedButton(
onClick = onPickPersistentFile,
shape = RoundedCornerShape(8.dp),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = if (currentPersistentUri != null) NeonCyan else TextMuted
),
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 4.dp),
modifier = Modifier.height(34.dp)
) {
Icon(Icons.Default.FolderOpen, contentDescription = "Choose File", modifier = Modifier.size(14.dp))
Spacer(modifier = Modifier.width(4.dp))
Text(if (currentPersistentUri != null) "Change File" else "Choose File", fontSize = 11.sp)
}
} }
} }
} }
@@ -629,4 +704,64 @@ fun MainScreen(
containerColor = DarkSurfaceElevated containerColor = DarkSurfaceElevated
) )
} }
// Restore Backup Confirmation Dialog
if (showRestoreDialog) {
AlertDialog(
onDismissRequest = { showRestoreDialog = false },
title = {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
Icons.Default.Restore,
contentDescription = null,
tint = StarGold,
modifier = Modifier.size(22.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "ย้อนกลับไฟล์สำรอง (Restore)",
color = TextLight,
fontWeight = FontWeight.Bold,
fontSize = 16.sp
)
}
},
text = {
Text(
text = "ต้องการกู้คืน persistent.json จากไฟล์สำรอง (.bak) ล่าสุดหรือไม่?\n\nระบบจะนำข้อมูลพิกัดฉากและ Calyx ก่อนการแก้ไขล่าสุดกลับมาใช้งาน",
fontSize = 13.sp,
color = TextMuted
)
},
confirmButton = {
Button(
onClick = {
showRestoreDialog = false
coroutineScope.launch {
val res = StorageManager.restoreBackup(context, isRootMode, currentPersistentUri)
res.onSuccess {
if (isRootMode) {
currentActiveConfig = StorageManager.readConfigViaRoot()
} else if (currentPersistentUri != null) {
currentActiveConfig = StorageManager.readCurrentConfig(context, currentPersistentUri)
}
Toast.makeText(context, "↩️ ย้อนกลับไฟล์สำรอง (.bak) สำเร็จ!", Toast.LENGTH_SHORT).show()
}.onFailure { err ->
Toast.makeText(context, "ไม่สามารถย้อนกลับได้: ${err.message}", Toast.LENGTH_SHORT).show()
}
}
},
colors = ButtonDefaults.buttonColors(containerColor = StarGold)
) {
Text("ย้อนกลับไฟล์ (Restore)", color = Color.Black, fontWeight = FontWeight.Bold, fontSize = 12.sp)
}
},
dismissButton = {
TextButton(onClick = { showRestoreDialog = false }) {
Text("ยกเลิก (Cancel)", color = TextMuted, fontSize = 12.sp)
}
},
containerColor = DarkSurfaceElevated
)
}
} }