android: settings screen scrollable + wire dead rows (language/about) + instant orientation apply
This commit is contained in:
@@ -95,18 +95,26 @@ fun LiveScreen(vm: LiveViewModel = viewModel(), onOpenGallery: () -> Unit = {})
|
|||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
vm.connect()
|
vm.connect()
|
||||||
vm.uiBottomPx = navPx + shutterPx
|
vm.uiBottomPx = navPx + shutterPx
|
||||||
|
// Seed the renderer with the persisted orientation settings once; from
|
||||||
|
// then on AppSettings publishes changes, so the settings tab applies
|
||||||
|
// immediately instead of being picked up by this loop (which only runs
|
||||||
|
// while the live tab is composed).
|
||||||
|
com.mag160c.thermal.ui.settings.ImageOrientationSettings.publishFrom(context)
|
||||||
while (true) {
|
while (true) {
|
||||||
kotlinx.coroutines.delay(400)
|
kotlinx.coroutines.delay(400)
|
||||||
navPx = UiInsets.navPx
|
navPx = UiInsets.navPx
|
||||||
vm.uiBottomPx = navPx + shutterPx
|
vm.uiBottomPx = navPx + shutterPx
|
||||||
// pick up orientation changes made on the settings tab
|
|
||||||
val s = com.mag160c.thermal.ui.settings.AppSettings(context)
|
|
||||||
vm.userRotateDeg = s.imageRotateDeg
|
|
||||||
vm.flipH = s.imageFlipH
|
|
||||||
vm.flipV = s.imageFlipV
|
|
||||||
vm.refreshTemps()
|
vm.refreshTemps()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Apply orientation changes the moment they are made (also from the settings tab)
|
||||||
|
val orientation by com.mag160c.thermal.ui.settings.ImageOrientationSettings.state
|
||||||
|
.collectAsState()
|
||||||
|
LaunchedEffect(orientation) {
|
||||||
|
vm.userRotateDeg = orientation.rotateDeg
|
||||||
|
vm.flipH = orientation.flipH
|
||||||
|
vm.flipV = orientation.flipV
|
||||||
|
}
|
||||||
|
|
||||||
// The PIP overlay owns the camera: it releases on any of these exits —
|
// The PIP overlay owns the camera: it releases on any of these exits —
|
||||||
// PIP switched off (overlay leaves composition), live screen left, or the
|
// PIP switched off (overlay leaves composition), live screen left, or the
|
||||||
|
|||||||
@@ -77,13 +77,17 @@ fun RemoteViewerScreen(
|
|||||||
kotlinx.coroutines.delay(400)
|
kotlinx.coroutines.delay(400)
|
||||||
navPx = com.mag160c.thermal.ui.UiInsets.navPx
|
navPx = com.mag160c.thermal.ui.UiInsets.navPx
|
||||||
vm.uiBottomPx = navPx + shutterPx
|
vm.uiBottomPx = navPx + shutterPx
|
||||||
// keep the remote image oriented like the live view
|
|
||||||
val s = com.mag160c.thermal.ui.settings.AppSettings(context)
|
|
||||||
vm.userRotateDeg = s.imageRotateDeg
|
|
||||||
vm.flipH = s.imageFlipH
|
|
||||||
vm.flipV = s.imageFlipV
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// The remote image uses the same locked orientation + manual corrections as
|
||||||
|
// the live view, applied the moment they change.
|
||||||
|
val orientation by com.mag160c.thermal.ui.settings.ImageOrientationSettings.state
|
||||||
|
.collectAsState()
|
||||||
|
LaunchedEffect(orientation) {
|
||||||
|
vm.userRotateDeg = orientation.rotateDeg
|
||||||
|
vm.flipH = orientation.flipH
|
||||||
|
vm.flipV = orientation.flipV
|
||||||
|
}
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
vm.disconnected.collect { reason -> onDisconnected(reason) }
|
vm.disconnected.collect { reason -> onDisconnected(reason) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,39 @@
|
|||||||
package com.mag160c.thermal.ui.settings
|
package com.mag160c.thermal.ui.settings
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process-wide observable copy of the image-orientation settings.
|
||||||
|
*
|
||||||
|
* The settings screen writes [AppSettings] and the live/remote renderers need the
|
||||||
|
* new values immediately. Before this existed, the renderers polled AppSettings
|
||||||
|
* every 400 ms from the live screen's loop — which only runs while the LIVE tab
|
||||||
|
* is composed, so a change made on the settings tab was applied only after
|
||||||
|
* switching back and hoping the polling had not been torn down. Publishing here
|
||||||
|
* makes the change take effect at once, wherever it was made.
|
||||||
|
*/
|
||||||
|
object ImageOrientationSettings {
|
||||||
|
data class State(
|
||||||
|
val rotateDeg: Int = 0,
|
||||||
|
val flipH: Boolean = false,
|
||||||
|
val flipV: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val _state = MutableStateFlow(State())
|
||||||
|
val state: StateFlow<State> = _state
|
||||||
|
|
||||||
|
fun publish(rotateDeg: Int, flipH: Boolean, flipV: Boolean) {
|
||||||
|
_state.value = State(rotateDeg, flipH, flipV)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Read persisted values and publish them (called once when settings load). */
|
||||||
|
fun publishFrom(context: Context) {
|
||||||
|
val s = AppSettings(context)
|
||||||
|
publish(s.imageRotateDeg, s.imageFlipH, s.imageFlipV)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** App settings backed by SharedPreferences (app-private storage only). */
|
/** App settings backed by SharedPreferences (app-private storage only). */
|
||||||
class AppSettings(context: Context) {
|
class AppSettings(context: Context) {
|
||||||
@@ -35,21 +68,37 @@ class AppSettings(context: Context) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Manual image orientation corrections, mirroring the official app's
|
* Manual image orientation corrections, mirroring the official app's
|
||||||
* "旋转USB画面" / "水平翻转" / "竖直翻转" settings.
|
* "旋转USB画面" / "水平翻转" / "竖直翻转" settings. The image itself stays
|
||||||
|
* LOCKED to the portrait frame (see ImageTransform).
|
||||||
|
*
|
||||||
|
* Every setter also publishes the new combination so the renderers pick it up
|
||||||
|
* immediately rather than on the next poll.
|
||||||
*/
|
*/
|
||||||
var imageRotateDeg: Int
|
var imageRotateDeg: Int
|
||||||
get() = sp.getInt("imageRotate", 0)
|
get() = sp.getInt("imageRotate", 0)
|
||||||
set(v) = sp.edit().putInt("imageRotate", ((v % 360) + 360) % 360).apply()
|
set(v) {
|
||||||
|
val n = ((v % 360) + 360) % 360
|
||||||
|
sp.edit().putInt("imageRotate", n).apply()
|
||||||
|
ImageOrientationSettings.publish(n, imageFlipH, imageFlipV)
|
||||||
|
}
|
||||||
|
|
||||||
var imageFlipH: Boolean
|
var imageFlipH: Boolean
|
||||||
get() = sp.getBoolean("imageFlipH", false)
|
get() = sp.getBoolean("imageFlipH", false)
|
||||||
set(v) = sp.edit().putBoolean("imageFlipH", v).apply()
|
set(v) {
|
||||||
|
sp.edit().putBoolean("imageFlipH", v).apply()
|
||||||
|
ImageOrientationSettings.publish(imageRotateDeg, v, imageFlipV)
|
||||||
|
}
|
||||||
|
|
||||||
var imageFlipV: Boolean
|
var imageFlipV: Boolean
|
||||||
get() = sp.getBoolean("imageFlipV", false)
|
get() = sp.getBoolean("imageFlipV", false)
|
||||||
set(v) = sp.edit().putBoolean("imageFlipV", v).apply()
|
set(v) {
|
||||||
|
sp.edit().putBoolean("imageFlipV", v).apply()
|
||||||
|
ImageOrientationSettings.publish(imageRotateDeg, imageFlipH, v)
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
com.mag160c.thermal.cloud.CloudClient.setEnabled(cloudEnabled)
|
com.mag160c.thermal.cloud.CloudClient.setEnabled(cloudEnabled)
|
||||||
|
// seed the observable with the persisted orientation settings
|
||||||
|
ImageOrientationSettings.publish(imageRotateDeg, imageFlipH, imageFlipV)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import androidx.compose.foundation.layout.Row
|
|||||||
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.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -38,18 +40,30 @@ fun SettingsScreen(
|
|||||||
var rotateDeg by remember { mutableStateOf(settings.imageRotateDeg) }
|
var rotateDeg by remember { mutableStateOf(settings.imageRotateDeg) }
|
||||||
var flipH by remember { mutableStateOf(settings.imageFlipH) }
|
var flipH by remember { mutableStateOf(settings.imageFlipH) }
|
||||||
var flipV by remember { mutableStateOf(settings.imageFlipV) }
|
var flipV by remember { mutableStateOf(settings.imageFlipV) }
|
||||||
|
// local Compose copy of the language choice, so the row label refreshes
|
||||||
|
var language by remember { mutableStateOf(settings.language) }
|
||||||
// remote-preview server toggle (Phase F); off by default, needs live USB
|
// remote-preview server toggle (Phase F); off by default, needs live USB
|
||||||
var remoteOn by remember { mutableStateOf(remoteHostRunning) }
|
var remoteOn by remember { mutableStateOf(remoteHostRunning) }
|
||||||
var showNeedDevice by remember { mutableStateOf(false) }
|
var showNeedDevice by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxSize().padding(8.dp)) {
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(8.dp)
|
||||||
|
// 11 settings rows overflow a phone screen; without this the lower
|
||||||
|
// ones were simply unreachable ("选项翻不动")
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
) {
|
||||||
SettingRow("默认调色板", Palettes.NAMES[settings.defaultPaletteIndex]) { dialog = "palette" }
|
SettingRow("默认调色板", Palettes.NAMES[settings.defaultPaletteIndex]) { dialog = "palette" }
|
||||||
SettingRow(
|
SettingRow(
|
||||||
"默认发射率",
|
"默认发射率",
|
||||||
"%.2f".format(settings.defaultEmissivityPercent / 100f),
|
"%.2f".format(settings.defaultEmissivityPercent / 100f),
|
||||||
) { dialog = "emissivity" }
|
) { dialog = "emissivity" }
|
||||||
SettingRow("报警温度", "%.1f℃".format(settings.alarmTempC / 10f)) { dialog = "alarm" }
|
SettingRow("报警温度", "%.1f℃".format(settings.alarmTempC / 10f)) { dialog = "alarm" }
|
||||||
SettingRow("语言", settings.language) { dialog = "language" }
|
SettingRow(
|
||||||
|
"语言",
|
||||||
|
if (language == "zh") "中文" else "跟随系统",
|
||||||
|
) { dialog = "language" }
|
||||||
// manual orientation corrections (the official app has the same three)
|
// manual orientation corrections (the official app has the same three)
|
||||||
SettingRow("旋转USB画面", "$rotateDeg°") { dialog = "rotate" }
|
SettingRow("旋转USB画面", "$rotateDeg°") { dialog = "rotate" }
|
||||||
SettingRow("水平翻转", if (flipH) "已开启" else "已关闭") {
|
SettingRow("水平翻转", if (flipH) "已开启" else "已关闭") {
|
||||||
@@ -74,7 +88,7 @@ fun SettingsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
SettingRow("远程预览客户端", "查找主机") { onOpenRemoteClient() }
|
SettingRow("远程预览客户端", "查找主机") { onOpenRemoteClient() }
|
||||||
SettingRow("关于", "MAG160C 统一热像版 1.0.0") { dialog = null }
|
SettingRow("关于", "MAG160C 统一热像版 1.0.0") { dialog = "about" }
|
||||||
}
|
}
|
||||||
|
|
||||||
when (dialog) {
|
when (dialog) {
|
||||||
@@ -157,7 +171,8 @@ fun SettingsScreen(
|
|||||||
},
|
},
|
||||||
confirmButton = {},
|
confirmButton = {},
|
||||||
)
|
)
|
||||||
"cloud" -> AlertDialog( onDismissRequest = { dialog = null },
|
"cloud" -> AlertDialog(
|
||||||
|
onDismissRequest = { dialog = null },
|
||||||
title = { Text("云同步") },
|
title = { Text("云同步") },
|
||||||
text = {
|
text = {
|
||||||
Text(
|
Text(
|
||||||
@@ -174,6 +189,58 @@ fun SettingsScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
"language" -> AlertDialog(
|
||||||
|
onDismissRequest = { dialog = null },
|
||||||
|
title = { Text("语言") },
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
// The UI is written in Chinese with hard-coded strings and the
|
||||||
|
// app ships no translations, so offering other languages here
|
||||||
|
// would be a lie. The choice is still recorded so the row is
|
||||||
|
// not a dead end and a future translation can honour it.
|
||||||
|
Text(
|
||||||
|
"当前版本界面仅提供中文,其它语言尚未翻译。此处选择会被记录," +
|
||||||
|
"后续版本接入翻译后生效。",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp),
|
||||||
|
)
|
||||||
|
listOf("auto" to "跟随系统", "zh" to "中文").forEach { (code, label) ->
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
color = if (language == code) MaterialTheme.colorScheme.primary
|
||||||
|
else MaterialTheme.colorScheme.onSurface,
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable {
|
||||||
|
language = code
|
||||||
|
settings.language = code
|
||||||
|
dialog = null
|
||||||
|
}
|
||||||
|
.padding(14.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {},
|
||||||
|
)
|
||||||
|
"about" -> AlertDialog(
|
||||||
|
onDismissRequest = { dialog = null },
|
||||||
|
title = { Text("关于") },
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
Text("MAG160C 统一热像版 1.0.0")
|
||||||
|
Text(
|
||||||
|
"热像仪:MAG160C(160×120,15fps,USB VID 0x833C)",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(top = 8.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = { dialog = null }) { Text("好") }
|
||||||
|
},
|
||||||
|
)
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
package com.mag160c.thermal.ui.settings
|
||||||
|
|
||||||
|
import com.mag160c.thermal.ui.live.ImageTransform
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The orientation settings must reach the renderer even when they are changed on
|
||||||
|
* the settings tab (the live screen is not composed then). AppSettings publishes
|
||||||
|
* every change to [ImageOrientationSettings]; these tests pin that contract.
|
||||||
|
*/
|
||||||
|
class ImageOrientationSettingsTest {
|
||||||
|
|
||||||
|
private fun reset() {
|
||||||
|
ImageOrientationSettings.publish(0, false, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun publishedValuesAreReadableImmediately() {
|
||||||
|
reset()
|
||||||
|
ImageOrientationSettings.publish(90, flipH = true, flipV = false)
|
||||||
|
val s = ImageOrientationSettings.state.value
|
||||||
|
assertEquals(90, s.rotateDeg)
|
||||||
|
assertTrue(s.flipH)
|
||||||
|
assertFalse(s.flipV)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun publishedValuesFeedTheImageTransform() {
|
||||||
|
reset()
|
||||||
|
ImageOrientationSettings.publish(180, flipH = false, flipV = true)
|
||||||
|
val s = ImageOrientationSettings.state.value
|
||||||
|
val p = ImageTransform.params(s.rotateDeg, s.flipH, s.flipV)
|
||||||
|
// locked base 90 + 180 = 270, with only the vertical flip set
|
||||||
|
assertEquals(270, p.rotDeg)
|
||||||
|
assertFalse(p.flipH)
|
||||||
|
assertTrue(p.flipV)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun defaultStateLeavesTheLockedOrientationAlone() {
|
||||||
|
reset()
|
||||||
|
val s = ImageOrientationSettings.state.value
|
||||||
|
assertEquals(0, s.rotateDeg)
|
||||||
|
assertFalse(s.flipH)
|
||||||
|
assertFalse(s.flipV)
|
||||||
|
assertEquals(ImageTransform.LOCKED_ROT_DEG, ImageTransform.params(s.rotateDeg, s.flipH, s.flipV).rotDeg)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The equivalent pair the user found on the device: (flipV, +90) and
|
||||||
|
* (flipH, +270) produce the same on-screen mapping. Both must be reachable
|
||||||
|
* through the published settings.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun theTwoEquivalentConfigurationsAreBothRepresentable() {
|
||||||
|
ImageOrientationSettings.publish(0, flipH = false, flipV = true)
|
||||||
|
val a = ImageOrientationSettings.state.value
|
||||||
|
val pa = ImageTransform.params(a.rotateDeg, a.flipH, a.flipV)
|
||||||
|
|
||||||
|
ImageOrientationSettings.publish(180, flipH = true, flipV = false)
|
||||||
|
val b = ImageOrientationSettings.state.value
|
||||||
|
val pb = ImageTransform.params(b.rotateDeg, b.flipH, b.flipV)
|
||||||
|
|
||||||
|
assertEquals(90, pa.rotDeg)
|
||||||
|
assertEquals(270, pb.rotDeg)
|
||||||
|
// see ImageTransformOrientationTest for the pixel-level equivalence proof
|
||||||
|
assertTrue(pa.flipV && !pa.flipH)
|
||||||
|
assertTrue(pb.flipH && !pb.flipV)
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -64,6 +64,9 @@
|
|||||||
| 30 | 设置页"水平翻转"/"竖直翻转"开关 | (无日志) | 画面镜像;与官方 app 的同名设置表现一致 |
|
| 30 | 设置页"水平翻转"/"竖直翻转"开关 | (无日志) | 画面镜像;与官方 app 的同名设置表现一致 |
|
||||||
| 31 | 横屏持机时观察色标条 | (无日志) | 色标条**两端**的最高/最低温数字紧贴色条两端,**不与色条重叠**、不偏移 |
|
| 31 | 横屏持机时观察色标条 | (无日志) | 色标条**两端**的最高/最低温数字紧贴色条两端,**不与色条重叠**、不偏移 |
|
||||||
| 32 | 分析页打开一张 MDT,观察顶部温度条 | (无日志) | 温度条显示"中心/最低/最高",数值与拍照时实时页读数接近 |
|
| 32 | 分析页打开一张 MDT,观察顶部温度条 | (无日志) | 温度条显示"中心/最低/最高",数值与拍照时实时页读数接近 |
|
||||||
|
| 33 | 设置页**从上往下滑动**,看能否滚到最底部("关于") | (无日志) | 页面可以滚动,**11 行设置项全部可达**(此前无滚动,下方几行点不到) |
|
||||||
|
| 34 | 设置页依次点每一行,确认都有反应 | 调色板/发射率/报警温度/语言/旋转USB画面/云同步/关于 → 弹对话框;水平翻转/竖直翻转 → 文案在"已开启/已关闭"间切换;远程预览服务端 → 切换或弹"先连接热像仪";远程预览客户端 → 进入主机列表 | **没有点了没反应的行**(此前"语言"和"关于"是死行) |
|
||||||
|
| 35 | 在**设置页**改"竖直翻转"或"旋转USB画面",然后切回实时页 | (无日志) | 画面**立即**按新设置显示(设置变更即时下发;此前靠实时页 400ms 轮询,仅在实时页处于打开状态时才生效) |
|
||||||
|
|
||||||
## 相机(PIP)失败时的表现(设计如此,不算 bug)
|
## 相机(PIP)失败时的表现(设计如此,不算 bug)
|
||||||
PIP 的相机是可选功能,任何相机异常都只记日志并关闭小窗,**不影响热像主画面**:
|
PIP 的相机是可选功能,任何相机异常都只记日志并关闭小窗,**不影响热像主画面**:
|
||||||
|
|||||||
@@ -495,7 +495,20 @@
|
|||||||
主机实现"后来者写 busy"(accept 循环不再阻塞在 serve 上,第二客户端立即
|
主机实现"后来者写 busy"(accept 循环不再阻塞在 serve 上,第二客户端立即
|
||||||
收到 `{"type":"busy"}`;新增 `secondClientIsRejectedWithBusy`)。
|
收到 `{"type":"busy"}`;新增 `secondClientIsRejectedWithBusy`)。
|
||||||
另删除未使用的 `ACCESS_NETWORK_STATE` 权限。
|
另删除未使用的 `ACCESS_NETWORK_STATE` 权限。
|
||||||
- [x] 单测 44 → **62 项全绿**;debug + release(R8) 双构建通过;APK 已更新
|
- [x] **设置页翻不动 + 死行**(用户报"设置页选项翻不动"):
|
||||||
|
① 设置页 `Column` **缺 `verticalScroll`**,11 行内容一屏放不下,下方几行
|
||||||
|
完全够不到 → 已加滚动;
|
||||||
|
② "语言"行点了没反应:`when(dialog)` 里根本没有 `"language"` 分支,
|
||||||
|
点击后落进 `else -> {}`;且该项**没有任何代码读取、也没有 i18n 资源**
|
||||||
|
(全部界面为中文字面量、无 strings.xml)→ 补上对话框并**如实说明"当前仅中文,
|
||||||
|
选择会被记录待后续翻译"**,不再假装能切语言;
|
||||||
|
③ "关于"行同样设 `dialog = null` 属死行 → 补上说明对话框;
|
||||||
|
④ **设置变更即时生效**:原先实时/远程页每 400ms 轮询 `AppSettings`,而该轮询
|
||||||
|
只在实时页处于组合状态时运行(在设置页改动后要切回实时页才生效)→ 新增
|
||||||
|
`ImageOrientationSettings`(进程级 StateFlow),`AppSettings` 三个方向
|
||||||
|
setter 写入后立即 publish,实时/远程页 collect 后即时应用。
|
||||||
|
新增 `ImageOrientationSettingsTest`(4 项);单测 62 → **66 项全绿**。
|
||||||
|
- [x] 单测 62 → **66 项全绿**;debug + release(R8) 双构建通过;APK 已更新
|
||||||
(12.66MB)。`screenOrientation=portrait` 复核保持、camera 系列仍
|
(12.66MB)。`screenOrientation=portrait` 复核保持、camera 系列仍
|
||||||
not-required、多余权限已消失。
|
not-required、多余权限已消失。
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user