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
|
.cxx
|
||||||
local.properties
|
local.properties
|
||||||
/app/build
|
/app/build
|
||||||
|
|
||||||
|
*.apk
|
||||||
@@ -109,7 +109,10 @@ object SceneRepository {
|
|||||||
isBeta = isBeta,
|
isBeta = isBeta,
|
||||||
descEn = descEn,
|
descEn = descEn,
|
||||||
descTh = descTh,
|
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 kotlinx.coroutines.withContext
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
|
||||||
|
|
||||||
object StorageManager {
|
object StorageManager {
|
||||||
|
|
||||||
private const val PREFS_NAME = "escape_penacony_prefs"
|
private const val PREFS_NAME = "escape_penacony_prefs"
|
||||||
private const val KEY_PERSISTENT_URI = "persistent_uri"
|
private const val KEY_PERSISTENT_URI = "persistent_uri"
|
||||||
private const val KEY_SELECTED_LANG = "selected_language"
|
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) {
|
fun savePersistentUri(context: Context, uri: Uri) {
|
||||||
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
@@ -45,6 +49,100 @@ object StorageManager {
|
|||||||
.getString(KEY_SELECTED_LANG, "en") ?: "en"
|
.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) {
|
suspend fun readCurrentConfig(context: Context, uri: Uri): SceneConfig? = withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val content = context.contentResolver.openInputStream(uri)?.bufferedReader()?.use { it.readText() }
|
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) {
|
suspend fun applySceneConfig(context: Context, uri: Uri, newConfig: SceneConfig): Result<Unit> = withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
// Read original content
|
|
||||||
val originalContent = context.contentResolver.openInputStream(uri)?.bufferedReader()?.use { it.readText() } ?: "{}"
|
val originalContent = context.contentResolver.openInputStream(uri)?.bufferedReader()?.use { it.readText() } ?: "{}"
|
||||||
|
|
||||||
// Backup copy in app cache
|
|
||||||
try {
|
try {
|
||||||
val backupFile = File(context.cacheDir, "persistent_backup.json")
|
val backupFile = File(context.cacheDir, "persistent_backup.json")
|
||||||
backupFile.writeText(originalContent)
|
backupFile.writeText(originalContent)
|
||||||
} catch (_: Exception) {}
|
} catch (_: Exception) {}
|
||||||
|
|
||||||
// Generate updated JSON
|
|
||||||
val updatedJson = PersistentHelper.updateJsonString(originalContent, newConfig)
|
val updatedJson = PersistentHelper.updateJsonString(originalContent, newConfig)
|
||||||
|
|
||||||
// Overwrite target file via SAF
|
|
||||||
context.contentResolver.openOutputStream(uri, "wt")?.use { output ->
|
context.contentResolver.openOutputStream(uri, "wt")?.use { output ->
|
||||||
output.write(updatedJson.toByteArray(Charsets.UTF_8))
|
output.write(updatedJson.toByteArray(Charsets.UTF_8))
|
||||||
output.flush()
|
output.flush()
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ data class SceneItem(
|
|||||||
val isBeta: Boolean,
|
val isBeta: Boolean,
|
||||||
val descEn: String,
|
val descEn: String,
|
||||||
val descTh: 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) {
|
fun localizedName(lang: AppLanguage): String = when (lang) {
|
||||||
AppLanguage.TH -> nameTh.ifEmpty { nameEn }
|
AppLanguage.TH -> nameTh.ifEmpty { nameEn }
|
||||||
@@ -47,6 +50,12 @@ data class SceneItem(
|
|||||||
AppLanguage.ZH -> descZh.ifEmpty { descEn }
|
AppLanguage.ZH -> descZh.ifEmpty { descEn }
|
||||||
AppLanguage.EN -> 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(
|
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
|
// Coords & Plane ID
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ import com.catlas.escapepenacony.model.*
|
|||||||
import com.catlas.escapepenacony.ui.theme.*
|
import com.catlas.escapepenacony.ui.theme.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
private const val PLANET_ID_ALL = "ALL_WORLDS"
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreen(
|
fun MainScreen(
|
||||||
@@ -45,11 +47,12 @@ fun MainScreen(
|
|||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
var planets by remember { mutableStateOf<List<PlanetItem>>(emptyList()) }
|
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 selectedCategory by remember { mutableStateOf(FilterCategory.ALL) }
|
||||||
var searchQuery by remember { mutableStateOf("") }
|
var searchQuery by remember { mutableStateOf("") }
|
||||||
var language by remember { mutableStateOf(AppLanguage.EN) }
|
var language by remember { mutableStateOf(AppLanguage.EN) }
|
||||||
|
|
||||||
|
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) }
|
||||||
|
|
||||||
@@ -60,29 +63,57 @@ fun MainScreen(
|
|||||||
var downloadProgress by remember { mutableStateOf(0) }
|
var downloadProgress by remember { mutableStateOf(0) }
|
||||||
var isCheckingUpdate by remember { mutableStateOf(false) }
|
var isCheckingUpdate by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
// Load initial data
|
// Load initial data and check Root
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
val savedLang = StorageManager.getSavedLanguage(context)
|
val savedLang = StorageManager.getSavedLanguage(context)
|
||||||
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)
|
||||||
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
|
// Refresh active config if in SAF Mode
|
||||||
LaunchedEffect(currentPersistentUri) {
|
LaunchedEffect(currentPersistentUri, isRootMode) {
|
||||||
if (currentPersistentUri != null) {
|
if (!isRootMode && currentPersistentUri != null) {
|
||||||
currentActiveConfig = StorageManager.readCurrentConfig(context, currentPersistentUri)
|
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
|
// Filter scenes
|
||||||
val activePlanet = planets.firstOrNull { it.id == selectedPlanetId }
|
// When BETA category is selected, search globally across all planets so Starforster & Fleeting Joy always appear!
|
||||||
val displayedScenes = remember(activePlanet, selectedCategory, searchQuery, language) {
|
val displayedScenes = remember(planetTabs, activePlanet, selectedCategory, searchQuery, language) {
|
||||||
val baseScenes = activePlanet?.scenes ?: emptyList()
|
val baseScenes = if (selectedCategory == FilterCategory.BETA || selectedPlanetId == PLANET_ID_ALL) {
|
||||||
|
planets.flatMap { it.scenes }
|
||||||
|
} else {
|
||||||
|
activePlanet?.scenes ?: emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
baseScenes.filter { scene ->
|
baseScenes.filter { scene ->
|
||||||
val matchCategory = when (selectedCategory) {
|
val matchCategory = when (selectedCategory) {
|
||||||
FilterCategory.ALL -> true
|
FilterCategory.ALL -> true
|
||||||
@@ -101,13 +132,16 @@ fun MainScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Category Counts
|
// Category Counts
|
||||||
val counts = remember(activePlanet) {
|
val globalBetaCount = remember(planets) {
|
||||||
val list = activePlanet?.scenes ?: emptyList()
|
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(
|
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 },
|
||||||
FilterCategory.EXPLORE to list.count { !it.combatReady && !it.isBeta },
|
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()
|
.fillMaxSize()
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
) {
|
) {
|
||||||
// Persistent Target File Section
|
// Persistent Target File Section (Root Mode vs SAF Mode)
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -222,40 +256,90 @@ fun MainScreen(
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Text(
|
if (isRootMode) {
|
||||||
text = if (currentPersistentUri != null) "Target: persistent.json" else "Target: Not Selected",
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
fontWeight = FontWeight.Bold,
|
Badge(text = "⚡ ROOT DIRECT", color = CombatGreen)
|
||||||
fontSize = 13.sp,
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
color = if (currentPersistentUri != null) CombatGreen else TextMuted
|
Text(
|
||||||
)
|
text = "ppSR Auto-Synced",
|
||||||
if (currentActiveConfig != null) {
|
fontWeight = FontWeight.Bold,
|
||||||
Text(
|
fontSize = 13.sp,
|
||||||
text = "Current Plane: ${currentActiveConfig?.planeId} (Layer ${currentActiveConfig?.mapLayer})",
|
color = CombatGreen
|
||||||
fontSize = 11.sp,
|
)
|
||||||
color = NeonCyan
|
}
|
||||||
)
|
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 {
|
} else {
|
||||||
Text(
|
Text(
|
||||||
text = "Select file to enable one-tap teleport",
|
text = if (currentPersistentUri != null) "Target: persistent.json" else "Target: Not Selected (SAF Mode)",
|
||||||
fontSize = 11.sp,
|
fontWeight = FontWeight.Bold,
|
||||||
color = TextMuted
|
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(
|
if (isRootMode) {
|
||||||
onClick = onPickPersistentFile,
|
// Restart ppSR Server button
|
||||||
shape = RoundedCornerShape(8.dp),
|
OutlinedButton(
|
||||||
colors = ButtonDefaults.buttonColors(
|
onClick = {
|
||||||
containerColor = if (currentPersistentUri != null) DarkSurfaceElevated else PurplePrimary
|
coroutineScope.launch {
|
||||||
),
|
Toast.makeText(context, "Restarting ppSR Server...", Toast.LENGTH_SHORT).show()
|
||||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp),
|
val res = StorageManager.restartPpsrServerViaRoot()
|
||||||
modifier = Modifier.height(32.dp)
|
res.onSuccess {
|
||||||
) {
|
Toast.makeText(context, "ppSR Server restarted successfully!", Toast.LENGTH_SHORT).show()
|
||||||
Text(
|
}.onFailure {
|
||||||
text = if (currentPersistentUri != null) "Change" else "Choose File",
|
Toast.makeText(context, "Restart failed: ${it.message}", Toast.LENGTH_SHORT).show()
|
||||||
fontSize = 11.sp
|
}
|
||||||
)
|
}
|
||||||
|
},
|
||||||
|
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(
|
OutlinedTextField(
|
||||||
value = searchQuery,
|
value = searchQuery,
|
||||||
onValueChange = { searchQuery = it },
|
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) },
|
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null, tint = TextMuted) },
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
if (searchQuery.isNotEmpty()) {
|
if (searchQuery.isNotEmpty()) {
|
||||||
@@ -286,8 +370,8 @@ fun MainScreen(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Planet Horizontal Tabs
|
// Planet Horizontal Tabs (Including "All Worlds")
|
||||||
if (planets.isNotEmpty()) {
|
if (planetTabs.isNotEmpty()) {
|
||||||
LazyRow(
|
LazyRow(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -295,7 +379,7 @@ fun MainScreen(
|
|||||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
items(planets) { planet ->
|
items(planetTabs) { planet ->
|
||||||
val isSelected = planet.id == selectedPlanetId
|
val isSelected = planet.id == selectedPlanetId
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
@@ -350,24 +434,43 @@ fun MainScreen(
|
|||||||
scene = scene,
|
scene = scene,
|
||||||
language = language,
|
language = language,
|
||||||
onTeleportClick = { targetScene ->
|
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)
|
val cfg = SceneConfig.fromScene(targetScene)
|
||||||
coroutineScope.launch {
|
|
||||||
val res = StorageManager.applySceneConfig(context, currentPersistentUri, cfg)
|
if (isRootMode) {
|
||||||
res.onSuccess {
|
// ⚡ Direct Root Execution to com.afn.ppSR
|
||||||
currentActiveConfig = cfg
|
coroutineScope.launch {
|
||||||
Toast.makeText(
|
val res = StorageManager.writeConfigViaRoot(context, cfg)
|
||||||
context,
|
res.onSuccess {
|
||||||
"Teleported to ${targetScene.localizedName(language)} (Plane ${targetScene.planeId})!",
|
currentActiveConfig = cfg
|
||||||
Toast.LENGTH_SHORT
|
Toast.makeText(
|
||||||
).show()
|
context,
|
||||||
}.onFailure {
|
"⚡ [Root Direct] Teleported to ${targetScene.localizedName(language)} (Plane ${targetScene.planeId})!",
|
||||||
Toast.makeText(context, "Teleport failed: ${it.message}", Toast.LENGTH_LONG).show()
|
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,
|
calyxZ = z + 1800,
|
||||||
mapLayer = layer
|
mapLayer = layer
|
||||||
)
|
)
|
||||||
if (currentPersistentUri == null) {
|
|
||||||
val jsonStr = StorageManager.exportTemplateJson(context, cfg)
|
if (isRootMode) {
|
||||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
coroutineScope.launch {
|
||||||
clipboard.setPrimaryClip(ClipData.newPlainText("ppSR Custom Teleport JSON", jsonStr))
|
val res = StorageManager.writeConfigViaRoot(context, cfg)
|
||||||
Toast.makeText(context, "JSON copied! Please choose persistent.json to apply directly.", Toast.LENGTH_LONG).show()
|
res.onSuccess {
|
||||||
} else {
|
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 {
|
coroutineScope.launch {
|
||||||
val res = StorageManager.applySceneConfig(context, currentPersistentUri, cfg)
|
val res = StorageManager.applySceneConfig(context, currentPersistentUri, cfg)
|
||||||
res.onSuccess {
|
res.onSuccess {
|
||||||
@@ -416,6 +525,11 @@ fun MainScreen(
|
|||||||
Toast.makeText(context, "Failed: ${it.message}", Toast.LENGTH_LONG).show()
|
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