feat: add explicit Root Mode button, restore backup dialog, and fix Beta category parity
This commit is contained in:
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
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user