Fix Android refresh flicker and pin training host

This commit is contained in:
Codex
2026-06-27 08:38:45 +03:00
parent f1f1cbb1e0
commit 98d6277d65
3 changed files with 58 additions and 7 deletions
+2 -2
View File
@@ -10,7 +10,7 @@ android {
applicationId = "xyz.kusoft.tradebotmonitor" applicationId = "xyz.kusoft.tradebotmonitor"
minSdk = 26 minSdk = 26
targetSdk = 36 targetSdk = 36
versionCode = 4 versionCode = 5
versionName = "0.2.1" versionName = "0.2.2"
} }
} }
@@ -10,6 +10,9 @@ class AppPrefs(context: Context) {
if (saved.isNullOrBlank() || saved == LEGACY_PI_API_BASE_URL) { if (saved.isNullOrBlank() || saved == LEGACY_PI_API_BASE_URL) {
prefs.edit().putString("api_base_url", DEFAULT_API_BASE_URL).apply() prefs.edit().putString("api_base_url", DEFAULT_API_BASE_URL).apply()
} }
if (prefs.getString("training_computer_name", null).isNullOrBlank()) {
pinDefaultTrainingComputer()
}
} }
var apiBaseUrl: String var apiBaseUrl: String
@@ -40,6 +43,23 @@ class AppPrefs(context: Context) {
get() = prefs.getInt("retrain_interval_hours", 6) get() = prefs.getInt("retrain_interval_hours", 6)
set(value) = prefs.edit().putInt("retrain_interval_hours", value.coerceAtLeast(1)).apply() set(value) = prefs.edit().putInt("retrain_interval_hours", value.coerceAtLeast(1)).apply()
val trainingComputerName: String
get() = prefs.getString("training_computer_name", DEFAULT_TRAINING_COMPUTER_NAME) ?: DEFAULT_TRAINING_COMPUTER_NAME
val trainingComputerPath: String
get() = prefs.getString("training_computer_path", DEFAULT_TRAINING_COMPUTER_PATH) ?: DEFAULT_TRAINING_COMPUTER_PATH
val trainingComputerPinned: Boolean
get() = prefs.getBoolean("training_computer_pinned", true)
fun pinDefaultTrainingComputer() {
prefs.edit()
.putString("training_computer_name", DEFAULT_TRAINING_COMPUTER_NAME)
.putString("training_computer_path", DEFAULT_TRAINING_COMPUTER_PATH)
.putBoolean("training_computer_pinned", true)
.apply()
}
private fun normalizeBaseUrl(value: String): String { private fun normalizeBaseUrl(value: String): String {
val trimmed = value.trim().trimEnd('/') val trimmed = value.trim().trimEnd('/')
if (trimmed.isBlank()) return DEFAULT_API_BASE_URL if (trimmed.isBlank()) return DEFAULT_API_BASE_URL
@@ -53,5 +73,7 @@ class AppPrefs(context: Context) {
private companion object { private companion object {
const val DEFAULT_API_BASE_URL = "https://tb.kusoft.xyz" const val DEFAULT_API_BASE_URL = "https://tb.kusoft.xyz"
const val LEGACY_PI_API_BASE_URL = "http://192.168.0.185:8787" const val LEGACY_PI_API_BASE_URL = "http://192.168.0.185:8787"
const val DEFAULT_TRAINING_COMPUTER_NAME = "DESKTOP-TMFDL0H"
const val DEFAULT_TRAINING_COMPUTER_PATH = "C:\\Repos\\TradeBot"
} }
} }
@@ -101,18 +101,22 @@ class MainActivity : Activity() {
try { try {
val data = TradeBotApi(prefs.apiBaseUrl, prefs.commandToken).fetchSnapshot() val data = TradeBotApi(prefs.apiBaseUrl, prefs.commandToken).fetchSnapshot()
mainHandler.post { mainHandler.post {
val hadSnapshot = snapshot != null
val hadError = lastError.isNotBlank()
snapshot = data snapshot = data
lastError = "" lastError = ""
ensureSelectedSymbol() ensureSelectedSymbol()
if (shouldRenderAfterRefresh()) { if (shouldRenderAfterRefresh(silent, hadSnapshot, hadError)) {
render() render()
} }
} }
} catch (error: Exception) { } catch (error: Exception) {
mainHandler.post { mainHandler.post {
val hadSnapshot = snapshot != null
val hadError = lastError.isNotBlank()
lastError = error.message ?: "ошибка подключения" lastError = error.message ?: "ошибка подключения"
if (!silent) toast(lastError) if (!silent) toast(lastError)
if (shouldRenderAfterRefresh()) { if (shouldRenderAfterRefresh(silent, hadSnapshot, hadError)) {
render() render()
} }
} }
@@ -120,9 +124,15 @@ class MainActivity : Activity() {
} }
} }
private fun shouldRenderAfterRefresh(): Boolean { private fun shouldRenderAfterRefresh(silent: Boolean, hadSnapshot: Boolean, hadError: Boolean): Boolean {
val focused = contentHost.findFocus() val focused = contentHost.findFocus()
return activeTab != "settings" || focused !is EditText if (activeTab == "settings" && focused is EditText) {
return false
}
if (!silent) {
return true
}
return !hadSnapshot || hadError
} }
private fun renderBottomNav() { private fun renderBottomNav() {
@@ -555,12 +565,31 @@ class MainActivity : Activity() {
return section("Открытые позиции", box) return section("Открытые позиции", box)
} }
private fun trainingComputerPanel(): View =
LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
addView(text("Компьютер обучения", 12f, Typeface.NORMAL, palette.muted))
addView(text(prefs.trainingComputerName, 18f, Typeface.BOLD, palette.green).top(dp(5)))
addView(text(prefs.trainingComputerPath, 12f, Typeface.NORMAL, palette.muted).top(dp(4)))
addView(keyValueLine("Статус", if (prefs.trainingComputerPinned) "закреплен" else "не закреплен", if (prefs.trainingComputerPinned) palette.green else palette.amber).top(dp(8)))
addView(text("Переобучение Torch закреплено за этой Windows-машиной. Телефон только управляет запуском и расписанием, Pi только исполняет бота.", 12f, Typeface.NORMAL, palette.muted).top(dp(8)))
addView(actionRow(
"Закрепить эту машину" to {
prefs.pinDefaultTrainingComputer()
toast("Компьютер обучения закреплен")
render()
},
).top(dp(10)))
}
private fun retrainSettingsBlock(retrain: JSONObject, backtest: JSONObject): View { private fun retrainSettingsBlock(retrain: JSONObject, backtest: JSONObject): View {
val webhookInput = input("URL запуска retrain на Windows", prefs.retrainWebhookUrl) val webhookInput = input("URL запуска retrain на Windows", prefs.retrainWebhookUrl)
return LinearLayout(this).apply { return LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL orientation = LinearLayout.VERTICAL
addView(keyValueLine("Доступно", yesNo(retrain.optBoolean("available", false)))) addView(keyValueLine("Доступно", yesNo(retrain.optBoolean("available", false))))
addView(keyValueLine("Принята новая модель", yesNo(retrain.optBoolean("accepted", false))).top(dp(4))) addView(keyValueLine("Принята новая модель", yesNo(retrain.optBoolean("accepted", false))).top(dp(4)))
addView(trainingComputerPanel().top(dp(12)))
addView(thinDivider().top(dp(12)))
val replay = backtest.optJSONObject("full_replay") ?: JSONObject() val replay = backtest.optJSONObject("full_replay") ?: JSONObject()
addView(keyValueLine("Replay сделок", replay.optInt("trades", 0).toString()).top(dp(4))) addView(keyValueLine("Replay сделок", replay.optInt("trades", 0).toString()).top(dp(4)))
addView(keyValueLine("Replay PnL", signedMoney(replay.optDouble("net_pnl", 0.0)), colorForSigned(replay.optDouble("net_pnl", 0.0))).top(dp(4))) addView(keyValueLine("Replay PnL", signedMoney(replay.optDouble("net_pnl", 0.0)), colorForSigned(replay.optDouble("net_pnl", 0.0))).top(dp(4)))
@@ -942,7 +971,7 @@ class MainActivity : Activity() {
if (stop) api.stopBot() else api.startBot() if (stop) api.stopBot() else api.startBot()
mainHandler.post { mainHandler.post {
toast(if (stop) "Команда остановки отправлена" else "Команда запуска отправлена") toast(if (stop) "Команда остановки отправлена" else "Команда запуска отправлена")
refreshData(silent = true) refreshData(silent = false)
} }
} catch (error: Exception) { } catch (error: Exception) {
mainHandler.post { toast(error.message ?: "Команда не выполнена") } mainHandler.post { toast(error.message ?: "Команда не выполнена") }