feat: add Root Direct Mode for ppSR auto-sync and fix global beta scenes filter
This commit is contained in:
1 parent
164219f7dd
commit
7f6583df17
6 files changed
+312
-79
No files matched your search
@@ -9,3 +9,5 @@
|
||||
.cxx
|
||||
local.properties
|
||||
/app/build
|
||||
|
||||
*.apk
|
||||
@@ -109,7 +109,10 @@ object SceneRepository {
|
||||
isBeta = isBeta,
|
||||
descEn = descEn,
|
||||
descTh = descTh,
|
||||
descZh = descZh
|
||||
descZh = descZh,
|
||||
planetNameEn = pNameEn,
|
||||
planetNameTh = pNameTh,
|
||||
planetNameZh = pNameZh
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,13 +8,17 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
object StorageManager {
|
||||
|
||||
private const val PREFS_NAME = "escape_penacony_prefs"
|
||||
private const val KEY_PERSISTENT_URI = "persistent_uri"
|
||||
private const val KEY_SELECTED_LANG = "selected_language"
|
||||
private const val KEY_PREFER_ROOT = "prefer_root_mode"
|
||||
|
||||
const val PPSR_PACKAGE_NAME = "com.afn.ppSR"
|
||||
const val PPSR_INTERNAL_PERSISTENT_PATH =
|
||||
"/data/data/com.afn.ppSR/files/server/build/build/misc/persistent.json"
|
||||
|
||||
fun savePersistentUri(context: Context, uri: Uri) {
|
||||
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
@@ -45,6 +49,100 @@ object StorageManager {
|
||||
.getString(KEY_SELECTED_LANG, "en") ?: "en"
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Root Direct Access Engine (su)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
suspend fun checkRootAccess(): Boolean = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val process = Runtime.getRuntime().exec(arrayOf("su", "-c", "id"))
|
||||
val exit = process.waitFor()
|
||||
exit == 0
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun checkPpsrInternalFileExists(): Boolean = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val cmd = "[ -f \"$PPSR_INTERNAL_PERSISTENT_PATH\" ] && echo 'EXISTS'"
|
||||
val process = Runtime.getRuntime().exec(arrayOf("su", "-c", cmd))
|
||||
val output = process.inputStream.bufferedReader().use { it.readText().trim() }
|
||||
process.waitFor()
|
||||
output == "EXISTS"
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun readConfigViaRoot(): SceneConfig? = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val cmd = "cat \"$PPSR_INTERNAL_PERSISTENT_PATH\""
|
||||
val process = Runtime.getRuntime().exec(arrayOf("su", "-c", cmd))
|
||||
val jsonContent = process.inputStream.bufferedReader().use { it.readText().trim() }
|
||||
val exit = process.waitFor()
|
||||
if (exit != 0 || jsonContent.isEmpty()) return@withContext null
|
||||
|
||||
val root = JSONObject(jsonContent)
|
||||
val sceneObj = root.optJSONObject("scene") ?: return@withContext null
|
||||
SceneConfig.fromJsonObject(sceneObj)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun writeConfigViaRoot(context: Context, newConfig: SceneConfig): Result<Unit> =
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
// Read original content to preserve avatar, key, send_hotfix, etc.
|
||||
val readCmd = "cat \"$PPSR_INTERNAL_PERSISTENT_PATH\""
|
||||
val readProcess = Runtime.getRuntime().exec(arrayOf("su", "-c", readCmd))
|
||||
val originalContent = readProcess.inputStream.bufferedReader().use { it.readText().trim() }
|
||||
readProcess.waitFor()
|
||||
|
||||
val updatedJson = PersistentHelper.updateJsonString(
|
||||
if (originalContent.isNotEmpty()) originalContent else "{}",
|
||||
newConfig
|
||||
)
|
||||
|
||||
// Write to cache file first
|
||||
val cacheFile = File(context.cacheDir, "persistent_root_sync.json")
|
||||
cacheFile.writeText(updatedJson, Charsets.UTF_8)
|
||||
|
||||
// Copy via su into ppSR private storage and fix permissions
|
||||
val copyCmd = "cp \"${cacheFile.absolutePath}\" \"$PPSR_INTERNAL_PERSISTENT_PATH\" && chmod 666 \"$PPSR_INTERNAL_PERSISTENT_PATH\""
|
||||
val copyProcess = Runtime.getRuntime().exec(arrayOf("su", "-c", copyCmd))
|
||||
val copyExit = copyProcess.waitFor()
|
||||
|
||||
if (copyExit == 0) {
|
||||
Result.success(Unit)
|
||||
} else {
|
||||
val errorText = copyProcess.errorStream.bufferedReader().use { it.readText().trim() }
|
||||
Result.failure(Exception("Root copy failed ($copyExit): $errorText"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun restartPpsrServerViaRoot(): Result<Unit> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
// Force-stop then relaunch MainActivity
|
||||
val restartCmd = "am force-stop $PPSR_PACKAGE_NAME && sleep 0.5 && am start -n $PPSR_PACKAGE_NAME/.MainActivity"
|
||||
val process = Runtime.getRuntime().exec(arrayOf("su", "-c", restartCmd))
|
||||
val exit = process.waitFor()
|
||||
if (exit == 0) Result.success(Unit) else Result.failure(Exception("Restart exited with $exit"))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// SAF / Shared Storage Access Engine (Non-Root fallback)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
suspend fun readCurrentConfig(context: Context, uri: Uri): SceneConfig? = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val content = context.contentResolver.openInputStream(uri)?.bufferedReader()?.use { it.readText() }
|
||||
@@ -60,19 +158,15 @@ object StorageManager {
|
||||
|
||||
suspend fun applySceneConfig(context: Context, uri: Uri, newConfig: SceneConfig): Result<Unit> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
// Read original content
|
||||
val originalContent = context.contentResolver.openInputStream(uri)?.bufferedReader()?.use { it.readText() } ?: "{}"
|
||||
|
||||
// Backup copy in app cache
|
||||
try {
|
||||
val backupFile = File(context.cacheDir, "persistent_backup.json")
|
||||
backupFile.writeText(originalContent)
|
||||
} catch (_: Exception) {}
|
||||
|
||||
// Generate updated JSON
|
||||
val updatedJson = PersistentHelper.updateJsonString(originalContent, newConfig)
|
||||
|
||||
// Overwrite target file via SAF
|
||||
context.contentResolver.openOutputStream(uri, "wt")?.use { output ->
|
||||
output.write(updatedJson.toByteArray(Charsets.UTF_8))
|
||||
output.flush()
|
||||
|
||||
@@ -34,7 +34,10 @@ data class SceneItem(
|
||||
val isBeta: Boolean,
|
||||
val descEn: String,
|
||||
val descTh: String,
|
||||
val descZh: String
|
||||
val descZh: String,
|
||||
val planetNameEn: String = "",
|
||||
val planetNameTh: String = "",
|
||||
val planetNameZh: String = ""
|
||||
) {
|
||||
fun localizedName(lang: AppLanguage): String = when (lang) {
|
||||
AppLanguage.TH -> nameTh.ifEmpty { nameEn }
|
||||
@@ -47,6 +50,12 @@ data class SceneItem(
|
||||
AppLanguage.ZH -> descZh.ifEmpty { descEn }
|
||||
AppLanguage.EN -> descEn
|
||||
}
|
||||
|
||||
fun localizedPlanetName(lang: AppLanguage): String = when (lang) {
|
||||
AppLanguage.TH -> planetNameTh.ifEmpty { planetNameEn }
|
||||
AppLanguage.ZH -> planetNameZh.ifEmpty { planetNameEn }
|
||||
AppLanguage.EN -> planetNameEn
|
||||
}
|
||||
}
|
||||
|
||||
data class PlanetItem(
|
||||
|
||||
@@ -134,7 +134,18 @@ fun SceneCard(
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
val planetName = scene.localizedPlanetName(language)
|
||||
if (planetName.isNotEmpty()) {
|
||||
Text(
|
||||
text = "🌐 $planetName",
|
||||
fontSize = 11.sp,
|
||||
color = StarGold,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
}
|
||||
|
||||
// Coords & Plane ID
|
||||
Text(
|
||||
|
||||
@@ -35,6 +35,8 @@ import com.catlas.escapepenacony.model.*
|
||||
import com.catlas.escapepenacony.ui.theme.*
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private const val PLANET_ID_ALL = "ALL_WORLDS"
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MainScreen(
|
||||
@@ -45,11 +47,12 @@ fun MainScreen(
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
var planets by remember { mutableStateOf<List<PlanetItem>>(emptyList()) }
|
||||
var selectedPlanetId by remember { mutableStateOf<String?>(null) }
|
||||
var selectedPlanetId by remember { mutableStateOf<String>(PLANET_ID_ALL) }
|
||||
var selectedCategory by remember { mutableStateOf(FilterCategory.ALL) }
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
var language by remember { mutableStateOf(AppLanguage.EN) }
|
||||
|
||||
var isRootMode by remember { mutableStateOf(false) }
|
||||
var currentActiveConfig by remember { mutableStateOf<SceneConfig?>(null) }
|
||||
var showCustomDialog by remember { mutableStateOf(false) }
|
||||
|
||||
@@ -60,29 +63,57 @@ fun MainScreen(
|
||||
var downloadProgress by remember { mutableStateOf(0) }
|
||||
var isCheckingUpdate by remember { mutableStateOf(false) }
|
||||
|
||||
// Load initial data
|
||||
// Load initial data and check Root
|
||||
LaunchedEffect(Unit) {
|
||||
val savedLang = StorageManager.getSavedLanguage(context)
|
||||
language = AppLanguage.values().firstOrNull { it.code == savedLang } ?: AppLanguage.EN
|
||||
planets = SceneRepository.loadScenes(context)
|
||||
if (planets.isNotEmpty()) {
|
||||
selectedPlanetId = planets.first().id
|
||||
|
||||
// Check Root Mode
|
||||
val hasRoot = StorageManager.checkRootAccess()
|
||||
val hasPpsr = if (hasRoot) StorageManager.checkPpsrInternalFileExists() else false
|
||||
isRootMode = hasRoot && hasPpsr
|
||||
if (isRootMode) {
|
||||
currentActiveConfig = StorageManager.readConfigViaRoot()
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh active config when persistent URI changes
|
||||
LaunchedEffect(currentPersistentUri) {
|
||||
if (currentPersistentUri != null) {
|
||||
// Refresh active config if in SAF Mode
|
||||
LaunchedEffect(currentPersistentUri, isRootMode) {
|
||||
if (!isRootMode && currentPersistentUri != null) {
|
||||
currentActiveConfig = StorageManager.readCurrentConfig(context, currentPersistentUri)
|
||||
} else {
|
||||
currentActiveConfig = null
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare list of planets including "All Worlds" tab
|
||||
val planetTabs = remember(planets, language) {
|
||||
if (planets.isEmpty()) emptyList() else {
|
||||
val allScenes = planets.flatMap { it.scenes }
|
||||
listOf(
|
||||
PlanetItem(
|
||||
id = PLANET_ID_ALL,
|
||||
nameEn = "All Worlds",
|
||||
nameTh = "ทุกโลก (All)",
|
||||
nameZh = "所有世界",
|
||||
scenes = allScenes
|
||||
)
|
||||
) + planets
|
||||
}
|
||||
}
|
||||
|
||||
val activePlanet = remember(planetTabs, selectedPlanetId) {
|
||||
planetTabs.firstOrNull { it.id == selectedPlanetId } ?: planetTabs.firstOrNull()
|
||||
}
|
||||
|
||||
// Filter scenes
|
||||
val activePlanet = planets.firstOrNull { it.id == selectedPlanetId }
|
||||
val displayedScenes = remember(activePlanet, selectedCategory, searchQuery, language) {
|
||||
val baseScenes = activePlanet?.scenes ?: emptyList()
|
||||
// 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 baseScenes = if (selectedCategory == FilterCategory.BETA || selectedPlanetId == PLANET_ID_ALL) {
|
||||
planets.flatMap { it.scenes }
|
||||
} else {
|
||||
activePlanet?.scenes ?: emptyList()
|
||||
}
|
||||
|
||||
baseScenes.filter { scene ->
|
||||
val matchCategory = when (selectedCategory) {
|
||||
FilterCategory.ALL -> true
|
||||
@@ -101,13 +132,16 @@ fun MainScreen(
|
||||
}
|
||||
|
||||
// Category Counts
|
||||
val counts = remember(activePlanet) {
|
||||
val list = activePlanet?.scenes ?: emptyList()
|
||||
val globalBetaCount = remember(planets) {
|
||||
planets.flatMap { it.scenes }.count { it.isBeta }
|
||||
}
|
||||
val counts = remember(activePlanet, selectedPlanetId, globalBetaCount) {
|
||||
val list = if (selectedPlanetId == PLANET_ID_ALL) planets.flatMap { it.scenes } else activePlanet?.scenes ?: emptyList()
|
||||
mapOf(
|
||||
FilterCategory.ALL to list.size,
|
||||
FilterCategory.COMBAT to list.count { it.combatReady },
|
||||
FilterCategory.EXPLORE to list.count { !it.combatReady && !it.isBeta },
|
||||
FilterCategory.BETA to list.count { it.isBeta }
|
||||
FilterCategory.BETA to globalBetaCount
|
||||
)
|
||||
}
|
||||
|
||||
@@ -206,7 +240,7 @@ fun MainScreen(
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
// Persistent Target File Section
|
||||
// Persistent Target File Section (Root Mode vs SAF Mode)
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -222,40 +256,90 @@ fun MainScreen(
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = if (currentPersistentUri != null) "Target: persistent.json" else "Target: Not Selected",
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 13.sp,
|
||||
color = if (currentPersistentUri != null) CombatGreen else TextMuted
|
||||
)
|
||||
if (currentActiveConfig != null) {
|
||||
Text(
|
||||
text = "Current Plane: ${currentActiveConfig?.planeId} (Layer ${currentActiveConfig?.mapLayer})",
|
||||
fontSize = 11.sp,
|
||||
color = NeonCyan
|
||||
)
|
||||
if (isRootMode) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Badge(text = "⚡ ROOT DIRECT", color = CombatGreen)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(
|
||||
text = "ppSR Auto-Synced",
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 13.sp,
|
||||
color = CombatGreen
|
||||
)
|
||||
}
|
||||
if (currentActiveConfig != null) {
|
||||
Text(
|
||||
text = "Current: Plane ${currentActiveConfig?.planeId} (Layer ${currentActiveConfig?.mapLayer})",
|
||||
fontSize = 11.sp,
|
||||
color = NeonCyan
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = "Connected to com.afn.ppSR internal storage",
|
||||
fontSize = 11.sp,
|
||||
color = TextMuted
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
text = "Select file to enable one-tap teleport",
|
||||
fontSize = 11.sp,
|
||||
color = TextMuted
|
||||
text = if (currentPersistentUri != null) "Target: persistent.json" else "Target: Not Selected (SAF Mode)",
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 13.sp,
|
||||
color = if (currentPersistentUri != null) CombatGreen else TextMuted
|
||||
)
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = onPickPersistentFile,
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = if (currentPersistentUri != null) DarkSurfaceElevated else PurplePrimary
|
||||
),
|
||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp),
|
||||
modifier = Modifier.height(32.dp)
|
||||
) {
|
||||
Text(
|
||||
text = if (currentPersistentUri != null) "Change" else "Choose File",
|
||||
fontSize = 11.sp
|
||||
)
|
||||
if (isRootMode) {
|
||||
// Restart ppSR Server button
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
Toast.makeText(context, "Restarting ppSR Server...", Toast.LENGTH_SHORT).show()
|
||||
val res = StorageManager.restartPpsrServerViaRoot()
|
||||
res.onSuccess {
|
||||
Toast.makeText(context, "ppSR Server restarted successfully!", Toast.LENGTH_SHORT).show()
|
||||
}.onFailure {
|
||||
Toast.makeText(context, "Restart failed: ${it.message}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 2.dp),
|
||||
modifier = Modifier.height(32.dp)
|
||||
) {
|
||||
Icon(Icons.Default.Refresh, contentDescription = "Restart", modifier = Modifier.size(13.dp), tint = NeonCyan)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text("Restart ppSR", fontSize = 11.sp, color = NeonCyan)
|
||||
}
|
||||
} else {
|
||||
Button(
|
||||
onClick = onPickPersistentFile,
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = if (currentPersistentUri != null) DarkSurfaceElevated else PurplePrimary
|
||||
),
|
||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp),
|
||||
modifier = Modifier.height(32.dp)
|
||||
) {
|
||||
Text(
|
||||
text = if (currentPersistentUri != null) "Change" else "Choose File",
|
||||
fontSize = 11.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -264,7 +348,7 @@ fun MainScreen(
|
||||
OutlinedTextField(
|
||||
value = searchQuery,
|
||||
onValueChange = { searchQuery = it },
|
||||
placeholder = { Text("Search scenes (e.g. Penacony, 20461, Parlor Car)...", fontSize = 12.sp) },
|
||||
placeholder = { Text("Search scenes (e.g. Starforster, Fleeting, 20571)...", fontSize = 12.sp) },
|
||||
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null, tint = TextMuted) },
|
||||
trailingIcon = {
|
||||
if (searchQuery.isNotEmpty()) {
|
||||
@@ -286,8 +370,8 @@ fun MainScreen(
|
||||
)
|
||||
)
|
||||
|
||||
// Planet Horizontal Tabs
|
||||
if (planets.isNotEmpty()) {
|
||||
// Planet Horizontal Tabs (Including "All Worlds")
|
||||
if (planetTabs.isNotEmpty()) {
|
||||
LazyRow(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -295,7 +379,7 @@ fun MainScreen(
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(planets) { planet ->
|
||||
items(planetTabs) { planet ->
|
||||
val isSelected = planet.id == selectedPlanetId
|
||||
Surface(
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
@@ -350,24 +434,43 @@ fun MainScreen(
|
||||
scene = scene,
|
||||
language = language,
|
||||
onTeleportClick = { targetScene ->
|
||||
if (currentPersistentUri == null) {
|
||||
Toast.makeText(context, "Please choose persistent.json first!", Toast.LENGTH_SHORT).show()
|
||||
onPickPersistentFile()
|
||||
return@SceneCard
|
||||
}
|
||||
|
||||
val cfg = SceneConfig.fromScene(targetScene)
|
||||
coroutineScope.launch {
|
||||
val res = StorageManager.applySceneConfig(context, currentPersistentUri, cfg)
|
||||
res.onSuccess {
|
||||
currentActiveConfig = cfg
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Teleported to ${targetScene.localizedName(language)} (Plane ${targetScene.planeId})!",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}.onFailure {
|
||||
Toast.makeText(context, "Teleport failed: ${it.message}", Toast.LENGTH_LONG).show()
|
||||
|
||||
if (isRootMode) {
|
||||
// ⚡ Direct Root Execution to com.afn.ppSR
|
||||
coroutineScope.launch {
|
||||
val res = StorageManager.writeConfigViaRoot(context, cfg)
|
||||
res.onSuccess {
|
||||
currentActiveConfig = cfg
|
||||
Toast.makeText(
|
||||
context,
|
||||
"⚡ [Root Direct] Teleported to ${targetScene.localizedName(language)} (Plane ${targetScene.planeId})!",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}.onFailure { err ->
|
||||
Toast.makeText(context, "Root write failed: ${err.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback SAF Mode
|
||||
if (currentPersistentUri == null) {
|
||||
Toast.makeText(context, "Please choose persistent.json first (or grant Root access)!", Toast.LENGTH_SHORT).show()
|
||||
onPickPersistentFile()
|
||||
return@SceneCard
|
||||
}
|
||||
|
||||
coroutineScope.launch {
|
||||
val res = StorageManager.applySceneConfig(context, currentPersistentUri, cfg)
|
||||
res.onSuccess {
|
||||
currentActiveConfig = cfg
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Teleported to ${targetScene.localizedName(language)} (Plane ${targetScene.planeId})!",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}.onFailure {
|
||||
Toast.makeText(context, "Teleport failed: ${it.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -401,12 +504,18 @@ fun MainScreen(
|
||||
calyxZ = z + 1800,
|
||||
mapLayer = layer
|
||||
)
|
||||
if (currentPersistentUri == null) {
|
||||
val jsonStr = StorageManager.exportTemplateJson(context, cfg)
|
||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText("ppSR Custom Teleport JSON", jsonStr))
|
||||
Toast.makeText(context, "JSON copied! Please choose persistent.json to apply directly.", Toast.LENGTH_LONG).show()
|
||||
} else {
|
||||
|
||||
if (isRootMode) {
|
||||
coroutineScope.launch {
|
||||
val res = StorageManager.writeConfigViaRoot(context, cfg)
|
||||
res.onSuccess {
|
||||
currentActiveConfig = cfg
|
||||
Toast.makeText(context, "⚡ Custom coordinates applied via Root!", Toast.LENGTH_SHORT).show()
|
||||
}.onFailure {
|
||||
Toast.makeText(context, "Root write failed: ${it.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
} else if (currentPersistentUri != null) {
|
||||
coroutineScope.launch {
|
||||
val res = StorageManager.applySceneConfig(context, currentPersistentUri, cfg)
|
||||
res.onSuccess {
|
||||
@@ -416,6 +525,11 @@ fun MainScreen(
|
||||
Toast.makeText(context, "Failed: ${it.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val jsonStr = StorageManager.exportTemplateJson(context, cfg)
|
||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText("ppSR Custom Teleport JSON", jsonStr))
|
||||
Toast.makeText(context, "JSON copied! Please choose persistent.json or enable Root to apply directly.", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user