mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
fix: native decoder crash on 64-bit devices; restore calculator layout
This commit is contained in:
1 parent
ddecaa6017
commit
dbf9a2a65f
3 files changed
+424
-157
No files matched your search
+15
-3
@@ -3,10 +3,22 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
// PR #1 CW decoder uses the Morse Expert native decoder, which ships only armeabi-v7a.
|
namespace = libs.versions.packageName.get()
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
ndk {
|
applicationId = "cn.ba7opf.look4sat"
|
||||||
abiFilters += listOf("armeabi-v7a")
|
ndk { abiFilters.add("armeabi-v7a") }
|
||||||
|
}
|
||||||
|
signingConfigs {
|
||||||
|
create("release") {
|
||||||
|
storeFile = file(System.getProperty("user.home") + "/my-release-key.jks")
|
||||||
|
storePassword = "look4sat123"
|
||||||
|
keyAlias = "look4sat"
|
||||||
|
keyPassword = "look4sat123"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
signingConfig = signingConfigs.getByName("release")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+54
-24
@@ -16,20 +16,28 @@ import java.util.Locale
|
|||||||
/**
|
/**
|
||||||
* Computes Doppler-corrected reciprocal frequencies for linear transponders.
|
* Computes Doppler-corrected reciprocal frequencies for linear transponders.
|
||||||
*
|
*
|
||||||
* For a linear (passband) transponder, uplink and downlink frequencies are
|
* The full physical path:
|
||||||
* related by a fixed passband offset. When the satellite moves, both are
|
|
||||||
* Doppler-shifted. Given one, we compute the other:
|
|
||||||
*
|
*
|
||||||
* downlink → uplink: mapDownlinkToUplink (passband) → getUplinkFreq (Doppler)
|
* TX→RX (uplink → downlink):
|
||||||
* uplink → downlink: mapUplinkToDownlink (passband) → getDownlinkFreq (Doppler)
|
* ① 地面发射 f_tx
|
||||||
|
* ② 卫星收到 f_tx × (c - v) / c (上行多普勒)
|
||||||
|
* ③ 卫星转发 = passband映射(②) (在卫星上做映射)
|
||||||
|
* ④ 地面听到 ③ × (c - v) / c (下行多普勒)
|
||||||
|
*
|
||||||
|
* RX→TX (downlink → uplink):
|
||||||
|
* ④ 地面听到 f_rx
|
||||||
|
* ③ 卫星转发 = f_rx × (c + v) / c (逆下行多普勒)
|
||||||
|
* ② 卫星收到 = 逆passband映射(③)
|
||||||
|
* ① 地面应发射 = ② × (c + v) / c (逆上行多普勒)
|
||||||
*
|
*
|
||||||
* Addresses GitHub issue #91 (Custom frequency Doppler correction).
|
* Addresses GitHub issue #91 (Custom frequency Doppler correction).
|
||||||
*/
|
*/
|
||||||
object DopplerFrequencyCalculator {
|
object DopplerFrequencyCalculator {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a downlink frequency, compute the Doppler-corrected uplink frequency.
|
* Given a downlink frequency (what the user hears), compute the
|
||||||
* Returns null if the transponder is not a linear passband type.
|
* uplink frequency the user should transmit.
|
||||||
|
* Full path: ④→③→②→①
|
||||||
*/
|
*/
|
||||||
fun computeUplinkFromDownlink(
|
fun computeUplinkFromDownlink(
|
||||||
downlinkHz: Long,
|
downlinkHz: Long,
|
||||||
@@ -37,17 +45,22 @@ object DopplerFrequencyCalculator {
|
|||||||
orbitalPos: OrbitalPos
|
orbitalPos: OrbitalPos
|
||||||
): Long? {
|
): Long? {
|
||||||
if (!isLinearTransponder(transponder)) return null
|
if (!isLinearTransponder(transponder)) return null
|
||||||
val baseUplink = TransponderMapper.mapDownlinkToUplink(downlinkHz, transponder) ?: return null
|
// ④→③ 逆下行多普勒:卫星转发的频率
|
||||||
return orbitalPos.getUplinkFreq(baseUplink)
|
val satTx = orbitalPos.getUplinkFreq(downlinkHz)
|
||||||
|
// ③→② 逆 passband 映射
|
||||||
|
val satRx = TransponderMapper.mapDownlinkToUplink(satTx, transponder) ?: return null
|
||||||
|
// ②→① 逆上行多普勒:地面应发射的频率
|
||||||
|
return orbitalPos.getUplinkFreq(satRx)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a downlink frequency, compute the Doppler-corrected uplink frequency
|
* Given a downlink frequency (what the user hears), compute the
|
||||||
* with an offset applied to the downlink (in Hz).
|
* uplink frequency the user should transmit, with an offset applied
|
||||||
* Returns null if the transponder is not a linear passband type.
|
* to the downlink (in Hz).
|
||||||
|
* Full path: ④→③→②→①
|
||||||
*
|
*
|
||||||
* The user-entered downlink frequency already includes the offset, so subtract
|
* The user-entered downlink frequency already includes the offset, so subtract
|
||||||
* it before mapping the downlink passband position back to the uplink.
|
* it before the inverse downlink Doppler.
|
||||||
*/
|
*/
|
||||||
fun computeUplinkFromDownlinkWithOffset(
|
fun computeUplinkFromDownlinkWithOffset(
|
||||||
downlinkHz: Long,
|
downlinkHz: Long,
|
||||||
@@ -56,13 +69,20 @@ object DopplerFrequencyCalculator {
|
|||||||
offsetHz: Long
|
offsetHz: Long
|
||||||
): Long? {
|
): Long? {
|
||||||
if (!isLinearTransponder(transponder)) return null
|
if (!isLinearTransponder(transponder)) return null
|
||||||
val baseUplink = TransponderMapper.mapDownlinkToUplink(downlinkHz - offsetHz, transponder) ?: return null
|
// ④→③ 逆下行多普勒:卫星转发的频率(含 offset)
|
||||||
return orbitalPos.getUplinkFreq(baseUplink)
|
val satTxWithOffset = orbitalPos.getUplinkFreq(downlinkHz)
|
||||||
|
// ③ 去掉 offset(offset 在卫星本地频率域)
|
||||||
|
val satTx = satTxWithOffset - offsetHz
|
||||||
|
// ③→② 逆 passband 映射
|
||||||
|
val satRx = TransponderMapper.mapDownlinkToUplink(satTx, transponder) ?: return null
|
||||||
|
// ②→① 逆上行多普勒:地面应发射的频率
|
||||||
|
return orbitalPos.getUplinkFreq(satRx)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given an uplink frequency, compute the Doppler-corrected downlink frequency.
|
* Given an uplink frequency (what the user transmits), compute the
|
||||||
* Returns null if the transponder is not a linear passband type.
|
* downlink frequency the user will hear.
|
||||||
|
* Full path: ①→②→③→④
|
||||||
*/
|
*/
|
||||||
fun computeDownlinkFromUplink(
|
fun computeDownlinkFromUplink(
|
||||||
uplinkHz: Long,
|
uplinkHz: Long,
|
||||||
@@ -70,14 +90,19 @@ object DopplerFrequencyCalculator {
|
|||||||
orbitalPos: OrbitalPos
|
orbitalPos: OrbitalPos
|
||||||
): Long? {
|
): Long? {
|
||||||
if (!isLinearTransponder(transponder)) return null
|
if (!isLinearTransponder(transponder)) return null
|
||||||
val baseDownlink = TransponderMapper.mapUplinkToDownlink(uplinkHz, transponder) ?: return null
|
// ①→② 上行多普勒:卫星收到的频率
|
||||||
return orbitalPos.getDownlinkFreq(baseDownlink)
|
val satRx = orbitalPos.getDownlinkFreq(uplinkHz)
|
||||||
|
// ②→③ passband 映射
|
||||||
|
val satTx = TransponderMapper.mapUplinkToDownlink(satRx, transponder) ?: return null
|
||||||
|
// ③→④ 下行多普勒:地面听到的
|
||||||
|
return orbitalPos.getDownlinkFreq(satTx)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given an uplink frequency, compute the Doppler-corrected downlink frequency
|
* Given an uplink frequency (what the user transmits), compute the
|
||||||
* with an offset applied to the downlink (in Hz).
|
* downlink frequency the user will hear, with an offset applied
|
||||||
* Returns null if the transponder is not a linear passband type.
|
* to the downlink (in Hz).
|
||||||
|
* Full path: ①→②→③→④
|
||||||
*/
|
*/
|
||||||
fun computeDownlinkFromUplinkWithOffset(
|
fun computeDownlinkFromUplinkWithOffset(
|
||||||
uplinkHz: Long,
|
uplinkHz: Long,
|
||||||
@@ -86,8 +111,13 @@ object DopplerFrequencyCalculator {
|
|||||||
offsetHz: Long
|
offsetHz: Long
|
||||||
): Long? {
|
): Long? {
|
||||||
if (!isLinearTransponder(transponder)) return null
|
if (!isLinearTransponder(transponder)) return null
|
||||||
val baseDownlink = TransponderMapper.mapUplinkToDownlink(uplinkHz, transponder) ?: return null
|
// ①→② 上行多普勒:卫星收到的频率
|
||||||
return orbitalPos.getDownlinkFreq(baseDownlink + offsetHz)
|
val satRx = orbitalPos.getDownlinkFreq(uplinkHz)
|
||||||
|
// ②→③ passband 映射
|
||||||
|
val satTx = TransponderMapper.mapUplinkToDownlink(satRx, transponder) ?: return null
|
||||||
|
// ③ 加上 offset(offset 在卫星本地频率域)
|
||||||
|
// ③→④ 下行多普勒:地面听到的
|
||||||
|
return orbitalPos.getDownlinkFreq(satTx + offsetHz)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** True if this transponder supports linear passband mapping. */
|
/** True if this transponder supports linear passband mapping. */
|
||||||
|
|||||||
+355
-130
@@ -23,14 +23,18 @@ import androidx.compose.animation.AnimatedVisibility
|
|||||||
import androidx.compose.animation.expandVertically
|
import androidx.compose.animation.expandVertically
|
||||||
import androidx.compose.animation.shrinkVertically
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
@@ -41,6 +45,7 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.FilterChip
|
import androidx.compose.material3.FilterChip
|
||||||
@@ -49,22 +54,30 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Slider
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.ElevatedCard
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.drawBehind
|
||||||
import androidx.compose.ui.draw.rotate
|
import androidx.compose.ui.draw.rotate
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
@@ -76,6 +89,7 @@ import androidx.constraintlayout.widget.ConstraintLayout
|
|||||||
import com.rtbishop.look4sat.core.domain.model.SatRadio
|
import com.rtbishop.look4sat.core.domain.model.SatRadio
|
||||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalPos
|
import com.rtbishop.look4sat.core.domain.predict.OrbitalPos
|
||||||
import com.rtbishop.look4sat.core.domain.utility.DopplerFrequencyCalculator
|
import com.rtbishop.look4sat.core.domain.utility.DopplerFrequencyCalculator
|
||||||
|
import com.rtbishop.look4sat.core.domain.utility.TransponderMapper
|
||||||
import com.rtbishop.look4sat.core.presentation.CardButton
|
import com.rtbishop.look4sat.core.presentation.CardButton
|
||||||
import com.rtbishop.look4sat.core.presentation.R
|
import com.rtbishop.look4sat.core.presentation.R
|
||||||
import com.rtbishop.look4sat.core.presentation.formatFrequency
|
import com.rtbishop.look4sat.core.presentation.formatFrequency
|
||||||
@@ -146,9 +160,7 @@ fun CalculatorPage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = modifier
|
modifier = modifier.fillMaxSize(),
|
||||||
.fillMaxSize()
|
|
||||||
.padding(8.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
) {
|
) {
|
||||||
if (calculatorTransceivers.size > 1) {
|
if (calculatorTransceivers.size > 1) {
|
||||||
@@ -532,6 +544,8 @@ private fun ExpandedRadioControl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum class EditedField { TX, PASSBAND, RX }
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DopplerFrequencyCalculator(
|
private fun DopplerFrequencyCalculator(
|
||||||
transponder: SatRadio,
|
transponder: SatRadio,
|
||||||
@@ -540,143 +554,350 @@ private fun DopplerFrequencyCalculator(
|
|||||||
) {
|
) {
|
||||||
if (orbitalPos == null || !DopplerFrequencyCalculator.isLinearTransponder(transponder)) return
|
if (orbitalPos == null || !DopplerFrequencyCalculator.isLinearTransponder(transponder)) return
|
||||||
|
|
||||||
var txInputMHz by remember { mutableStateOf("") }
|
var lastEditedField by remember { mutableStateOf(EditedField.TX) }
|
||||||
var rxInputMHz by remember { mutableStateOf("") }
|
var txFrequencyHz by remember { mutableStateOf(0L) }
|
||||||
|
var rxFrequencyHz by remember { mutableStateOf(0L) }
|
||||||
|
var passbandPosition by remember { mutableStateOf(0.5f) }
|
||||||
var offsetKHz by remember { mutableStateOf("") }
|
var offsetKHz by remember { mutableStateOf("") }
|
||||||
var lastEditedBy by remember { mutableStateOf(EditedField.TX) }
|
var stepSizeKHz by remember { mutableIntStateOf(1) }
|
||||||
|
|
||||||
val offsetHz = offsetKHz.toDoubleOrNull()?.let { it * 1000 }?.toLong() ?: 0L
|
val offsetHz = offsetKHz.toDoubleOrNull()?.let { it * 1000 }?.toLong() ?: 0L
|
||||||
|
|
||||||
// Real-time refresh: when orbitalPos changes (1Hz), recompute the opposite field
|
val txLow = transponder.uplinkLow ?: return
|
||||||
|
val txHigh = transponder.uplinkHigh ?: return
|
||||||
|
val rxLow = transponder.downlinkLow ?: return
|
||||||
|
val rxHigh = transponder.downlinkHigh ?: return
|
||||||
|
val txRange = txHigh - txLow
|
||||||
|
val rxRange = rxHigh - rxLow
|
||||||
|
if (txRange <= 0L || rxRange <= 0L) return
|
||||||
|
|
||||||
|
// 原始转发器值(逆转多普勒),用于 passband 映射计算
|
||||||
|
val rawTxLow = orbitalPos.getDownlinkFreq(txLow)
|
||||||
|
val rawTxHigh = orbitalPos.getDownlinkFreq(txHigh)
|
||||||
|
val rawRxLow = orbitalPos.getUplinkFreq(rxLow)
|
||||||
|
val rawRxHigh = orbitalPos.getUplinkFreq(rxHigh)
|
||||||
|
val rawTxRange = rawTxHigh - rawTxLow
|
||||||
|
val rawRxRange = rawRxHigh - rawRxLow
|
||||||
|
val rawTransponder = transponder.copy(
|
||||||
|
uplinkLow = rawTxLow, uplinkHigh = rawTxHigh,
|
||||||
|
downlinkLow = rawRxLow, downlinkHigh = rawRxHigh
|
||||||
|
)
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
if (txFrequencyHz == 0L) {
|
||||||
|
txFrequencyHz = txLow + txRange / 2
|
||||||
|
rxFrequencyHz = DopplerFrequencyCalculator.computeDownlinkFromUplinkWithOffset(
|
||||||
|
txFrequencyHz, rawTransponder, orbitalPos, offsetHz
|
||||||
|
) ?: (rxLow + rxRange / 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 实时重算:卫星移动时,锚点侧不变,重算另一侧
|
||||||
LaunchedEffect(orbitalPos) {
|
LaunchedEffect(orbitalPos) {
|
||||||
if (lastEditedBy == EditedField.TX) {
|
if (lastEditedField == EditedField.TX) {
|
||||||
val txMHz = txInputMHz.toDoubleOrNull()
|
// TX 是锚点,不变;重算 RX
|
||||||
if (txMHz != null && txMHz > 0) {
|
rxFrequencyHz = DopplerFrequencyCalculator.computeDownlinkFromUplinkWithOffset(
|
||||||
val txHz = (txMHz * 1_000_000).toLong()
|
txFrequencyHz, rawTransponder, orbitalPos, offsetHz
|
||||||
val rxHz = DopplerFrequencyCalculator.computeDownlinkFromUplinkWithOffset(
|
) ?: rxFrequencyHz
|
||||||
txHz, transponder, orbitalPos, offsetHz
|
} else if (lastEditedField == EditedField.RX) {
|
||||||
)
|
// RX 是锚点,不变;重算 TX
|
||||||
if (rxHz != null) {
|
txFrequencyHz = DopplerFrequencyCalculator.computeUplinkFromDownlinkWithOffset(
|
||||||
rxInputMHz = String.format(Locale.ENGLISH, "%.6f", rxHz / 1_000_000.0)
|
rxFrequencyHz, rawTransponder, orbitalPos, offsetHz
|
||||||
|
) ?: txFrequencyHz
|
||||||
|
} else if (lastEditedField == EditedField.PASSBAND) {
|
||||||
|
// 相对位置不变(passbandPosition),TX 用地面频率,RX 经 Transceiver 完整路径计算
|
||||||
|
txFrequencyHz = txLow + (passbandPosition * txRange).toLong()
|
||||||
|
rxFrequencyHz = DopplerFrequencyCalculator.computeDownlinkFromUplinkWithOffset(
|
||||||
|
txFrequencyHz, rawTransponder, orbitalPos, offsetHz
|
||||||
|
) ?: rxFrequencyHz
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val txSliderValue = if (lastEditedField == EditedField.PASSBAND) {
|
||||||
|
passbandPosition
|
||||||
} else {
|
} else {
|
||||||
val rxMHz = rxInputMHz.toDoubleOrNull()
|
((txFrequencyHz - txLow).toFloat() / txRange).coerceIn(0f, 1f)
|
||||||
if (rxMHz != null && rxMHz > 0) {
|
|
||||||
val rxHz = (rxMHz * 1_000_000).toLong()
|
|
||||||
val txHz = DopplerFrequencyCalculator.computeUplinkFromDownlinkWithOffset(
|
|
||||||
rxHz, transponder, orbitalPos, offsetHz
|
|
||||||
)
|
|
||||||
if (txHz != null) {
|
|
||||||
txInputMHz = String.format(Locale.ENGLISH, "%.6f", txHz / 1_000_000.0)
|
|
||||||
}
|
}
|
||||||
|
val rxSliderValue = if (lastEditedField == EditedField.PASSBAND) {
|
||||||
|
((rxFrequencyHz - rxLow).toFloat() / rxRange).coerceIn(0f, 1f)
|
||||||
|
} else {
|
||||||
|
((rxFrequencyHz - rxLow).toFloat() / rxRange).coerceIn(0f, 1f)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateTx(newTxHz: Long) {
|
||||||
|
when (lastEditedField) {
|
||||||
|
EditedField.PASSBAND -> {
|
||||||
|
// Passband 模式:滑块位置 → passbandPosition → 地面频率,RX 经 Transceiver 计算
|
||||||
|
val pos = ((newTxHz - txLow).toFloat() / txRange).coerceIn(0f, 1f)
|
||||||
|
passbandPosition = pos
|
||||||
|
txFrequencyHz = txLow + (pos * txRange).toLong()
|
||||||
|
rxFrequencyHz = DopplerFrequencyCalculator.computeDownlinkFromUplinkWithOffset(
|
||||||
|
txFrequencyHz, rawTransponder, orbitalPos, offsetHz
|
||||||
|
) ?: rxFrequencyHz
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
txFrequencyHz = newTxHz.coerceIn(txLow, txHigh)
|
||||||
|
rxFrequencyHz = DopplerFrequencyCalculator.computeDownlinkFromUplinkWithOffset(
|
||||||
|
txFrequencyHz, rawTransponder, orbitalPos, offsetHz
|
||||||
|
) ?: rxFrequencyHz
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateRx(newRxHz: Long) {
|
||||||
|
when (lastEditedField) {
|
||||||
|
EditedField.PASSBAND -> {
|
||||||
|
// Passband 模式:从 RX 反推 TX 位置,TX 用地面频率
|
||||||
|
val pos = ((newRxHz - rxLow).toFloat() / rxRange).coerceIn(0f, 1f)
|
||||||
|
val txFromRx = TransponderMapper.mapDownlinkToUplink(newRxHz, rawTransponder)
|
||||||
|
passbandPosition = if (txFromRx != null) {
|
||||||
|
((txFromRx - rawTxLow).toFloat() / rawTxRange).coerceIn(0f, 1f)
|
||||||
|
} else pos
|
||||||
|
txFrequencyHz = txLow + (passbandPosition * txRange).toLong()
|
||||||
|
rxFrequencyHz = DopplerFrequencyCalculator.computeDownlinkFromUplinkWithOffset(
|
||||||
|
txFrequencyHz, rawTransponder, orbitalPos, offsetHz
|
||||||
|
) ?: rxFrequencyHz
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
rxFrequencyHz = newRxHz.coerceIn(rxLow, rxHigh)
|
||||||
|
txFrequencyHz = DopplerFrequencyCalculator.computeUplinkFromDownlinkWithOffset(
|
||||||
|
rxFrequencyHz, rawTransponder, orbitalPos, offsetHz
|
||||||
|
) ?: txFrequencyHz
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier,
|
modifier = modifier.fillMaxWidth().padding(top = 16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
|
) {
|
||||||
|
// 左半边: TX + RX 按钮
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
FilterChip(
|
||||||
|
selected = lastEditedField == EditedField.TX,
|
||||||
|
onClick = {
|
||||||
|
when (lastEditedField) {
|
||||||
|
EditedField.TX -> {
|
||||||
|
passbandPosition = ((txFrequencyHz - txLow).toFloat() / txRange).coerceIn(0f, 1f)
|
||||||
|
lastEditedField = EditedField.PASSBAND
|
||||||
|
rxFrequencyHz = DopplerFrequencyCalculator.computeDownlinkFromUplinkWithOffset(
|
||||||
|
txFrequencyHz, rawTransponder, orbitalPos, offsetHz
|
||||||
|
) ?: rxFrequencyHz
|
||||||
|
}
|
||||||
|
EditedField.PASSBAND -> { lastEditedField = EditedField.TX }
|
||||||
|
else -> { lastEditedField = EditedField.TX }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label = {
|
||||||
|
Text("TX", fontSize = 13.sp, textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth())
|
||||||
|
},
|
||||||
|
modifier = Modifier.weight(1f).fillMaxHeight()
|
||||||
|
)
|
||||||
|
FilterChip(
|
||||||
|
selected = lastEditedField == EditedField.RX,
|
||||||
|
onClick = {
|
||||||
|
when (lastEditedField) {
|
||||||
|
EditedField.RX -> {
|
||||||
|
txFrequencyHz = DopplerFrequencyCalculator.computeUplinkFromDownlinkWithOffset(
|
||||||
|
rxFrequencyHz, rawTransponder, orbitalPos, offsetHz
|
||||||
|
) ?: txFrequencyHz
|
||||||
|
passbandPosition = ((txFrequencyHz - txLow).toFloat() / txRange).coerceIn(0f, 1f)
|
||||||
|
lastEditedField = EditedField.PASSBAND
|
||||||
|
}
|
||||||
|
EditedField.PASSBAND -> {
|
||||||
|
lastEditedField = EditedField.RX
|
||||||
|
txFrequencyHz = DopplerFrequencyCalculator.computeUplinkFromDownlinkWithOffset(
|
||||||
|
rxFrequencyHz, rawTransponder, orbitalPos, offsetHz
|
||||||
|
) ?: txFrequencyHz
|
||||||
|
}
|
||||||
|
else -> { lastEditedField = EditedField.RX }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label = {
|
||||||
|
Text("RX", fontSize = 13.sp, textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth())
|
||||||
|
},
|
||||||
|
modifier = Modifier.weight(1f).fillMaxHeight()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// 右半边: Offset 输入框(带细线边框)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(48.dp)
|
||||||
|
.border(1.dp, MaterialTheme.colorScheme.outline, MaterialTheme.shapes.small),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
BasicTextField(
|
||||||
|
value = offsetKHz,
|
||||||
|
onValueChange = { offsetKHz = it },
|
||||||
|
singleLine = true,
|
||||||
|
textStyle = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
),
|
||||||
|
decorationBox = { innerTextField ->
|
||||||
|
if (offsetKHz.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = "Offset (kHz)",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
innerTextField()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TX 行
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { updateTx(txFrequencyHz - stepSizeKHz * 1000L) },
|
||||||
|
modifier = Modifier.width(32.dp).height(28.dp),
|
||||||
|
contentPadding = PaddingValues(0.dp),
|
||||||
|
shape = MaterialTheme.shapes.extraSmall
|
||||||
|
) { Text("−", fontSize = 14.sp) }
|
||||||
|
|
||||||
|
Slider(
|
||||||
|
value = txSliderValue,
|
||||||
|
onValueChange = { norm ->
|
||||||
|
val rawFreq = txLow + (txRange * norm).toLong()
|
||||||
|
val snapped = ((rawFreq + 500) / 1000L) * 1000L
|
||||||
|
updateTx(snapped.coerceIn(txLow, txHigh))
|
||||||
|
},
|
||||||
|
valueRange = 0f..1f,
|
||||||
|
modifier = Modifier.weight(1f).height(20.dp).padding(horizontal = 4.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { updateTx(txFrequencyHz + stepSizeKHz * 1000L) },
|
||||||
|
modifier = Modifier.width(32.dp).height(28.dp),
|
||||||
|
contentPadding = PaddingValues(0.dp),
|
||||||
|
shape = MaterialTheme.shapes.extraSmall
|
||||||
|
) { Text("+", fontSize = 14.sp) }
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.radar_doppler_calc),
|
text = String.format(Locale.ENGLISH, "%.4f", txLow / 1_000_000.0),
|
||||||
fontSize = 14.sp,
|
fontSize = 12.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = String.format(Locale.ENGLISH, "%.6f", txFrequencyHz / 1_000_000.0),
|
||||||
|
fontSize = 20.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
color = MaterialTheme.colorScheme.primary
|
color = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.radar_doppler_info),
|
text = " MHz",
|
||||||
|
fontSize = 20.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = String.format(Locale.ENGLISH, "%.4f", txHigh / 1_000_000.0),
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
textAlign = TextAlign.End
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = 0.5.dp,
|
||||||
|
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Offset input
|
// RX 行
|
||||||
OutlinedTextField(
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
value = offsetKHz,
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
onValueChange = { newVal ->
|
OutlinedButton(
|
||||||
offsetKHz = newVal
|
onClick = { updateRx(rxFrequencyHz - stepSizeKHz * 1000L) },
|
||||||
// Trigger recompute based on last edited field
|
modifier = Modifier.width(32.dp).height(28.dp),
|
||||||
if (lastEditedBy == EditedField.TX) {
|
contentPadding = PaddingValues(0.dp),
|
||||||
val txMHz = txInputMHz.toDoubleOrNull()
|
shape = MaterialTheme.shapes.extraSmall
|
||||||
if (txMHz != null && txMHz > 0) {
|
) { Text("−", fontSize = 14.sp) }
|
||||||
val txHz = (txMHz * 1_000_000).toLong()
|
|
||||||
val rxHz = DopplerFrequencyCalculator.computeDownlinkFromUplinkWithOffset(
|
Slider(
|
||||||
txHz, transponder, orbitalPos, offsetHz
|
value = rxSliderValue,
|
||||||
)
|
onValueChange = { norm ->
|
||||||
rxInputMHz = if (rxHz != null) String.format(Locale.ENGLISH, "%.6f", rxHz / 1_000_000.0) else ""
|
val rawFreq = rxLow + (rxRange * norm).toLong()
|
||||||
}
|
val snapped = ((rawFreq + 500) / 1000L) * 1000L
|
||||||
} else {
|
updateRx(snapped.coerceIn(rxLow, rxHigh))
|
||||||
val rxMHz = rxInputMHz.toDoubleOrNull()
|
|
||||||
if (rxMHz != null && rxMHz > 0) {
|
|
||||||
val rxHz = (rxMHz * 1_000_000).toLong()
|
|
||||||
val txHz = DopplerFrequencyCalculator.computeUplinkFromDownlinkWithOffset(
|
|
||||||
rxHz, transponder, orbitalPos, offsetHz
|
|
||||||
)
|
|
||||||
txInputMHz = if (txHz != null) String.format(Locale.ENGLISH, "%.6f", txHz / 1_000_000.0) else ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
label = { Text(stringResource(R.string.radar_doppler_offset_hint)) },
|
valueRange = 0f..1f,
|
||||||
singleLine = true,
|
modifier = Modifier.weight(1f).height(20.dp).padding(horizontal = 4.dp)
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// TX and RX inputs on the same row
|
OutlinedButton(
|
||||||
|
onClick = { updateRx(rxFrequencyHz + stepSizeKHz * 1000L) },
|
||||||
|
modifier = Modifier.width(32.dp).height(28.dp),
|
||||||
|
contentPadding = PaddingValues(0.dp),
|
||||||
|
shape = MaterialTheme.shapes.extraSmall
|
||||||
|
) { Text("+", fontSize = 14.sp) }
|
||||||
|
}
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 4.dp),
|
||||||
modifier = Modifier.fillMaxWidth()
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// TX input → compute RX
|
Text(
|
||||||
OutlinedTextField(
|
text = String.format(Locale.ENGLISH, "%.4f", rxLow / 1_000_000.0),
|
||||||
value = txInputMHz,
|
fontSize = 12.sp,
|
||||||
onValueChange = { newVal ->
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
txInputMHz = newVal
|
|
||||||
lastEditedBy = EditedField.TX
|
|
||||||
val mhz = newVal.toDoubleOrNull()
|
|
||||||
if (mhz != null && mhz > 0) {
|
|
||||||
val txHz = (mhz * 1_000_000).toLong()
|
|
||||||
val rxHz = DopplerFrequencyCalculator.computeDownlinkFromUplinkWithOffset(
|
|
||||||
txHz, transponder, orbitalPos, offsetHz
|
|
||||||
)
|
|
||||||
rxInputMHz = if (rxHz != null) String.format(Locale.ENGLISH, "%.6f", rxHz / 1_000_000.0) else ""
|
|
||||||
} else if (newVal.isEmpty()) {
|
|
||||||
rxInputMHz = ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
label = { Text(stringResource(R.string.radar_doppler_tx_hint)) },
|
|
||||||
singleLine = true,
|
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
|
Row(
|
||||||
// RX input → compute TX
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
OutlinedTextField(
|
horizontalArrangement = Arrangement.Center
|
||||||
value = rxInputMHz,
|
) {
|
||||||
onValueChange = { newVal ->
|
Text(
|
||||||
rxInputMHz = newVal
|
text = String.format(Locale.ENGLISH, "%.6f", rxFrequencyHz / 1_000_000.0),
|
||||||
lastEditedBy = EditedField.RX
|
fontSize = 20.sp,
|
||||||
val mhz = newVal.toDoubleOrNull()
|
fontWeight = FontWeight.Bold,
|
||||||
if (mhz != null && mhz > 0) {
|
color = MaterialTheme.colorScheme.primary
|
||||||
val rxHz = (mhz * 1_000_000).toLong()
|
)
|
||||||
val txHz = DopplerFrequencyCalculator.computeUplinkFromDownlinkWithOffset(
|
Text(
|
||||||
rxHz, transponder, orbitalPos, offsetHz
|
text = " MHz",
|
||||||
|
fontSize = 20.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
txInputMHz = if (txHz != null) String.format(Locale.ENGLISH, "%.6f", txHz / 1_000_000.0) else ""
|
|
||||||
} else if (newVal.isEmpty()) {
|
|
||||||
txInputMHz = ""
|
|
||||||
}
|
}
|
||||||
},
|
Text(
|
||||||
label = { Text(stringResource(R.string.radar_doppler_rx_hint)) },
|
text = String.format(Locale.ENGLISH, "%.4f", rxHigh / 1_000_000.0),
|
||||||
singleLine = true,
|
fontSize = 12.sp,
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f),
|
||||||
|
textAlign = TextAlign.End
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private enum class EditedField { TX, RX }
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun CwDecoderPanel(
|
private fun CwDecoderPanel(
|
||||||
@@ -717,31 +938,37 @@ private fun CwDecoderPanel(
|
|||||||
// PR #1 Morse Expert engine: mini layout with waterfall + decoded text.
|
// PR #1 Morse Expert engine: mini layout with waterfall + decoded text.
|
||||||
// Keep the old Kotlin decoder state/actions as fallback code, but this panel no longer feeds it.
|
// Keep the old Kotlin decoder state/actions as fallback code, but this panel no longer feeds it.
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val activity = remember { context as? Activity }
|
val activity = remember(context) {
|
||||||
|
context as? Activity ?: error("CwDecoderPanel must be hosted in an Activity")
|
||||||
|
}
|
||||||
val controller = remember { MainActivity() }
|
val controller = remember { MainActivity() }
|
||||||
val rootView = remember {
|
val rootView = remember(context) {
|
||||||
LayoutInflater.from(context).inflate(CwR.layout.cw_panel_main, null) as ConstraintLayout
|
LayoutInflater.from(context).inflate(CwR.layout.activity_main, null) as ConstraintLayout
|
||||||
}
|
}
|
||||||
var initialized by remember { mutableStateOf(false) }
|
var initialized by remember { mutableStateOf(false) }
|
||||||
var listening by remember { mutableStateOf(false) }
|
var listening by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
DisposableEffect(Unit) {
|
// 重要: 不在展开时初始化控制器(避免"一展开就崩溃")。
|
||||||
if (activity != null) {
|
// controller.onCreate/onResume/onPermissionGranted 全部推迟到用户点 Start 才执行。
|
||||||
controller.onCreate(activity, rootView, false)
|
fun startDecoding() {
|
||||||
}
|
if (!initialized) {
|
||||||
onDispose {
|
controller.onCreate(activity, rootView)
|
||||||
controller.onPause()
|
|
||||||
controller.onDestroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(cw.hasPermission) {
|
|
||||||
if (cw.hasPermission && !initialized) {
|
|
||||||
controller.onPermissionGranted()
|
controller.onPermissionGranted()
|
||||||
controller.onResume()
|
controller.onResume()
|
||||||
initialized = true
|
initialized = true
|
||||||
|
} else {
|
||||||
|
controller.onResume()
|
||||||
|
}
|
||||||
listening = true
|
listening = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
if (initialized) {
|
||||||
|
controller.onPause()
|
||||||
|
controller.onDestroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
@@ -754,14 +981,8 @@ private fun CwDecoderPanel(
|
|||||||
onClick = {
|
onClick = {
|
||||||
if (!cw.hasPermission) {
|
if (!cw.hasPermission) {
|
||||||
requestMicPermission()
|
requestMicPermission()
|
||||||
} else if (!initialized) {
|
|
||||||
controller.onPermissionGranted()
|
|
||||||
controller.onResume()
|
|
||||||
initialized = true
|
|
||||||
listening = true
|
|
||||||
} else {
|
} else {
|
||||||
controller.onResume()
|
startDecoding()
|
||||||
listening = true
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
@@ -780,23 +1001,27 @@ private fun CwDecoderPanel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = { controller.clearDecoded() },
|
onClick = { if (initialized) controller.clearDecoded() },
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.radar_cw_reset))
|
Text(stringResource(R.string.radar_cw_reset))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AndroidView(
|
Box(
|
||||||
factory = { rootView },
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(150.dp)
|
.height(150.dp)
|
||||||
|
) {
|
||||||
|
AndroidView(
|
||||||
|
factory = { rootView },
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun FrequencyText(frequency: Long?, modifier: Modifier = Modifier) {
|
private fun FrequencyText(frequency: Long?, modifier: Modifier = Modifier) {
|
||||||
|
|||||||
Reference in new issue
Block a user