2070 lines
104 KiB
Kotlin
2070 lines
104 KiB
Kotlin
package com.catlas.escapepenacony.ui
|
|
|
|
import android.content.ClipData
|
|
import android.content.ClipboardManager
|
|
import android.content.Context
|
|
import android.widget.Toast
|
|
import com.catlas.escapepenacony.BuildConfig
|
|
import androidx.compose.foundation.background
|
|
import androidx.compose.foundation.clickable
|
|
import androidx.compose.foundation.layout.*
|
|
import androidx.compose.foundation.lazy.LazyColumn
|
|
import androidx.compose.foundation.lazy.LazyRow
|
|
import androidx.compose.foundation.lazy.items
|
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
import androidx.compose.foundation.shape.CircleShape
|
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
import androidx.compose.material.icons.Icons
|
|
import androidx.compose.material.icons.automirrored.filled.InsertDriveFile
|
|
import androidx.compose.material.icons.automirrored.filled.List
|
|
import androidx.compose.material.icons.filled.*
|
|
import androidx.compose.material3.*
|
|
import androidx.compose.runtime.*
|
|
import androidx.compose.ui.Alignment
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.draw.clip
|
|
import androidx.compose.ui.graphics.Color
|
|
import androidx.compose.ui.platform.LocalContext
|
|
import androidx.compose.ui.text.font.FontFamily
|
|
import androidx.compose.ui.text.font.FontWeight
|
|
import androidx.compose.ui.text.style.TextOverflow
|
|
import androidx.compose.ui.unit.dp
|
|
import androidx.compose.ui.unit.sp
|
|
import com.catlas.escapepenacony.data.*
|
|
import com.catlas.escapepenacony.model.*
|
|
import com.catlas.escapepenacony.service.ServerService
|
|
import com.catlas.escapepenacony.service.ServerStatus
|
|
import com.catlas.escapepenacony.ui.theme.*
|
|
import kotlinx.coroutines.delay
|
|
import kotlinx.coroutines.launch
|
|
import java.io.File
|
|
|
|
fun tr(lang: AppLanguage, en: String, th: String, zh: String = en): String {
|
|
return when (lang) {
|
|
AppLanguage.TH -> th
|
|
AppLanguage.ZH -> zh
|
|
AppLanguage.EN -> en
|
|
}
|
|
}
|
|
|
|
enum class ScreenTab(
|
|
val titleEn: String,
|
|
val titleTh: String,
|
|
val titleZh: String,
|
|
val icon: androidx.compose.ui.graphics.vector.ImageVector
|
|
) {
|
|
SERVER("Server", "เซิร์ฟเวอร์", "服务器", Icons.Default.Computer),
|
|
TELEPORT("Teleport", "วาร์ปแผนที่", "传送地图", Icons.Default.Explore),
|
|
UPDATER("Updater", "อัปเดต", "更新", Icons.Default.CloudDownload);
|
|
|
|
fun localizedTitle(lang: AppLanguage): String = when (lang) {
|
|
AppLanguage.TH -> titleTh
|
|
AppLanguage.ZH -> titleZh
|
|
AppLanguage.EN -> titleEn
|
|
}
|
|
}
|
|
|
|
@OptIn(ExperimentalMaterial3Api::class)
|
|
@Composable
|
|
fun MainScreen() {
|
|
val context = LocalContext.current
|
|
val scope = rememberCoroutineScope()
|
|
|
|
val persistentRepo = remember { PersistentRepository(context) }
|
|
val updateRepo = remember { UpdateRepository(context) }
|
|
|
|
var selectedTab by remember { mutableStateOf(ScreenTab.SERVER) }
|
|
var currentLanguage by remember { mutableStateOf(AppLanguage.TH) }
|
|
|
|
// Server Service States
|
|
val serverStatus by ServerService.serverStatus.collectAsState()
|
|
val serverLogs by ServerService.logs.collectAsState()
|
|
|
|
// Persistent States
|
|
var isServerInstalled by remember { mutableStateOf(persistentRepo.isServerInstalled()) }
|
|
var sendHotfix by remember { mutableStateOf(true) }
|
|
var serverKey by remember { mutableStateOf("") }
|
|
var trailblazerConfig by remember { mutableStateOf(TrailblazerConfig("Female", "Destruction", "Preservation")) }
|
|
var exRankConfigs by remember { mutableStateOf(PersistentRepository.DEFAULT_EX_RANK_CONFIGS) }
|
|
var currentSceneConfig by remember { mutableStateOf<SceneConfig?>(null) }
|
|
|
|
// Dialog states
|
|
var showPasswordDialog by remember { mutableStateOf(false) }
|
|
var showTrailblazerDialog by remember { mutableStateOf(false) }
|
|
var showExRankDialog by remember { mutableStateOf(false) }
|
|
var showMiscExplorerDialog by remember { mutableStateOf(false) }
|
|
var showCustomCoordsDialog by remember { mutableStateOf(false) }
|
|
var showVersionSelectorDialog by remember { mutableStateOf(false) }
|
|
|
|
// Misc file editor
|
|
var miscFiles by remember { mutableStateOf<List<File>>(emptyList()) }
|
|
var selectedMiscFile by remember { mutableStateOf<File?>(null) }
|
|
var miscFileContent by remember { mutableStateOf("") }
|
|
|
|
// Teleport States
|
|
var planets by remember { mutableStateOf<List<PlanetItem>>(emptyList()) }
|
|
var selectedPlanetId by remember { mutableStateOf<String?>(null) }
|
|
var searchQuery by remember { mutableStateOf("") }
|
|
var selectedCategory by remember { mutableStateOf(FilterCategory.ALL) }
|
|
|
|
// Update States
|
|
var installedVersion by remember { mutableStateOf(updateRepo.getInstalledVersion()) }
|
|
var updateInfo by remember { mutableStateOf<UpdateInfo?>(null) }
|
|
var isCheckingUpdate by remember { mutableStateOf(false) }
|
|
var isInstalling by remember { mutableStateOf(false) }
|
|
var installProgress by remember { mutableStateOf(0f) }
|
|
var installStatusMessage by remember { mutableStateOf("") }
|
|
|
|
// App Update States (APK)
|
|
var includePrerelease by remember { mutableStateOf(AppUpdateManager.isIncludePrerelease(context)) }
|
|
var appReleaseInfo by remember { mutableStateOf<AppReleaseInfo?>(null) }
|
|
var showAppUpdateDialog by remember { mutableStateOf(false) }
|
|
var isCheckingAppUpdate by remember { mutableStateOf(false) }
|
|
var isDownloadingApk by remember { mutableStateOf(false) }
|
|
var apkDownloadProgress by remember { mutableStateOf(0) }
|
|
|
|
// Load initial data
|
|
LaunchedEffect(Unit) {
|
|
planets = SceneRepository.loadScenes(context)
|
|
if (planets.isNotEmpty()) {
|
|
selectedPlanetId = planets.first().id
|
|
}
|
|
|
|
if (isServerInstalled) {
|
|
persistentRepo.ensureValidPersistentJsonSync()
|
|
sendHotfix = persistentRepo.getSendHotfix()
|
|
serverKey = persistentRepo.getServerKey()
|
|
trailblazerConfig = persistentRepo.getTrailblazerConfig()
|
|
exRankConfigs = persistentRepo.getExRankConfigs()
|
|
val pJson = persistentRepo.readPersistentJson().getOrNull()
|
|
pJson?.optJSONObject("scene")?.let {
|
|
currentSceneConfig = SceneConfig.fromJsonObject(it)
|
|
}
|
|
}
|
|
|
|
// Silent background check for APK updates on launch
|
|
scope.launch {
|
|
val res = AppUpdateManager.checkForAppUpdate(includePrerelease)
|
|
res.onSuccess { info ->
|
|
if (info != null) {
|
|
appReleaseInfo = info
|
|
if (info.isNewer) {
|
|
showAppUpdateDialog = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
val snackbarHostState = remember { SnackbarHostState() }
|
|
|
|
Scaffold(
|
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
|
topBar = {
|
|
TopAppBar(
|
|
title = {
|
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
Text(
|
|
text = "ppSR & Escape Penacony",
|
|
fontWeight = FontWeight.Bold,
|
|
color = Color.White,
|
|
fontSize = 17.sp
|
|
)
|
|
Spacer(modifier = Modifier.width(8.dp))
|
|
Surface(
|
|
shape = RoundedCornerShape(6.dp),
|
|
color = when (serverStatus) {
|
|
ServerStatus.RUNNING -> CombatGreen.copy(alpha = 0.2f)
|
|
ServerStatus.STARTING, ServerStatus.STOPPING -> ExploreAmber.copy(alpha = 0.2f)
|
|
ServerStatus.ERROR -> Color.Red.copy(alpha = 0.2f)
|
|
ServerStatus.STOPPED -> Color.Gray.copy(alpha = 0.2f)
|
|
}
|
|
) {
|
|
val statusLabel = when (serverStatus) {
|
|
ServerStatus.RUNNING -> tr(currentLanguage, "RUNNING", "กำลังทำงาน", "运行中")
|
|
ServerStatus.STARTING -> tr(currentLanguage, "STARTING", "กำลังเริ่ม", "启动中")
|
|
ServerStatus.STOPPING -> tr(currentLanguage, "STOPPING", "กำลังหยุด", "停止中")
|
|
ServerStatus.ERROR -> tr(currentLanguage, "ERROR", "ข้อผิดพลาด", "错误")
|
|
ServerStatus.STOPPED -> tr(currentLanguage, "STOPPED", "หยุดทำงาน", "已停止")
|
|
}
|
|
Text(
|
|
text = statusLabel,
|
|
color = when (serverStatus) {
|
|
ServerStatus.RUNNING -> CombatGreen
|
|
ServerStatus.STARTING, ServerStatus.STOPPING -> ExploreAmber
|
|
ServerStatus.ERROR -> Color.Red
|
|
ServerStatus.STOPPED -> Color.Gray
|
|
},
|
|
fontSize = 11.sp,
|
|
fontWeight = FontWeight.Bold,
|
|
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
|
|
)
|
|
}
|
|
}
|
|
},
|
|
actions = {
|
|
// Language Switcher Pill
|
|
TextButton(onClick = {
|
|
currentLanguage = when (currentLanguage) {
|
|
AppLanguage.TH -> AppLanguage.EN
|
|
AppLanguage.EN -> AppLanguage.ZH
|
|
AppLanguage.ZH -> AppLanguage.TH
|
|
}
|
|
}) {
|
|
Text(
|
|
text = currentLanguage.displayName,
|
|
color = PurplePrimary,
|
|
fontWeight = FontWeight.SemiBold
|
|
)
|
|
}
|
|
},
|
|
colors = TopAppBarDefaults.topAppBarColors(containerColor = BgCard)
|
|
)
|
|
},
|
|
bottomBar = {
|
|
NavigationBar(containerColor = BgCard) {
|
|
ScreenTab.values().forEach { tab ->
|
|
NavigationBarItem(
|
|
selected = selectedTab == tab,
|
|
onClick = { selectedTab = tab },
|
|
icon = { Icon(tab.icon, contentDescription = tab.titleEn) },
|
|
label = {
|
|
Text(tab.localizedTitle(currentLanguage))
|
|
},
|
|
colors = NavigationBarItemDefaults.colors(
|
|
selectedIconColor = PurplePrimary,
|
|
selectedTextColor = PurplePrimary,
|
|
indicatorColor = PurplePrimary.copy(alpha = 0.15f)
|
|
)
|
|
)
|
|
}
|
|
}
|
|
},
|
|
containerColor = BgDark
|
|
) { paddingValues ->
|
|
Box(
|
|
modifier = Modifier
|
|
.fillMaxSize()
|
|
.padding(paddingValues)
|
|
) {
|
|
when (selectedTab) {
|
|
ScreenTab.SERVER -> {
|
|
ServerControlTab(
|
|
context = context,
|
|
currentLanguage = currentLanguage,
|
|
serverStatus = serverStatus,
|
|
isServerInstalled = isServerInstalled,
|
|
sendHotfix = sendHotfix,
|
|
serverKey = serverKey,
|
|
trailblazerConfig = trailblazerConfig,
|
|
currentSceneConfig = currentSceneConfig,
|
|
serverLogs = serverLogs,
|
|
onStartServer = { ServerService.start(context) },
|
|
onStopServer = { ServerService.stop(context) },
|
|
onRestartServer = { ServerService.restart(context) },
|
|
onToggleHotfix = { enabled ->
|
|
scope.launch {
|
|
persistentRepo.setSendHotfix(enabled)
|
|
sendHotfix = enabled
|
|
snackbarHostState.showSnackbar("send_hotfix: $enabled")
|
|
}
|
|
},
|
|
onOpenSetPassword = { showPasswordDialog = true },
|
|
onOpenEditTrailblazer = { showTrailblazerDialog = true },
|
|
onOpenExRank = { showExRankDialog = true },
|
|
onOpenMiscExplorer = {
|
|
scope.launch {
|
|
miscFiles = persistentRepo.listMiscFiles()
|
|
showMiscExplorerDialog = true
|
|
}
|
|
},
|
|
onClearLogs = { ServerService.clearLogs() },
|
|
onCopyLogs = {
|
|
val clip = ClipData.newPlainText("ppSR Logs", serverLogs.joinToString("\n"))
|
|
val cm = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
cm.setPrimaryClip(clip)
|
|
Toast.makeText(context, tr(currentLanguage, "Logs copied to clipboard", "คัดลอกบันทึกแล้ว", "日志已复制"), Toast.LENGTH_SHORT).show()
|
|
}
|
|
)
|
|
}
|
|
ScreenTab.TELEPORT -> {
|
|
TeleportTab(
|
|
context = context,
|
|
planets = planets,
|
|
selectedPlanetId = selectedPlanetId,
|
|
currentLanguage = currentLanguage,
|
|
searchQuery = searchQuery,
|
|
selectedCategory = selectedCategory,
|
|
currentSceneConfig = currentSceneConfig,
|
|
onSelectPlanet = { selectedPlanetId = it },
|
|
onSearchChange = { searchQuery = it },
|
|
onCategoryChange = { selectedCategory = it },
|
|
onTeleport = { scene, planetName ->
|
|
scope.launch {
|
|
val wasRunning = serverStatus == ServerStatus.RUNNING
|
|
if (wasRunning) {
|
|
snackbarHostState.showSnackbar(tr(currentLanguage, "Pausing server to update scene...", "กำลังพักเซิร์ฟเวอร์เพื่อเปลี่ยนฉาก...", "正在暂停服务器更新场景..."))
|
|
ServerService.stop(context)
|
|
delay(1000)
|
|
}
|
|
|
|
val cx = scene.calyxX?.toInt() ?: scene.x.toInt()
|
|
val cy = scene.calyxY?.toInt() ?: scene.y.toInt()
|
|
val cz = scene.calyxZ?.toInt() ?: (scene.z.toInt() + 1800)
|
|
|
|
val payload = SceneTeleportPayload(
|
|
planeId = scene.planeId.toInt(),
|
|
x = scene.x.toInt(),
|
|
y = scene.y.toInt(),
|
|
z = scene.z.toInt(),
|
|
calyxX = cx,
|
|
calyxY = cy,
|
|
calyxZ = cz,
|
|
mapLayer = scene.mapLayer,
|
|
calyxGroupId = scene.calyxGroupId.toInt(),
|
|
calyxInstId = scene.calyxInstId.toInt(),
|
|
calyxEntityId = scene.calyxEntityId.toInt(),
|
|
calyxPropId = scene.calyxPropId.toInt()
|
|
)
|
|
|
|
val res = persistentRepo.applySceneTeleport(payload)
|
|
if (res.isSuccess) {
|
|
currentSceneConfig = SceneConfig.fromScene(scene)
|
|
snackbarHostState.showSnackbar(tr(currentLanguage, "Teleported to ${scene.localizedName(currentLanguage)}!", "วาร์ปไป ${scene.localizedName(currentLanguage)} สำเร็จ!", "已传送至 ${scene.localizedName(currentLanguage)}!"))
|
|
if (wasRunning) {
|
|
delay(500)
|
|
ServerService.start(context)
|
|
}
|
|
} else {
|
|
snackbarHostState.showSnackbar(tr(currentLanguage, "Failed: ${res.exceptionOrNull()?.message}", "ล้มเหลว: ${res.exceptionOrNull()?.message}", "失败: ${res.exceptionOrNull()?.message}"))
|
|
}
|
|
}
|
|
},
|
|
onOpenCustomCoords = { showCustomCoordsDialog = true }
|
|
)
|
|
}
|
|
ScreenTab.UPDATER -> {
|
|
UpdaterTab(
|
|
context = context,
|
|
currentLanguage = currentLanguage,
|
|
installedVersion = installedVersion,
|
|
updateInfo = updateInfo,
|
|
isChecking = isCheckingUpdate,
|
|
isInstalling = isInstalling,
|
|
installProgress = installProgress,
|
|
installMessage = installStatusMessage,
|
|
appReleaseInfo = appReleaseInfo,
|
|
isCheckingApp = isCheckingAppUpdate,
|
|
isDownloadingApk = isDownloadingApk,
|
|
apkProgress = apkDownloadProgress,
|
|
onCheckUpdate = {
|
|
scope.launch {
|
|
isCheckingUpdate = true
|
|
val res = updateRepo.checkUpdate()
|
|
isCheckingUpdate = false
|
|
if (res.isSuccess) {
|
|
updateInfo = res.getOrNull()
|
|
snackbarHostState.showSnackbar(tr(currentLanguage, "Remote latest: ${updateInfo?.latestVersion}", "เวอร์ชันล่าสุดบนเซิร์ฟเวอร์: ${updateInfo?.latestVersion}", "远程最新版本: ${updateInfo?.latestVersion}"))
|
|
} else {
|
|
snackbarHostState.showSnackbar(tr(currentLanguage, "Check failed: ${res.exceptionOrNull()?.message}", "ตรวจหาไม่สำเร็จ: ${res.exceptionOrNull()?.message}", "检查失败: ${res.exceptionOrNull()?.message}"))
|
|
}
|
|
}
|
|
},
|
|
onOpenVersionSelector = {
|
|
scope.launch {
|
|
isCheckingUpdate = true
|
|
val res = updateRepo.fetchAvailableVersions()
|
|
isCheckingUpdate = false
|
|
if (res.isSuccess) {
|
|
val versions = res.getOrNull() ?: emptyList()
|
|
val latest = versions.lastOrNull() ?: ""
|
|
updateInfo = UpdateInfo(
|
|
currentVersion = installedVersion,
|
|
latestVersion = latest,
|
|
availableVersions = versions,
|
|
hasUpdate = installedVersion.isBlank() || installedVersion != latest
|
|
)
|
|
showVersionSelectorDialog = true
|
|
} else {
|
|
snackbarHostState.showSnackbar(tr(currentLanguage, "Failed to fetch versions", "ไม่สามารถดึงรายการเวอร์ชันได้", "获取版本列表失败"))
|
|
}
|
|
}
|
|
},
|
|
onInstall = { versionToInstall ->
|
|
scope.launch {
|
|
if (serverStatus == ServerStatus.RUNNING) {
|
|
ServerService.stop(context)
|
|
delay(1000)
|
|
}
|
|
isInstalling = true
|
|
val res = updateRepo.downloadAndInstall(versionToInstall) { p, msg ->
|
|
installProgress = p
|
|
installStatusMessage = msg
|
|
}
|
|
isInstalling = false
|
|
if (res.isSuccess) {
|
|
installedVersion = versionToInstall
|
|
isServerInstalled = true
|
|
// Refresh updateInfo state
|
|
updateInfo?.let { prev ->
|
|
updateInfo = prev.copy(
|
|
currentVersion = versionToInstall,
|
|
hasUpdate = versionToInstall != prev.latestVersion
|
|
)
|
|
}
|
|
snackbarHostState.showSnackbar(tr(currentLanguage, "Server v$versionToInstall installed successfully!", "ติดตั้งเซิร์ฟเวอร์ v$versionToInstall สำเร็จแล้ว!", "服务器 v$versionToInstall 安装成功!"))
|
|
} else {
|
|
snackbarHostState.showSnackbar(tr(currentLanguage, "Install error: ${res.exceptionOrNull()?.message}", "ติดตั้งล้มเหลว: ${res.exceptionOrNull()?.message}", "安装错误: ${res.exceptionOrNull()?.message}"))
|
|
}
|
|
}
|
|
},
|
|
includePrerelease = includePrerelease,
|
|
onTogglePrerelease = { newVal ->
|
|
includePrerelease = newVal
|
|
AppUpdateManager.setIncludePrerelease(context, newVal)
|
|
scope.launch {
|
|
isCheckingAppUpdate = true
|
|
val res = AppUpdateManager.checkForAppUpdate(newVal)
|
|
isCheckingAppUpdate = false
|
|
res.onSuccess { info ->
|
|
appReleaseInfo = info
|
|
}
|
|
}
|
|
},
|
|
onCheckAppUpdate = {
|
|
scope.launch {
|
|
isCheckingAppUpdate = true
|
|
val res = AppUpdateManager.checkForAppUpdate(includePrerelease)
|
|
isCheckingAppUpdate = false
|
|
res.onSuccess { info ->
|
|
appReleaseInfo = info
|
|
if (info != null && info.isNewer) {
|
|
showAppUpdateDialog = true
|
|
} else {
|
|
snackbarHostState.showSnackbar(
|
|
tr(currentLanguage, "App is already up to date (${BuildConfig.VERSION_NAME})", "แอปพลิเคชันเป็นเวอร์ชันล่าสุดแล้ว (${BuildConfig.VERSION_NAME})", "应用程序已是最新版本 (${BuildConfig.VERSION_NAME})")
|
|
)
|
|
}
|
|
}.onFailure {
|
|
snackbarHostState.showSnackbar(
|
|
tr(currentLanguage, "Check failed: ${it.message}", "ตรวจหาไม่สำเร็จ: ${it.message}", "检查失败: ${it.message}")
|
|
)
|
|
}
|
|
}
|
|
},
|
|
onDownloadApk = {
|
|
appReleaseInfo?.apkDownloadUrl?.let { url ->
|
|
scope.launch {
|
|
isDownloadingApk = true
|
|
val res = AppUpdateManager.downloadApk(context, url) { p ->
|
|
apkDownloadProgress = p
|
|
}
|
|
isDownloadingApk = false
|
|
res.onSuccess { apkFile ->
|
|
AppUpdateManager.promptInstallApk(context, apkFile)
|
|
}.onFailure {
|
|
snackbarHostState.showSnackbar(
|
|
tr(currentLanguage, "Download failed: ${it.message}", "ดาวน์โหลดไม่สำเร็จ: ${it.message}", "下载失败: ${it.message}")
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Dialogs with Full Localization ---
|
|
|
|
// Password Dialog
|
|
if (showPasswordDialog) {
|
|
var newKey by remember { mutableStateOf(serverKey) }
|
|
AlertDialog(
|
|
onDismissRequest = { showPasswordDialog = false },
|
|
title = { Text(tr(currentLanguage, "Set Server Password / Key", "ตั้งรหัสผ่าน / คีย์เซิร์ฟเวอร์", "设置服务器密码/密钥")) },
|
|
text = {
|
|
Column {
|
|
Text(
|
|
tr(currentLanguage, "Leave blank if no password is required by your client.", "ปล่อยว่างไว้หากตัวเกมไคลเอนต์ไม่ต้องการรหัสผ่าน", "如果客户端不需要密码,请留空。"),
|
|
fontSize = 13.sp,
|
|
color = Color.Gray
|
|
)
|
|
Spacer(modifier = Modifier.height(12.dp))
|
|
OutlinedTextField(
|
|
value = newKey,
|
|
onValueChange = { newKey = it },
|
|
label = { Text(tr(currentLanguage, "Key / Password", "คีย์ / รหัสผ่าน", "密钥 / 密码")) },
|
|
singleLine = true,
|
|
modifier = Modifier.fillMaxWidth()
|
|
)
|
|
}
|
|
},
|
|
confirmButton = {
|
|
Button(onClick = {
|
|
scope.launch {
|
|
persistentRepo.setServerKey(newKey)
|
|
serverKey = newKey
|
|
showPasswordDialog = false
|
|
snackbarHostState.showSnackbar(tr(currentLanguage, "Password updated", "บันทึกรหัสผ่านแล้ว", "密码已更新"))
|
|
}
|
|
}) {
|
|
Text(tr(currentLanguage, "Save", "บันทึก", "保存"))
|
|
}
|
|
},
|
|
dismissButton = {
|
|
TextButton(onClick = { showPasswordDialog = false }) {
|
|
Text(tr(currentLanguage, "Cancel", "ยกเลิก", "取消"))
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
// Trailblazer Dialog
|
|
if (showTrailblazerDialog) {
|
|
var gender by remember { mutableStateOf(trailblazerConfig.gender) }
|
|
var tbPath by remember { mutableStateOf(trailblazerConfig.tbPath) }
|
|
var marchPath by remember { mutableStateOf(trailblazerConfig.marchPath) }
|
|
|
|
val paths = listOf("Destruction", "Preservation", "Harmony", "Remembrance", "Elation")
|
|
val marchPaths = listOf("Preservation", "Hunt")
|
|
|
|
AlertDialog(
|
|
onDismissRequest = { showTrailblazerDialog = false },
|
|
title = { Text(tr(currentLanguage, "Edit Trailblazer (ตัวละคร)", "แก้ไขตัวละครผู้บุกเบิก (Trailblazer)", "编辑开拓者角色")) },
|
|
text = {
|
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
Text(tr(currentLanguage, "Trailblazer Gender:", "เพศตัวเอก:", "开拓者性别:"), fontWeight = FontWeight.Bold, fontSize = 13.sp)
|
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
FilterChip(
|
|
selected = gender == "Female",
|
|
onClick = { gender = "Female" },
|
|
label = { Text(tr(currentLanguage, "Female (Stelle)", "หญิง (Stelle)", "女性 (星)")) }
|
|
)
|
|
FilterChip(
|
|
selected = gender == "Male",
|
|
onClick = { gender = "Male" },
|
|
label = { Text(tr(currentLanguage, "Male (Caelus)", "ชาย (Caelus)", "男性 (穹)")) }
|
|
)
|
|
}
|
|
|
|
Text(tr(currentLanguage, "Trailblazer Path:", "Path ของตัวเอก:", "开拓者命途:"), fontWeight = FontWeight.Bold, fontSize = 13.sp)
|
|
LazyRow(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
|
items(paths) { p ->
|
|
FilterChip(
|
|
selected = tbPath == p,
|
|
onClick = { tbPath = p },
|
|
label = { Text(p) }
|
|
)
|
|
}
|
|
}
|
|
|
|
Text(tr(currentLanguage, "March 7th Path:", "Path ของ March 7th:", "三月七命途:"), fontWeight = FontWeight.Bold, fontSize = 13.sp)
|
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
marchPaths.forEach { mp ->
|
|
FilterChip(
|
|
selected = marchPath == mp,
|
|
onClick = { marchPath = mp },
|
|
label = { Text(mp) }
|
|
)
|
|
}
|
|
}
|
|
}
|
|
},
|
|
confirmButton = {
|
|
Button(onClick = {
|
|
scope.launch {
|
|
val newCfg = TrailblazerConfig(gender, tbPath, marchPath)
|
|
persistentRepo.setTrailblazerConfig(newCfg)
|
|
trailblazerConfig = newCfg
|
|
showTrailblazerDialog = false
|
|
snackbarHostState.showSnackbar(tr(currentLanguage, "Trailblazer profile saved", "บันทึกข้อมูลตัวละครแล้ว", "主角配置已保存"))
|
|
}
|
|
}) {
|
|
Text(tr(currentLanguage, "Save", "บันทึก", "保存"))
|
|
}
|
|
},
|
|
dismissButton = {
|
|
TextButton(onClick = { showTrailblazerDialog = false }) {
|
|
Text(tr(currentLanguage, "Cancel", "ยกเลิก", "取消"))
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
// EX Eidolon (3 Ex Rank) Dialog
|
|
if (showExRankDialog) {
|
|
var currentConfigs by remember { mutableStateOf(exRankConfigs) }
|
|
|
|
AlertDialog(
|
|
onDismissRequest = { showExRankDialog = false },
|
|
title = {
|
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
Icon(
|
|
Icons.Default.Star,
|
|
contentDescription = null,
|
|
tint = ExploreAmber,
|
|
modifier = Modifier.size(22.dp)
|
|
)
|
|
Spacer(modifier = Modifier.width(8.dp))
|
|
Text(
|
|
tr(
|
|
currentLanguage,
|
|
"EX Eidolon (3 EX Rank)",
|
|
"ปรับแต่ง EX Eidolon (3 EX Rank)",
|
|
"EX星魂强化设置 (3 EX Rank)"
|
|
),
|
|
fontWeight = FontWeight.Bold,
|
|
fontSize = 17.sp
|
|
)
|
|
}
|
|
},
|
|
text = {
|
|
Column(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
verticalArrangement = Arrangement.spacedBy(10.dp)
|
|
) {
|
|
Text(
|
|
tr(
|
|
currentLanguage,
|
|
"Toggle 3 EX Eidolon skill upgrades for special characters in ppSR:",
|
|
"เปิด/ปิดระบบ 3 EX Eidolon ปลดล็อกสกิลพิเศษสำหรับตัวละครใน ppSR:",
|
|
"为ppSR特殊角色开启/关闭3个EX星魂技能强化:"
|
|
),
|
|
fontSize = 12.sp,
|
|
color = Color.LightGray
|
|
)
|
|
|
|
currentConfigs.forEachIndexed { index, cfg ->
|
|
Card(
|
|
colors = CardDefaults.cardColors(containerColor = BgCard),
|
|
shape = RoundedCornerShape(10.dp),
|
|
modifier = Modifier.fillMaxWidth()
|
|
) {
|
|
Row(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(horizontal = 12.dp, vertical = 8.dp),
|
|
horizontalArrangement = Arrangement.SpaceBetween,
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
Column(modifier = Modifier.weight(1f)) {
|
|
val displayName = when (cfg.avatarId) {
|
|
1204 -> tr(currentLanguage, "JingYuan (General)", "JingYuan (จิ่งหยวน)", "景元")
|
|
1213 -> tr(currentLanguage, "DanHeng IL (Imbibitor Lunae)", "DanHeng IL (จ้าวยลจันทรา)", "丹恒·饮月")
|
|
1308 -> tr(currentLanguage, "Acheron (Galaxy Ranger)", "Acheron (อาเคอรอน)", "黄泉")
|
|
else -> cfg.name
|
|
}
|
|
Text(displayName, fontWeight = FontWeight.SemiBold, fontSize = 14.sp)
|
|
Text(
|
|
"Avatar ID: ${cfg.avatarId} • ${if (cfg.active) tr(currentLanguage, "Active", "เปิดใช้งาน", "已启用") else tr(currentLanguage, "Disabled", "ปิดใช้งาน", "已禁用")}",
|
|
fontSize = 11.sp,
|
|
color = if (cfg.active) CombatGreen else Color.Gray
|
|
)
|
|
}
|
|
Switch(
|
|
checked = cfg.active,
|
|
onCheckedChange = { isActive ->
|
|
currentConfigs = currentConfigs.toMutableList().also {
|
|
it[index] = it[index].copy(active = isActive)
|
|
}
|
|
}
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
Row(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
) {
|
|
OutlinedButton(
|
|
onClick = {
|
|
currentConfigs = currentConfigs.map { it.copy(active = true) }
|
|
},
|
|
modifier = Modifier.weight(1f),
|
|
contentPadding = PaddingValues(vertical = 4.dp)
|
|
) {
|
|
Text(tr(currentLanguage, "Enable All", "เปิดทั้งหมด", "全部开启"), fontSize = 11.sp)
|
|
}
|
|
OutlinedButton(
|
|
onClick = {
|
|
currentConfigs = currentConfigs.map { it.copy(active = false) }
|
|
},
|
|
modifier = Modifier.weight(1f),
|
|
contentPadding = PaddingValues(vertical = 4.dp)
|
|
) {
|
|
Text(tr(currentLanguage, "Disable All", "ปิดทั้งหมด", "全部关闭"), fontSize = 11.sp)
|
|
}
|
|
}
|
|
}
|
|
},
|
|
confirmButton = {
|
|
Button(onClick = {
|
|
scope.launch {
|
|
persistentRepo.setExRankConfigs(currentConfigs)
|
|
exRankConfigs = currentConfigs
|
|
showExRankDialog = false
|
|
snackbarHostState.showSnackbar(
|
|
tr(
|
|
currentLanguage,
|
|
"EX Eidolon configurations saved",
|
|
"บันทึกการตั้งค่า EX Eidolon สำเร็จ",
|
|
"EX星魂配置已保存"
|
|
)
|
|
)
|
|
}
|
|
}) {
|
|
Text(tr(currentLanguage, "Save", "บันทึก", "保存"))
|
|
}
|
|
},
|
|
dismissButton = {
|
|
TextButton(onClick = { showExRankDialog = false }) {
|
|
Text(tr(currentLanguage, "Cancel", "ยกเลิก", "取消"))
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
// Misc Explorer & Editor Dialog
|
|
if (showMiscExplorerDialog) {
|
|
AlertDialog(
|
|
onDismissRequest = {
|
|
showMiscExplorerDialog = false
|
|
selectedMiscFile = null
|
|
},
|
|
title = {
|
|
Text(
|
|
if (selectedMiscFile == null)
|
|
tr(currentLanguage, "Misc Files Explorer", "จัดการไฟล์ในโฟลเดอร์ Misc", "杂项文件管理器")
|
|
else
|
|
"${tr(currentLanguage, "Edit", "แก้ไข", "编辑")}: ${selectedMiscFile?.name}"
|
|
)
|
|
},
|
|
text = {
|
|
if (selectedMiscFile == null) {
|
|
LazyColumn(modifier = Modifier.fillMaxWidth().heightIn(max = 350.dp)) {
|
|
items(miscFiles) { file ->
|
|
Card(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(vertical = 4.dp)
|
|
.clickable {
|
|
scope.launch {
|
|
selectedMiscFile = file
|
|
miscFileContent = persistentRepo.readMiscFile(file.name).getOrDefault("")
|
|
}
|
|
},
|
|
colors = CardDefaults.cardColors(containerColor = BgCardLight)
|
|
) {
|
|
Row(
|
|
modifier = Modifier.padding(12.dp),
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
Icon(Icons.AutoMirrored.Filled.InsertDriveFile, contentDescription = null, tint = PurplePrimary)
|
|
Spacer(modifier = Modifier.width(10.dp))
|
|
Column {
|
|
Text(file.name, fontWeight = FontWeight.SemiBold, fontSize = 14.sp)
|
|
Text("${file.length()} bytes", fontSize = 11.sp, color = Color.Gray)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
OutlinedTextField(
|
|
value = miscFileContent,
|
|
onValueChange = { miscFileContent = it },
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.height(350.dp),
|
|
textStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace, fontSize = 12.sp)
|
|
)
|
|
}
|
|
},
|
|
confirmButton = {
|
|
if (selectedMiscFile != null) {
|
|
Button(onClick = {
|
|
scope.launch {
|
|
selectedMiscFile?.let { file ->
|
|
persistentRepo.writeMiscFile(file.name, miscFileContent)
|
|
snackbarHostState.showSnackbar("${file.name} ${tr(currentLanguage, "saved successfully", "บันทึกสำเร็จ", "保存成功")}")
|
|
selectedMiscFile = null
|
|
miscFiles = persistentRepo.listMiscFiles()
|
|
}
|
|
}
|
|
}) {
|
|
Text(tr(currentLanguage, "Save File", "บันทึกไฟล์", "保存文件"))
|
|
}
|
|
} else {
|
|
TextButton(onClick = { showMiscExplorerDialog = false }) {
|
|
Text(tr(currentLanguage, "Close", "ปิด", "关闭"))
|
|
}
|
|
}
|
|
},
|
|
dismissButton = {
|
|
if (selectedMiscFile != null) {
|
|
TextButton(onClick = { selectedMiscFile = null }) {
|
|
Text(tr(currentLanguage, "Back to list", "ย้อนกลับ", "返回列表"))
|
|
}
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
// Version Selector Dialog
|
|
if (showVersionSelectorDialog) {
|
|
val versions = updateInfo?.availableVersions ?: emptyList()
|
|
AlertDialog(
|
|
onDismissRequest = { showVersionSelectorDialog = false },
|
|
title = { Text(tr(currentLanguage, "Select ppSR Server Version", "เลือกเวอร์ชันเซิร์ฟเวอร์ ppSR", "选择 ppSR 服务器版本")) },
|
|
text = {
|
|
LazyColumn(modifier = Modifier.fillMaxWidth().heightIn(max = 300.dp)) {
|
|
items(versions.reversed()) { ver ->
|
|
val isInstalled = ver == installedVersion
|
|
Row(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.clickable {
|
|
showVersionSelectorDialog = false
|
|
scope.launch {
|
|
if (serverStatus == ServerStatus.RUNNING) {
|
|
ServerService.stop(context)
|
|
delay(1000)
|
|
}
|
|
isInstalling = true
|
|
val res = updateRepo.downloadAndInstall(ver) { p, msg ->
|
|
installProgress = p
|
|
installStatusMessage = msg
|
|
}
|
|
isInstalling = false
|
|
if (res.isSuccess) {
|
|
installedVersion = ver
|
|
isServerInstalled = true
|
|
updateInfo?.let { prev ->
|
|
updateInfo = prev.copy(
|
|
currentVersion = ver,
|
|
hasUpdate = ver != prev.latestVersion
|
|
)
|
|
}
|
|
snackbarHostState.showSnackbar("v$ver ${tr(currentLanguage, "installed successfully!", "ติดตั้งสำเร็จแล้ว!", "安装成功!")}")
|
|
} else {
|
|
snackbarHostState.showSnackbar("${tr(currentLanguage, "Failed", "ล้มเหลว", "失败")}: ${res.exceptionOrNull()?.message}")
|
|
}
|
|
}
|
|
}
|
|
.padding(vertical = 10.dp, horizontal = 8.dp),
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
Text(
|
|
text = ver,
|
|
fontWeight = if (isInstalled) FontWeight.Bold else FontWeight.Normal,
|
|
color = if (isInstalled) PurplePrimary else Color.White,
|
|
modifier = Modifier.weight(1f)
|
|
)
|
|
if (isInstalled) {
|
|
Text(
|
|
"(${tr(currentLanguage, "Active", "กำลังใช้งาน", "当前使用")})",
|
|
fontSize = 12.sp,
|
|
color = CombatGreen
|
|
)
|
|
}
|
|
}
|
|
HorizontalDivider(color = Color.DarkGray)
|
|
}
|
|
}
|
|
},
|
|
confirmButton = {
|
|
TextButton(onClick = { showVersionSelectorDialog = false }) {
|
|
Text(tr(currentLanguage, "Close", "ปิด", "关闭"))
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
// App Update Dialog (APK)
|
|
if (showAppUpdateDialog && appReleaseInfo != null) {
|
|
val release = appReleaseInfo!!
|
|
AlertDialog(
|
|
onDismissRequest = { if (!isDownloadingApk) showAppUpdateDialog = false },
|
|
title = {
|
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
Text(
|
|
tr(currentLanguage, "New App Update", "พบอัปเดตแอปใหม่", "发现新版本应用"),
|
|
fontWeight = FontWeight.Bold,
|
|
color = Color.White
|
|
)
|
|
Spacer(modifier = Modifier.width(8.dp))
|
|
if (release.isPrerelease) {
|
|
Surface(
|
|
shape = RoundedCornerShape(4.dp),
|
|
color = ExploreAmber.copy(alpha = 0.2f)
|
|
) {
|
|
Text(
|
|
text = "BETA",
|
|
color = ExploreAmber,
|
|
fontSize = 11.sp,
|
|
fontWeight = FontWeight.Bold,
|
|
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
|
|
)
|
|
}
|
|
} else {
|
|
Surface(
|
|
shape = RoundedCornerShape(4.dp),
|
|
color = CombatGreen.copy(alpha = 0.2f)
|
|
) {
|
|
Text(
|
|
text = "STABLE",
|
|
color = CombatGreen,
|
|
fontSize = 11.sp,
|
|
fontWeight = FontWeight.Bold,
|
|
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
},
|
|
text = {
|
|
Column {
|
|
Text(
|
|
tr(currentLanguage, "Current: ${BuildConfig.VERSION_NAME} -> Latest: ${release.tagName}", "เวอร์ชันปัจจุบัน: ${BuildConfig.VERSION_NAME} -> ล่าสุด: ${release.tagName}", "当前版本: ${BuildConfig.VERSION_NAME} -> 最新: ${release.tagName}"),
|
|
fontSize = 13.sp,
|
|
color = Color.LightGray
|
|
)
|
|
if (release.changelog.isNotBlank()) {
|
|
Spacer(modifier = Modifier.height(8.dp))
|
|
Text(
|
|
release.changelog,
|
|
fontSize = 12.sp,
|
|
color = Color.Gray,
|
|
maxLines = 6,
|
|
overflow = TextOverflow.Ellipsis
|
|
)
|
|
}
|
|
if (isDownloadingApk) {
|
|
Spacer(modifier = Modifier.height(12.dp))
|
|
LinearProgressIndicator(
|
|
progress = { apkDownloadProgress / 100f },
|
|
modifier = Modifier.fillMaxWidth(),
|
|
color = PurplePrimary
|
|
)
|
|
Spacer(modifier = Modifier.height(4.dp))
|
|
Text(
|
|
tr(currentLanguage, "Downloading APK: $apkDownloadProgress%", "กำลังดาวน์โหลดไฟล์ APK: $apkDownloadProgress%", "正在下载安装包: $apkDownloadProgress%"),
|
|
fontSize = 11.sp,
|
|
color = Color.Gray
|
|
)
|
|
}
|
|
}
|
|
},
|
|
confirmButton = {
|
|
Button(
|
|
onClick = {
|
|
if (!isDownloadingApk && release.apkDownloadUrl.isNotBlank()) {
|
|
scope.launch {
|
|
isDownloadingApk = true
|
|
val res = AppUpdateManager.downloadApk(context, release.apkDownloadUrl) { p ->
|
|
apkDownloadProgress = p
|
|
}
|
|
isDownloadingApk = false
|
|
res.onSuccess { apkFile ->
|
|
showAppUpdateDialog = false
|
|
AppUpdateManager.promptInstallApk(context, apkFile)
|
|
}.onFailure {
|
|
snackbarHostState.showSnackbar(
|
|
tr(currentLanguage, "Download failed: ${it.message}", "ดาวน์โหลดไม่สำเร็จ: ${it.message}", "下载失败: ${it.message}")
|
|
)
|
|
}
|
|
}
|
|
}
|
|
},
|
|
enabled = !isDownloadingApk && release.apkDownloadUrl.isNotBlank(),
|
|
colors = ButtonDefaults.buttonColors(containerColor = CombatGreen)
|
|
) {
|
|
Text(tr(currentLanguage, "Update Now", "อัปเดตทันที", "立即更新"))
|
|
}
|
|
},
|
|
dismissButton = {
|
|
if (!isDownloadingApk) {
|
|
TextButton(onClick = { showAppUpdateDialog = false }) {
|
|
Text(tr(currentLanguage, "Later", "ไว้ภายหลัง", "稍后"))
|
|
}
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
// Custom Coordinates Dialog
|
|
if (showCustomCoordsDialog) {
|
|
var planeIdStr by remember { mutableStateOf(currentSceneConfig?.planeId?.toString() ?: "20101") }
|
|
var xStr by remember { mutableStateOf(currentSceneConfig?.x?.toString() ?: "0") }
|
|
var yStr by remember { mutableStateOf(currentSceneConfig?.y?.toString() ?: "0") }
|
|
var zStr by remember { mutableStateOf(currentSceneConfig?.z?.toString() ?: "0") }
|
|
var layerStr by remember { mutableStateOf(currentSceneConfig?.mapLayer?.toString() ?: "1") }
|
|
|
|
AlertDialog(
|
|
onDismissRequest = { showCustomCoordsDialog = false },
|
|
title = { Text(tr(currentLanguage, "Custom Coordinates", "กำหนดพิกัดเอง (Custom Coordinates)", "自定义坐标")) },
|
|
text = {
|
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
OutlinedTextField(
|
|
value = planeIdStr,
|
|
onValueChange = { planeIdStr = it },
|
|
label = { Text("Plane ID") },
|
|
singleLine = true,
|
|
modifier = Modifier.fillMaxWidth()
|
|
)
|
|
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
|
OutlinedTextField(
|
|
value = xStr,
|
|
onValueChange = { xStr = it },
|
|
label = { Text("X") },
|
|
singleLine = true,
|
|
modifier = Modifier.weight(1f)
|
|
)
|
|
OutlinedTextField(
|
|
value = yStr,
|
|
onValueChange = { yStr = it },
|
|
label = { Text("Y") },
|
|
singleLine = true,
|
|
modifier = Modifier.weight(1f)
|
|
)
|
|
OutlinedTextField(
|
|
value = zStr,
|
|
onValueChange = { zStr = it },
|
|
label = { Text("Z") },
|
|
singleLine = true,
|
|
modifier = Modifier.weight(1f)
|
|
)
|
|
}
|
|
OutlinedTextField(
|
|
value = layerStr,
|
|
onValueChange = { layerStr = it },
|
|
label = { Text(tr(currentLanguage, "Map Layer (1, 2, 3...)", "ชั้นของแผนที่ (Map Layer)", "地图层级 (1, 2, 3...)")) },
|
|
singleLine = true,
|
|
modifier = Modifier.fillMaxWidth()
|
|
)
|
|
}
|
|
},
|
|
confirmButton = {
|
|
Button(onClick = {
|
|
val pId = planeIdStr.toIntOrNull() ?: 20101
|
|
val x = xStr.toIntOrNull() ?: 0
|
|
val y = yStr.toIntOrNull() ?: 0
|
|
val z = zStr.toIntOrNull() ?: 0
|
|
val layer = layerStr.toIntOrNull() ?: 1
|
|
|
|
scope.launch {
|
|
val wasRunning = serverStatus == ServerStatus.RUNNING
|
|
if (wasRunning) {
|
|
ServerService.stop(context)
|
|
delay(1000)
|
|
}
|
|
|
|
val payload = SceneTeleportPayload(
|
|
planeId = pId,
|
|
x = x,
|
|
y = y,
|
|
z = z,
|
|
calyxX = x,
|
|
calyxY = y,
|
|
calyxZ = z + 1800,
|
|
mapLayer = layer
|
|
)
|
|
val res = persistentRepo.applySceneTeleport(payload)
|
|
showCustomCoordsDialog = false
|
|
if (res.isSuccess) {
|
|
currentSceneConfig = SceneConfig(
|
|
planeId = pId.toLong(),
|
|
x = x.toLong(),
|
|
y = y.toLong(),
|
|
z = z.toLong(),
|
|
mapLayer = layer
|
|
)
|
|
snackbarHostState.showSnackbar(tr(currentLanguage, "Custom scene coordinates applied!", "บันทึกพิกัดฉากเรียบร้อยแล้ว!", "已应用自定义场景坐标!"))
|
|
if (wasRunning) {
|
|
delay(500)
|
|
ServerService.start(context)
|
|
}
|
|
}
|
|
}
|
|
}) {
|
|
Text(tr(currentLanguage, "Apply", "นำไปใช้", "应用"))
|
|
}
|
|
},
|
|
dismissButton = {
|
|
TextButton(onClick = { showCustomCoordsDialog = false }) {
|
|
Text(tr(currentLanguage, "Cancel", "ยกเลิก", "取消"))
|
|
}
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun ServerControlTab(
|
|
context: Context,
|
|
currentLanguage: AppLanguage,
|
|
serverStatus: ServerStatus,
|
|
isServerInstalled: Boolean,
|
|
sendHotfix: Boolean,
|
|
serverKey: String,
|
|
trailblazerConfig: TrailblazerConfig,
|
|
currentSceneConfig: SceneConfig?,
|
|
serverLogs: List<String>,
|
|
onStartServer: () -> Unit,
|
|
onStopServer: () -> Unit,
|
|
onRestartServer: () -> Unit,
|
|
onToggleHotfix: (Boolean) -> Unit,
|
|
onOpenSetPassword: () -> Unit,
|
|
onOpenEditTrailblazer: () -> Unit,
|
|
onOpenExRank: () -> Unit,
|
|
onOpenMiscExplorer: () -> Unit,
|
|
onClearLogs: () -> Unit,
|
|
onCopyLogs: () -> Unit
|
|
) {
|
|
val listState = rememberLazyListState()
|
|
|
|
// Auto-scroll logs
|
|
LaunchedEffect(serverLogs.size) {
|
|
if (serverLogs.isNotEmpty()) {
|
|
listState.animateScrollToItem(serverLogs.size - 1)
|
|
}
|
|
}
|
|
|
|
Column(
|
|
modifier = Modifier
|
|
.fillMaxSize()
|
|
.padding(16.dp),
|
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
|
) {
|
|
// Status & Control Card
|
|
Card(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
colors = CardDefaults.cardColors(containerColor = BgCard),
|
|
shape = RoundedCornerShape(16.dp)
|
|
) {
|
|
Column(modifier = Modifier.padding(16.dp)) {
|
|
Row(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
horizontalArrangement = Arrangement.SpaceBetween,
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
Column {
|
|
Text(
|
|
tr(currentLanguage, "Server Status", "สถานะเซิร์ฟเวอร์", "服务器状态"),
|
|
fontSize = 12.sp,
|
|
color = Color.Gray
|
|
)
|
|
Text(
|
|
text = when (serverStatus) {
|
|
ServerStatus.RUNNING -> tr(currentLanguage, "🟢 Running (Online)", "🟢 กำลังทำงาน (ออนไลน์)", "🟢 运行中 (在线)")
|
|
ServerStatus.STARTING -> tr(currentLanguage, "🟡 Starting...", "🟡 กำลังเริ่มเซิร์ฟเวอร์...", "🟡 正在启动...")
|
|
ServerStatus.STOPPING -> tr(currentLanguage, "🟠 Stopping...", "🟠 กำลังหยุดเซิร์ฟเวอร์...", "🟠 正在停止...")
|
|
ServerStatus.ERROR -> tr(currentLanguage, "🔴 Error", "🔴 ข้อผิดพลาด", "🔴 错误")
|
|
ServerStatus.STOPPED -> tr(currentLanguage, "⚪ Stopped (Offline)", "⚪ ปิดอยู่ (ออฟไลน์)", "⚪ 已停止 (离线)")
|
|
},
|
|
fontWeight = FontWeight.Bold,
|
|
fontSize = 17.sp,
|
|
color = Color.White
|
|
)
|
|
}
|
|
|
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
if (serverStatus == ServerStatus.RUNNING) {
|
|
Button(
|
|
onClick = onStopServer,
|
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFC62828))
|
|
) {
|
|
Icon(Icons.Default.Stop, contentDescription = null)
|
|
Spacer(modifier = Modifier.width(4.dp))
|
|
Text(tr(currentLanguage, "Stop", "หยุด", "停止"))
|
|
}
|
|
IconButton(onClick = onRestartServer) {
|
|
Icon(Icons.Default.Refresh, contentDescription = "Restart", tint = PurplePrimary)
|
|
}
|
|
} else {
|
|
Button(
|
|
onClick = onStartServer,
|
|
enabled = isServerInstalled && serverStatus != ServerStatus.STARTING,
|
|
colors = ButtonDefaults.buttonColors(containerColor = CombatGreen)
|
|
) {
|
|
Icon(Icons.Default.PlayArrow, contentDescription = null)
|
|
Spacer(modifier = Modifier.width(4.dp))
|
|
Text(tr(currentLanguage, "Start", "เริ่ม", "启动"))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Spacer(modifier = Modifier.height(10.dp))
|
|
HorizontalDivider(color = Color.DarkGray.copy(alpha = 0.5f))
|
|
Spacer(modifier = Modifier.height(10.dp))
|
|
|
|
Row(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
horizontalArrangement = Arrangement.SpaceBetween
|
|
) {
|
|
Text("Dispatch: 127.0.0.1:21000", fontSize = 12.sp, color = Color.LightGray)
|
|
Text("Forward: 127.0.0.1:23301", fontSize = 12.sp, color = Color.LightGray)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Quick Tools Row (Buttons from server)
|
|
Card(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
colors = CardDefaults.cardColors(containerColor = BgCard),
|
|
shape = RoundedCornerShape(16.dp)
|
|
) {
|
|
Column(modifier = Modifier.padding(14.dp)) {
|
|
Text(
|
|
tr(currentLanguage, "Server Controls & Configuration", "การควบคุม & ตั้งค่าเซิร์ฟเวอร์", "服务器控制与设置"),
|
|
fontWeight = FontWeight.Bold,
|
|
fontSize = 14.sp,
|
|
color = Color.White
|
|
)
|
|
Spacer(modifier = Modifier.height(10.dp))
|
|
|
|
// Tools Grid Row 1: Password & Trailblazer
|
|
Row(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
) {
|
|
OutlinedButton(
|
|
onClick = onOpenSetPassword,
|
|
modifier = Modifier.weight(1f),
|
|
contentPadding = PaddingValues(horizontal = 4.dp, vertical = 6.dp)
|
|
) {
|
|
Icon(Icons.Default.Lock, contentDescription = null, modifier = Modifier.size(16.dp))
|
|
Spacer(modifier = Modifier.width(4.dp))
|
|
Text(
|
|
if (serverKey.isBlank()) tr(currentLanguage, "Password", "รหัสผ่าน", "密码")
|
|
else tr(currentLanguage, "Key Set", "มีรหัสแล้ว", "已设密码"),
|
|
fontSize = 12.sp,
|
|
maxLines = 1
|
|
)
|
|
}
|
|
|
|
OutlinedButton(
|
|
onClick = onOpenEditTrailblazer,
|
|
modifier = Modifier.weight(1f),
|
|
contentPadding = PaddingValues(horizontal = 4.dp, vertical = 6.dp)
|
|
) {
|
|
Icon(Icons.Default.Person, contentDescription = null, modifier = Modifier.size(16.dp))
|
|
Spacer(modifier = Modifier.width(4.dp))
|
|
Text(tr(currentLanguage, "Trailblazer", "ตัวละคร", "主角"), fontSize = 12.sp, maxLines = 1)
|
|
}
|
|
}
|
|
|
|
Spacer(modifier = Modifier.height(6.dp))
|
|
|
|
// Tools Grid Row 2: EX Eidolon & Misc Files
|
|
Row(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
) {
|
|
OutlinedButton(
|
|
onClick = onOpenExRank,
|
|
modifier = Modifier.weight(1f),
|
|
contentPadding = PaddingValues(horizontal = 4.dp, vertical = 6.dp)
|
|
) {
|
|
Icon(Icons.Default.Star, contentDescription = null, tint = ExploreAmber, modifier = Modifier.size(16.dp))
|
|
Spacer(modifier = Modifier.width(4.dp))
|
|
Text(tr(currentLanguage, "EX Eidolon", "EX Eidolon", "EX星魂"), fontSize = 12.sp, maxLines = 1)
|
|
}
|
|
|
|
OutlinedButton(
|
|
onClick = onOpenMiscExplorer,
|
|
modifier = Modifier.weight(1f),
|
|
contentPadding = PaddingValues(horizontal = 4.dp, vertical = 6.dp)
|
|
) {
|
|
Icon(Icons.Default.FolderOpen, contentDescription = null, modifier = Modifier.size(16.dp))
|
|
Spacer(modifier = Modifier.width(4.dp))
|
|
Text(tr(currentLanguage, "Misc Files", "ไฟล์ Misc", "杂项文件"), fontSize = 12.sp, maxLines = 1)
|
|
}
|
|
}
|
|
|
|
Spacer(modifier = Modifier.height(10.dp))
|
|
|
|
// Hotfix toggle row
|
|
Row(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
horizontalArrangement = Arrangement.SpaceBetween,
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
Column(modifier = Modifier.weight(1f)) {
|
|
Text(
|
|
tr(currentLanguage, "Send Hotfix (Asset Patching)", "Send Hotfix (โหมดดาวน์โหลดแพตช์)", "Send Hotfix (补丁模式)"),
|
|
fontSize = 13.sp,
|
|
fontWeight = FontWeight.SemiBold
|
|
)
|
|
Text(
|
|
if (sendHotfix)
|
|
tr(currentLanguage, "Enabled: Online hoyoverse assets", "เปิด: ต่อเน็ตโหลด Asset จาก Hoyoverse", "开启: 在线获取官方资源")
|
|
else
|
|
tr(currentLanguage, "Disabled: 100% Offline (fixes black screen)", "ปิด: ออฟไลน์ 100% (แก้ปัญหาจอดำ)", "关闭: 100% 离线 (修复黑屏)"),
|
|
fontSize = 11.sp,
|
|
color = if (sendHotfix) ExploreAmber else CombatGreen
|
|
)
|
|
}
|
|
Switch(
|
|
checked = sendHotfix,
|
|
onCheckedChange = onToggleHotfix
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Live Console Card
|
|
Card(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.weight(1f),
|
|
colors = CardDefaults.cardColors(containerColor = Color(0xFF0F1117)),
|
|
shape = RoundedCornerShape(16.dp)
|
|
) {
|
|
Column(modifier = Modifier.padding(12.dp)) {
|
|
Row(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
horizontalArrangement = Arrangement.SpaceBetween,
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
Box(
|
|
modifier = Modifier
|
|
.size(8.dp)
|
|
.clip(CircleShape)
|
|
.background(if (serverStatus == ServerStatus.RUNNING) CombatGreen else Color.Gray)
|
|
)
|
|
Spacer(modifier = Modifier.width(8.dp))
|
|
Text(
|
|
tr(currentLanguage, "Console Logs", "บันทึกคอนโซล (Console Logs)", "控制台日志"),
|
|
fontWeight = FontWeight.Bold,
|
|
fontSize = 13.sp,
|
|
color = Color.LightGray
|
|
)
|
|
}
|
|
|
|
Row {
|
|
IconButton(onClick = onCopyLogs) {
|
|
Icon(Icons.Default.ContentCopy, contentDescription = "Copy", tint = Color.Gray, modifier = Modifier.size(18.dp))
|
|
}
|
|
IconButton(onClick = onClearLogs) {
|
|
Icon(Icons.Default.Delete, contentDescription = "Clear", tint = Color.Gray, modifier = Modifier.size(18.dp))
|
|
}
|
|
}
|
|
}
|
|
|
|
HorizontalDivider(color = Color.DarkGray.copy(alpha = 0.5f))
|
|
|
|
LazyColumn(
|
|
state = listState,
|
|
modifier = Modifier
|
|
.fillMaxSize()
|
|
.padding(top = 8.dp)
|
|
) {
|
|
if (serverLogs.isEmpty()) {
|
|
item {
|
|
Text(
|
|
tr(currentLanguage, "Server logs will appear here when started...", "บันทึกการทำงานของเซิร์ฟเวอร์จะแสดงที่นี่เมื่อเริ่มทำงาน...", "服务器启动后日志将显示在此处..."),
|
|
color = Color.DarkGray,
|
|
fontSize = 12.sp,
|
|
fontFamily = FontFamily.Monospace
|
|
)
|
|
}
|
|
} else {
|
|
items(serverLogs) { line ->
|
|
Text(
|
|
text = line,
|
|
color = when {
|
|
line.contains("[Error]") || line.contains("Exception") -> Color(0xFFFF5252)
|
|
line.contains("[Warning]") -> ExploreAmber
|
|
line.contains("[System]") || line.contains("[Patch]") -> Color(0xFF81D4FA)
|
|
line.contains("Successfully") || line.contains("Ready") -> CombatGreen
|
|
else -> Color(0xFFCFD8DC)
|
|
},
|
|
fontSize = 11.sp,
|
|
fontFamily = FontFamily.Monospace
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun TeleportTab(
|
|
context: Context,
|
|
planets: List<PlanetItem>,
|
|
selectedPlanetId: String?,
|
|
currentLanguage: AppLanguage,
|
|
searchQuery: String,
|
|
selectedCategory: FilterCategory,
|
|
currentSceneConfig: SceneConfig?,
|
|
onSelectPlanet: (String) -> Unit,
|
|
onSearchChange: (String) -> Unit,
|
|
onCategoryChange: (FilterCategory) -> Unit,
|
|
onTeleport: (SceneItem, String) -> Unit,
|
|
onOpenCustomCoords: () -> Unit
|
|
) {
|
|
val activePlanet = planets.find { it.id == selectedPlanetId }
|
|
|
|
val filteredScenes = remember(activePlanet, searchQuery, selectedCategory, currentLanguage) {
|
|
val baseScenes = activePlanet?.scenes ?: emptyList()
|
|
baseScenes.filter { scene ->
|
|
val matchesCategory = when (selectedCategory) {
|
|
FilterCategory.ALL -> true
|
|
FilterCategory.COMBAT -> scene.combatReady
|
|
FilterCategory.EXPLORE -> !scene.combatReady && !scene.isBeta
|
|
FilterCategory.BETA -> scene.isBeta
|
|
}
|
|
val matchesSearch = searchQuery.isBlank() ||
|
|
scene.localizedName(currentLanguage).contains(searchQuery, ignoreCase = true) ||
|
|
scene.planeId.toString().contains(searchQuery)
|
|
|
|
matchesCategory && matchesSearch
|
|
}
|
|
}
|
|
|
|
Column(
|
|
modifier = Modifier
|
|
.fillMaxSize()
|
|
.padding(12.dp)
|
|
) {
|
|
// Active Map Banner
|
|
Card(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
colors = CardDefaults.cardColors(containerColor = BgCard),
|
|
shape = RoundedCornerShape(12.dp)
|
|
) {
|
|
Row(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(12.dp),
|
|
horizontalArrangement = Arrangement.SpaceBetween,
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
Column {
|
|
Text(
|
|
tr(currentLanguage, "Current Teleport Destination", "พิกัดปลายทางปัจจุบัน", "当前传送目的地"),
|
|
fontSize = 11.sp,
|
|
color = Color.Gray
|
|
)
|
|
Text(
|
|
text = "Plane: ${currentSceneConfig?.planeId ?: tr(currentLanguage, "Unknown", "ไม่ระบุ", "未知")} (Layer ${currentSceneConfig?.mapLayer ?: 1})",
|
|
fontWeight = FontWeight.Bold,
|
|
fontSize = 15.sp,
|
|
color = PurplePrimary
|
|
)
|
|
Text(
|
|
text = "X: ${currentSceneConfig?.x ?: 0} | Y: ${currentSceneConfig?.y ?: 0} | Z: ${currentSceneConfig?.z ?: 0}",
|
|
fontSize = 11.sp,
|
|
color = Color.LightGray
|
|
)
|
|
}
|
|
|
|
Button(
|
|
onClick = onOpenCustomCoords,
|
|
colors = ButtonDefaults.buttonColors(containerColor = PurplePrimary)
|
|
) {
|
|
Icon(Icons.Default.EditLocation, contentDescription = null, modifier = Modifier.size(16.dp))
|
|
Spacer(modifier = Modifier.width(4.dp))
|
|
Text(tr(currentLanguage, "Custom", "พิกัดเอง", "自定义"), fontSize = 12.sp)
|
|
}
|
|
}
|
|
}
|
|
|
|
Spacer(modifier = Modifier.height(10.dp))
|
|
|
|
// Search Bar
|
|
OutlinedTextField(
|
|
value = searchQuery,
|
|
onValueChange = onSearchChange,
|
|
placeholder = { Text(tr(currentLanguage, "Search map name or plane ID...", "ค้นหาชื่อฉากหรือ Plane ID...", "搜索场景名称或Plane ID...")) },
|
|
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
|
|
singleLine = true,
|
|
modifier = Modifier.fillMaxWidth(),
|
|
shape = RoundedCornerShape(12.dp)
|
|
)
|
|
|
|
Spacer(modifier = Modifier.height(10.dp))
|
|
|
|
// Planet Chips Row
|
|
LazyRow(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
) {
|
|
items(planets) { planet ->
|
|
FilterChip(
|
|
selected = planet.id == selectedPlanetId,
|
|
onClick = { onSelectPlanet(planet.id) },
|
|
label = { Text(planet.localizedName(currentLanguage)) }
|
|
)
|
|
}
|
|
}
|
|
|
|
Spacer(modifier = Modifier.height(6.dp))
|
|
|
|
// Category Filter Row
|
|
Row(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
) {
|
|
FilterCategory.values().forEach { cat ->
|
|
val label = when (cat) {
|
|
FilterCategory.ALL -> tr(currentLanguage, "All", "ทั้งหมด", "全部")
|
|
FilterCategory.COMBAT -> tr(currentLanguage, "Combat", "ต่อสู้", "战斗")
|
|
FilterCategory.EXPLORE -> tr(currentLanguage, "Explore", "สำรวจ", "探索")
|
|
FilterCategory.BETA -> tr(currentLanguage, "Beta", "เบต้า", "测试")
|
|
}
|
|
FilterChip(
|
|
selected = selectedCategory == cat,
|
|
onClick = { onCategoryChange(cat) },
|
|
label = { Text(label) }
|
|
)
|
|
}
|
|
}
|
|
|
|
Spacer(modifier = Modifier.height(10.dp))
|
|
|
|
// Scene List
|
|
LazyColumn(
|
|
modifier = Modifier
|
|
.fillMaxSize()
|
|
.weight(1f),
|
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
|
) {
|
|
items(filteredScenes) { scene ->
|
|
Card(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
colors = CardDefaults.cardColors(containerColor = BgCard),
|
|
shape = RoundedCornerShape(12.dp)
|
|
) {
|
|
Row(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(14.dp),
|
|
horizontalArrangement = Arrangement.SpaceBetween,
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
Column(modifier = Modifier.weight(1f)) {
|
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
Text(
|
|
text = scene.localizedName(currentLanguage),
|
|
fontWeight = FontWeight.Bold,
|
|
fontSize = 15.sp,
|
|
color = Color.White
|
|
)
|
|
Spacer(modifier = Modifier.width(8.dp))
|
|
if (scene.combatReady) {
|
|
Surface(
|
|
shape = RoundedCornerShape(4.dp),
|
|
color = CombatGreen.copy(alpha = 0.2f)
|
|
) {
|
|
Text(
|
|
tr(currentLanguage, "Combat", "ต่อสู้", "战斗"),
|
|
color = CombatGreen,
|
|
fontSize = 10.sp,
|
|
modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp)
|
|
)
|
|
}
|
|
}
|
|
if (scene.isBeta) {
|
|
Spacer(modifier = Modifier.width(4.dp))
|
|
Surface(
|
|
shape = RoundedCornerShape(4.dp),
|
|
color = BetaPurple.copy(alpha = 0.2f)
|
|
) {
|
|
Text("Beta", color = BetaPurple, fontSize = 10.sp, modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp))
|
|
}
|
|
}
|
|
}
|
|
|
|
Spacer(modifier = Modifier.height(4.dp))
|
|
Text(
|
|
text = "Plane: ${scene.planeId} | Layer: ${scene.mapLayer} | Coords: (${scene.x}, ${scene.y}, ${scene.z})",
|
|
fontSize = 11.sp,
|
|
color = Color.Gray
|
|
)
|
|
|
|
if (scene.localizedDesc(currentLanguage).isNotBlank()) {
|
|
Spacer(modifier = Modifier.height(4.dp))
|
|
Text(
|
|
text = scene.localizedDesc(currentLanguage),
|
|
fontSize = 12.sp,
|
|
color = Color.LightGray,
|
|
maxLines = 2,
|
|
overflow = TextOverflow.Ellipsis
|
|
)
|
|
}
|
|
}
|
|
|
|
Button(
|
|
onClick = { onTeleport(scene, activePlanet?.localizedName(currentLanguage) ?: "") },
|
|
colors = ButtonDefaults.buttonColors(containerColor = PurplePrimary)
|
|
) {
|
|
Text(tr(currentLanguage, "Warp", "วาร์ป", "传送"))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun UpdaterTab(
|
|
context: Context,
|
|
currentLanguage: AppLanguage,
|
|
installedVersion: String,
|
|
updateInfo: UpdateInfo?,
|
|
isChecking: Boolean,
|
|
isInstalling: Boolean,
|
|
installProgress: Float,
|
|
installMessage: String,
|
|
appReleaseInfo: AppReleaseInfo?,
|
|
isCheckingApp: Boolean,
|
|
isDownloadingApk: Boolean,
|
|
apkProgress: Int,
|
|
includePrerelease: Boolean,
|
|
onTogglePrerelease: (Boolean) -> Unit,
|
|
onCheckUpdate: () -> Unit,
|
|
onOpenVersionSelector: () -> Unit,
|
|
onInstall: (String) -> Unit,
|
|
onCheckAppUpdate: () -> Unit,
|
|
onDownloadApk: () -> Unit
|
|
) {
|
|
val latestVersion = updateInfo?.latestVersion ?: ""
|
|
val isInstalled = installedVersion.isNotBlank()
|
|
val isUpToDate = isInstalled && latestVersion.isNotBlank() && installedVersion == latestVersion
|
|
val hasNewVersion = isInstalled && latestVersion.isNotBlank() && installedVersion != latestVersion
|
|
|
|
Column(
|
|
modifier = Modifier
|
|
.fillMaxSize()
|
|
.padding(16.dp),
|
|
verticalArrangement = Arrangement.spacedBy(14.dp)
|
|
) {
|
|
// --- 1. App Updater Card (APK from GitHub) ---
|
|
Card(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
colors = CardDefaults.cardColors(containerColor = BgCard),
|
|
shape = RoundedCornerShape(16.dp)
|
|
) {
|
|
Column(modifier = Modifier.padding(16.dp)) {
|
|
Text(
|
|
tr(currentLanguage, "Escape Penacony Standalone", "แอปพลิเคชัน Escape Penacony Standalone", "Escape Penacony Standalone 客户端"),
|
|
fontWeight = FontWeight.Bold,
|
|
fontSize = 16.sp,
|
|
color = Color.White
|
|
)
|
|
Text(
|
|
tr(currentLanguage, "Application APK updater via GitHub Releases", "ตรวจหาและอัปเดตไฟล์ APK จาก GitHub Releases", "通过 GitHub Releases 更新 APK 应用程序"),
|
|
fontSize = 12.sp,
|
|
color = Color.Gray
|
|
)
|
|
|
|
Spacer(modifier = Modifier.height(12.dp))
|
|
|
|
// Update Channel Selector (Stable vs Pre-release / Beta)
|
|
Row(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
Text(
|
|
tr(currentLanguage, "Channel:", "ช่องทาง:", "渠道:"),
|
|
fontSize = 12.sp,
|
|
color = Color.Gray
|
|
)
|
|
FilterChip(
|
|
selected = !includePrerelease,
|
|
onClick = {
|
|
if (includePrerelease) {
|
|
onTogglePrerelease(false)
|
|
}
|
|
},
|
|
label = {
|
|
Text(
|
|
tr(currentLanguage, "Latest (Stable)", "เสถียร (ล่าสุด)", "稳定版 (最新)"),
|
|
fontSize = 11.sp
|
|
)
|
|
},
|
|
colors = FilterChipDefaults.filterChipColors(
|
|
selectedContainerColor = PurplePrimary,
|
|
selectedLabelColor = Color.White
|
|
)
|
|
)
|
|
FilterChip(
|
|
selected = includePrerelease,
|
|
onClick = {
|
|
if (!includePrerelease) {
|
|
onTogglePrerelease(true)
|
|
}
|
|
},
|
|
label = {
|
|
Text(
|
|
tr(currentLanguage, "Pre-release (Beta)", "ทดสอบ (เบต้า)", "预发布 (测试版)"),
|
|
fontSize = 11.sp
|
|
)
|
|
},
|
|
colors = FilterChipDefaults.filterChipColors(
|
|
selectedContainerColor = ExploreAmber,
|
|
selectedLabelColor = Color.Black
|
|
)
|
|
)
|
|
}
|
|
|
|
Spacer(modifier = Modifier.height(10.dp))
|
|
|
|
Row(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
horizontalArrangement = Arrangement.SpaceBetween,
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
Column {
|
|
Text(
|
|
tr(currentLanguage, "Current App Version", "เวอร์ชันแอปปัจจุบัน", "当前应用版本"),
|
|
fontSize = 12.sp,
|
|
color = Color.Gray
|
|
)
|
|
Text(
|
|
text = BuildConfig.VERSION_NAME,
|
|
fontWeight = FontWeight.Bold,
|
|
fontSize = 15.sp,
|
|
color = Color.White
|
|
)
|
|
}
|
|
|
|
Column(horizontalAlignment = Alignment.End) {
|
|
Text(
|
|
tr(currentLanguage, "Latest on GitHub", "เวอร์ชันล่าสุดบน GitHub", "GitHub 最新版本"),
|
|
fontSize = 12.sp,
|
|
color = Color.Gray
|
|
)
|
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
if (appReleaseInfo != null) {
|
|
if (appReleaseInfo.isPrerelease) {
|
|
Surface(
|
|
shape = RoundedCornerShape(4.dp),
|
|
color = ExploreAmber.copy(alpha = 0.2f),
|
|
modifier = Modifier.padding(end = 4.dp)
|
|
) {
|
|
Text(
|
|
text = "BETA",
|
|
color = ExploreAmber,
|
|
fontSize = 9.sp,
|
|
fontWeight = FontWeight.Bold,
|
|
modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp)
|
|
)
|
|
}
|
|
} else {
|
|
Surface(
|
|
shape = RoundedCornerShape(4.dp),
|
|
color = CombatGreen.copy(alpha = 0.2f),
|
|
modifier = Modifier.padding(end = 4.dp)
|
|
) {
|
|
Text(
|
|
text = "STABLE",
|
|
color = CombatGreen,
|
|
fontSize = 9.sp,
|
|
fontWeight = FontWeight.Bold,
|
|
modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
Text(
|
|
text = appReleaseInfo?.tagName ?: tr(currentLanguage, "Tap check to query", "แตะตรวจหาเพื่อดูเวอร์ชัน", "点击检查获取"),
|
|
fontWeight = FontWeight.Bold,
|
|
fontSize = 15.sp,
|
|
color = PurplePrimary
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
Spacer(modifier = Modifier.height(14.dp))
|
|
|
|
if (isDownloadingApk) {
|
|
Column(modifier = Modifier.fillMaxWidth()) {
|
|
LinearProgressIndicator(
|
|
progress = { apkProgress / 100f },
|
|
modifier = Modifier.fillMaxWidth(),
|
|
color = PurplePrimary
|
|
)
|
|
Spacer(modifier = Modifier.height(6.dp))
|
|
Text(
|
|
tr(currentLanguage, "Downloading APK: $apkProgress%", "กำลังดาวน์โหลดไฟล์ APK: $apkProgress%", "正在下载安装包: $apkProgress%"),
|
|
fontSize = 12.sp,
|
|
color = Color.LightGray
|
|
)
|
|
}
|
|
} else {
|
|
Row(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
) {
|
|
Button(
|
|
onClick = onCheckAppUpdate,
|
|
enabled = !isCheckingApp,
|
|
modifier = Modifier.weight(1f)
|
|
) {
|
|
if (isCheckingApp) {
|
|
CircularProgressIndicator(modifier = Modifier.size(16.dp), color = Color.White)
|
|
} else {
|
|
Icon(Icons.Default.Refresh, contentDescription = null, modifier = Modifier.size(16.dp))
|
|
Spacer(modifier = Modifier.width(4.dp))
|
|
Text(tr(currentLanguage, "Check App Update", "ตรวจหาอัปเดตแอป", "检查应用更新"))
|
|
}
|
|
}
|
|
|
|
if (appReleaseInfo != null && appReleaseInfo.isNewer) {
|
|
Button(
|
|
onClick = onDownloadApk,
|
|
colors = ButtonDefaults.buttonColors(containerColor = CombatGreen),
|
|
modifier = Modifier.weight(1f)
|
|
) {
|
|
Icon(Icons.Default.Download, contentDescription = null, modifier = Modifier.size(16.dp))
|
|
Spacer(modifier = Modifier.width(4.dp))
|
|
Text(tr(currentLanguage, "Update App", "อัปเดตแอป", "更新应用"))
|
|
}
|
|
}
|
|
}
|
|
|
|
if (appReleaseInfo != null && !appReleaseInfo.isNewer) {
|
|
Spacer(modifier = Modifier.height(10.dp))
|
|
Surface(
|
|
shape = RoundedCornerShape(8.dp),
|
|
color = CombatGreen.copy(alpha = 0.15f),
|
|
modifier = Modifier.fillMaxWidth()
|
|
) {
|
|
Row(
|
|
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
Icon(Icons.Default.CheckCircle, contentDescription = null, tint = CombatGreen, modifier = Modifier.size(18.dp))
|
|
Spacer(modifier = Modifier.width(8.dp))
|
|
Text(
|
|
text = tr(currentLanguage, "Application is up to date (${BuildConfig.VERSION_NAME})", "แอปพลิเคชันเป็นเวอร์ชันล่าสุดแล้ว (${BuildConfig.VERSION_NAME})", "应用程序已是最新版本 (${BuildConfig.VERSION_NAME})"),
|
|
color = CombatGreen,
|
|
fontSize = 12.sp,
|
|
fontWeight = FontWeight.Medium
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- 2. ppSR Server Core Card ---
|
|
Card(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
colors = CardDefaults.cardColors(containerColor = BgCard),
|
|
shape = RoundedCornerShape(16.dp)
|
|
) {
|
|
Column(modifier = Modifier.padding(16.dp)) {
|
|
Text(
|
|
tr(currentLanguage, "ppSR Direct Updater", "ตัวอัปเดตเซิร์ฟเวอร์ ppSR โดยตรง", "ppSR 直接更新器"),
|
|
fontWeight = FontWeight.Bold,
|
|
fontSize = 16.sp,
|
|
color = Color.White
|
|
)
|
|
Text(
|
|
tr(currentLanguage, "Direct synchronization from official ppSR endpoint", "ซิงค์ข้อมูลจากเซิร์ฟเวอร์ ppSR (File Garden) โดยตรง", "直接从 ppSR 官方端点同步数据"),
|
|
fontSize = 12.sp,
|
|
color = Color.Gray
|
|
)
|
|
|
|
Spacer(modifier = Modifier.height(16.dp))
|
|
|
|
Row(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
horizontalArrangement = Arrangement.SpaceBetween
|
|
) {
|
|
Column {
|
|
Text(
|
|
tr(currentLanguage, "Installed Version", "เวอร์ชันที่ติดตั้งในเครื่อง", "已安装版本"),
|
|
fontSize = 12.sp,
|
|
color = Color.Gray
|
|
)
|
|
Text(
|
|
text = if (installedVersion.isBlank()) tr(currentLanguage, "None (Not Installed)", "ยังไม่ได้ติดตั้ง", "未安装") else installedVersion,
|
|
fontWeight = FontWeight.Bold,
|
|
fontSize = 16.sp,
|
|
color = if (installedVersion.isBlank()) Color.Red else Color.White
|
|
)
|
|
}
|
|
|
|
Column(horizontalAlignment = Alignment.End) {
|
|
Text(
|
|
tr(currentLanguage, "Remote Latest", "เวอร์ชันล่าสุดบนเซิร์ฟเวอร์", "最新远程版本"),
|
|
fontSize = 12.sp,
|
|
color = Color.Gray
|
|
)
|
|
Text(
|
|
text = if (latestVersion.isBlank()) tr(currentLanguage, "Tap check to fetch", "แตะปุ่มเพื่อตรวจหา", "点击检查获取") else latestVersion,
|
|
fontWeight = FontWeight.Bold,
|
|
fontSize = 16.sp,
|
|
color = PurplePrimary
|
|
)
|
|
}
|
|
}
|
|
|
|
Spacer(modifier = Modifier.height(16.dp))
|
|
|
|
if (isInstalling) {
|
|
Column(modifier = Modifier.fillMaxWidth()) {
|
|
LinearProgressIndicator(
|
|
progress = { installProgress },
|
|
modifier = Modifier.fillMaxWidth(),
|
|
color = PurplePrimary
|
|
)
|
|
Spacer(modifier = Modifier.height(6.dp))
|
|
Text(installMessage, fontSize = 12.sp, color = Color.LightGray)
|
|
}
|
|
} else {
|
|
Row(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
) {
|
|
// Check Updates Button
|
|
Button(
|
|
onClick = onCheckUpdate,
|
|
enabled = !isChecking,
|
|
modifier = Modifier.weight(1f)
|
|
) {
|
|
if (isChecking) {
|
|
CircularProgressIndicator(modifier = Modifier.size(16.dp), color = Color.White)
|
|
} else {
|
|
Icon(Icons.Default.Refresh, contentDescription = null, modifier = Modifier.size(16.dp))
|
|
Spacer(modifier = Modifier.width(4.dp))
|
|
Text(tr(currentLanguage, "Check Updates", "ตรวจหาอัปเดต", "检查更新"))
|
|
}
|
|
}
|
|
|
|
// Action Button based on accurate version comparison
|
|
if (latestVersion.isNotBlank()) {
|
|
if (!isInstalled) {
|
|
// Not installed -> Show Install Button
|
|
Button(
|
|
onClick = { onInstall(latestVersion) },
|
|
colors = ButtonDefaults.buttonColors(containerColor = CombatGreen),
|
|
modifier = Modifier.weight(1f)
|
|
) {
|
|
Icon(Icons.Default.Download, contentDescription = null, modifier = Modifier.size(16.dp))
|
|
Spacer(modifier = Modifier.width(4.dp))
|
|
Text(tr(currentLanguage, "Install", "ติดตั้ง", "安装"))
|
|
}
|
|
} else if (hasNewVersion) {
|
|
// Has New Version -> Show Update Button
|
|
Button(
|
|
onClick = { onInstall(latestVersion) },
|
|
colors = ButtonDefaults.buttonColors(containerColor = PurplePrimary),
|
|
modifier = Modifier.weight(1f)
|
|
) {
|
|
Icon(Icons.Default.Upgrade, contentDescription = null, modifier = Modifier.size(16.dp))
|
|
Spacer(modifier = Modifier.width(4.dp))
|
|
Text(tr(currentLanguage, "Update", "อัปเดต", "更新"))
|
|
}
|
|
} else if (isUpToDate) {
|
|
// Up to date -> Show Reinstall Button
|
|
OutlinedButton(
|
|
onClick = { onInstall(latestVersion) },
|
|
modifier = Modifier.weight(1f)
|
|
) {
|
|
Icon(Icons.Default.Refresh, contentDescription = null, modifier = Modifier.size(16.dp))
|
|
Spacer(modifier = Modifier.width(4.dp))
|
|
Text(tr(currentLanguage, "Reinstall", "ติดตั้งใหม่", "重新安装"))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Up-to-date Confirmation Badge
|
|
if (isUpToDate) {
|
|
Spacer(modifier = Modifier.height(10.dp))
|
|
Surface(
|
|
shape = RoundedCornerShape(8.dp),
|
|
color = CombatGreen.copy(alpha = 0.15f),
|
|
modifier = Modifier.fillMaxWidth()
|
|
) {
|
|
Row(
|
|
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
Icon(Icons.Default.CheckCircle, contentDescription = null, tint = CombatGreen, modifier = Modifier.size(18.dp))
|
|
Spacer(modifier = Modifier.width(8.dp))
|
|
Text(
|
|
text = tr(currentLanguage, "You are using the latest version ($installedVersion)", "คุณกำลังใช้เวอร์ชันล่าสุดแล้ว ($installedVersion)", "您已是最新版本 ($installedVersion)"),
|
|
color = CombatGreen,
|
|
fontSize = 12.sp,
|
|
fontWeight = FontWeight.Medium
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
Spacer(modifier = Modifier.height(8.dp))
|
|
|
|
OutlinedButton(
|
|
onClick = onOpenVersionSelector,
|
|
modifier = Modifier.fillMaxWidth()
|
|
) {
|
|
Icon(Icons.AutoMirrored.Filled.List, contentDescription = null, modifier = Modifier.size(16.dp))
|
|
Spacer(modifier = Modifier.width(6.dp))
|
|
Text(tr(currentLanguage, "Select Version to Install (Archive)", "เลือกเวอร์ชันที่ต้องการติดตั้ง (คลังเวอร์ชัน)", "选择安装版本 (归档)"))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Diagnostics Card
|
|
Card(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
colors = CardDefaults.cardColors(containerColor = BgCard),
|
|
shape = RoundedCornerShape(16.dp)
|
|
) {
|
|
Column(modifier = Modifier.padding(16.dp)) {
|
|
Text(
|
|
tr(currentLanguage, "System Diagnostics", "ข้อมูลวินิจฉัยระบบ", "系统诊断"),
|
|
fontWeight = FontWeight.Bold,
|
|
fontSize = 14.sp,
|
|
color = Color.White
|
|
)
|
|
Spacer(modifier = Modifier.height(10.dp))
|
|
|
|
DiagnosticItem(
|
|
title = tr(currentLanguage, "Python Runtime", "สภาพแวดล้อม Python", "Python 运行环境"),
|
|
value = "Embedded Chaquopy Python 3.14 (arm64-v8a)"
|
|
)
|
|
DiagnosticItem(
|
|
title = tr(currentLanguage, "Root Requirement", "ความต้องการสิทธิ์ Root", "Root 权限需求"),
|
|
value = tr(currentLanguage, "None (Fully Standalone)", "ไม่ต้องใช้ (รันในตัวแอป 100%)", "无需 (完全独立运行)")
|
|
)
|
|
DiagnosticItem(
|
|
title = tr(currentLanguage, "Storage Sandbox", "พื้นที่จัดเก็บข้อมูลแซนด์บ็อกซ์", "存储沙盒"),
|
|
value = "${context.filesDir.absolutePath}/server"
|
|
)
|
|
DiagnosticItem(
|
|
title = tr(currentLanguage, "Update Endpoint", "ลิงก์ตรวจสอบอัปเดต", "更新端点"),
|
|
value = UpdateRepository.VERSIONS_URL
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun DiagnosticItem(title: String, value: String) {
|
|
Column(modifier = Modifier.padding(vertical = 4.dp)) {
|
|
Text(title, fontSize = 11.sp, color = Color.Gray)
|
|
Text(value, fontSize = 13.sp, color = Color.LightGray)
|
|
}
|
|
}
|