UI: PiezoMonitoring 누적 업데이트 (108-122)
- 빌드 108: "Void/" → "Voiding/", Toast "Recorded" → 48sp Dialog 오버레이 (1.5초 자동) - 빌드 109: Donut 캡 제거, 너비×0.85 적응형 (dev 모드는 0.65) - 빌드 110: useScrollLayout = isDevMode && showDevPanels — 패널 표시될 때만 스크롤 레이아웃 - 빌드 111+115: Volume Text 24→72sp (3× ExtraBold, letterSpacing -1), Bladdy 0.85→0.483, 흰 bg 0.85→0.78, scroll 40/55→46/63dp - 빌드 113: Voiding 시 Auto Scan 자동 stop + displayMaxVolumeMl/window/timer 정규화 - 빌드 114: Status 배지 dev 모드 한정 탭 → 60~maxVol-50 더미 측정값 랜덤 주입 - 빌드 117: 설정 패널 9개 라벨 14sp→28sp, 행간 spacedBy 6→9dp, HorizontalDivider 9개 padding(vertical=28dp) - 빌드 122: 측정 상단 좌측 Home 아이콘 추가 (Placement 옆), Placement 진입은 enterPlacementFromMonitoring() 경유 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+76
-39
@@ -78,11 +78,33 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
var reconnectionState by remember { mutableStateOf(BleReconnectionState.DISCONNECTED) }
|
||||
var isAutoMeasuring by remember { mutableStateOf(false) }
|
||||
var showDevPanels by remember { mutableStateOf(false) }
|
||||
// Charles: Scan Log dialog + Bladdy 3-tap dev toggle
|
||||
var showScanLogDialog by remember { mutableStateOf(false) }
|
||||
val scanLogState by ScanLogStore.scanLog.collectAsState()
|
||||
// Triple-tap Bladdy to toggle Developer Mode (parity with HomeView)
|
||||
var devTapCount by remember { mutableIntStateOf(0) }
|
||||
var devLastTapTime by remember { mutableLongStateOf(0L) }
|
||||
// 빌드 110: 스크롤 레이아웃은 dev 패널이 실제로 표시될 때만 활성화
|
||||
val useScrollLayout = appState.isDevMode && showDevPanels
|
||||
// 빌드 108: Voiding 기록 후 큰 확인 다이얼로그 (1.5초 자동 닫힘)
|
||||
var showRecordedDialog by remember { mutableStateOf(false) }
|
||||
if (showRecordedDialog) {
|
||||
LaunchedEffect(showRecordedDialog) {
|
||||
kotlinx.coroutines.delay(1500)
|
||||
showRecordedDialog = false
|
||||
}
|
||||
androidx.compose.ui.window.Dialog(onDismissRequest = { showRecordedDialog = false }) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.9f)
|
||||
.clip(RoundedCornerShape(24.dp))
|
||||
.background(MlPrimary.copy(alpha = 0.95f))
|
||||
.padding(horizontal = 32.dp, vertical = 64.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("Recorded", fontSize = 48.sp, fontWeight = FontWeight.ExtraBold, color = Color.White)
|
||||
}
|
||||
}
|
||||
}
|
||||
var isPlacementTesting by remember { mutableStateOf(false) }
|
||||
var measureRetryCount by remember { mutableIntStateOf(0) }
|
||||
val maxMeasureRetries = 2
|
||||
@@ -820,7 +842,11 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
) {
|
||||
val btnSize = if (isCompact) 48.dp else 56.dp
|
||||
val iconSize = if (isCompact) 28.dp else 32.dp
|
||||
IconButton(onClick = { appState.currentScreen = AppScreen.PLACEMENT_GUIDE }, modifier = Modifier.size(btnSize)) {
|
||||
// 빌드 122: Home 아이콘 추가 (Placement 옆)
|
||||
IconButton(onClick = { appState.goHome() }, modifier = Modifier.size(btnSize)) {
|
||||
Icon(Icons.Default.Home, "Home", tint = MlPrimary, modifier = Modifier.size(iconSize))
|
||||
}
|
||||
IconButton(onClick = { appState.enterPlacementFromMonitoring() }, modifier = Modifier.size(btnSize)) {
|
||||
Icon(Icons.Default.MyLocation, "Placement", tint = MlPrimary, modifier = Modifier.size(iconSize))
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
@@ -949,13 +975,13 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
.padding(horizontal = hPad)
|
||||
.padding(top = if (isCompact) 8.dp else 16.dp, bottom = if (isCompact) 16.dp else 32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = if (appState.isDevMode)
|
||||
verticalArrangement = if (useScrollLayout)
|
||||
Arrangement.spacedBy(if (isCompact) 12.dp else 20.dp)
|
||||
else
|
||||
Arrangement.spacedBy(nonDevSpacing)
|
||||
) {
|
||||
|
||||
// Status badge (항상 urgency 메시지)
|
||||
// Status badge (항상 urgency 메시지) — 빌드 114: dev 모드 한정 탭 시 더미 측정값 주입
|
||||
val statusText = urgency.message
|
||||
val statusColor = urgency.accentColor
|
||||
Text(
|
||||
@@ -966,10 +992,20 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.background(statusColor.copy(alpha = 0.15f))
|
||||
.then(if (appState.isDevMode) Modifier.clickable {
|
||||
val maxVol = appState.piezoSettings.maxVolume
|
||||
val dummy = (60..(maxVol - 50).coerceAtLeast(70)).random().toDouble()
|
||||
lastVolumeMl = dummy
|
||||
maxVolumeMl = maxOf(maxVolumeMl, dummy)
|
||||
displayMaxVolumeMl = maxVolumeMl
|
||||
val pct = (dummy / maxVol.toDouble()).coerceAtMost(1.0)
|
||||
val level = (pct * BladderLevel.MAX_LEVEL).toInt().coerceIn(0, BladderLevel.MAX_LEVEL)
|
||||
appState.bladderLevel = appState.bladderLevel.copy(currentLevel = level)
|
||||
} else Modifier)
|
||||
.padding(horizontal = 20.dp, vertical = 8.dp)
|
||||
)
|
||||
|
||||
// Donut + Bladdy sizing — BoxWithConstraints 의 availableH 기반.
|
||||
// Donut + Bladdy sizing — Charles의 height-aware donutFitSize 채택 [833c110]
|
||||
// - dev 모드: isCompact 기반 고정 dp (scroll 가능)
|
||||
// - non-dev: donutFitSize 사용 → 화면 높이에 맞춰 자동 축소 (120~240dp cap)
|
||||
val baseDonutSize = if (appState.isDevMode) {
|
||||
@@ -994,11 +1030,11 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
val donutProgress = if (maxVolumeForDonut > 0 && maxVolumeMl > 0)
|
||||
(maxVolumeMl / maxVolumeForDonut).coerceIn(0.0, 1.0) else 0.0
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
// 원 내부 흰색 톤 배경
|
||||
if (!appState.isDevMode) {
|
||||
// 원 내부 흰색 톤 배경 (빌드 111: 0.85→0.78)
|
||||
if (!useScrollLayout) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(donutSize * 0.85f)
|
||||
.size(donutSize * 0.78f)
|
||||
.clip(CircleShape)
|
||||
.background(Color.White.copy(alpha = 0.5f))
|
||||
)
|
||||
@@ -1015,11 +1051,9 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.matchParentSize().offset(y = -(donutSize * 0.03f))
|
||||
) {
|
||||
// Charles 833c110: Bladdy + 3-tap dev mode toggle (HomeView 패턴 통일)
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
BladdyRiveView(urgency = urgency, size = bladdySize)
|
||||
// Transparent overlay — Rive AndroidView swallows touches,
|
||||
// so we layer a clickable Box on top to catch the 3-tap
|
||||
// dev-mode gesture (same trick HomeView uses).
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(bladdySize)
|
||||
@@ -1044,8 +1078,9 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
Text(
|
||||
if (displayMaxVolumeMl > 0) "${"%.0f".format(displayMaxVolumeMl)} mL" else "${appState.estimatedVolume} mL",
|
||||
fontSize = volFontSize,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontWeight = if (useScrollLayout) FontWeight.Bold else FontWeight.ExtraBold,
|
||||
color = urgency.accentColor,
|
||||
letterSpacing = if (useScrollLayout) 0.sp else (-1).sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1069,7 +1104,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
// Control buttons: Void/Catheterize / Auto / Spot
|
||||
val btnHeight = (screenWidth * 0.18f).dp.coerceIn(56.dp, 80.dp)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(if (appState.isDevMode) 8.dp else 10.dp)) {
|
||||
// Void / Catheterize — 즉시 카테터 1개 감소 + 기록
|
||||
// Voiding / Catheterize — 즉시 카테터 1개 감소 + 기록 + Auto 자동 stop + 정규화
|
||||
Button(
|
||||
onClick = {
|
||||
val quickVol = if (lastVolumeMl > 0) lastVolumeMl.toInt() else appState.estimatedVolume
|
||||
@@ -1078,13 +1113,18 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
volume = quickVol, method = "catheterize"
|
||||
)
|
||||
)
|
||||
// 빌드 113: Voiding 시 Auto Scan 자동 stop + 측정 상태 정규화
|
||||
if (isAutoMeasuring) stopAutoMeasure()
|
||||
appState.resetLevel()
|
||||
lastVolumeMl = 0.0
|
||||
maxVolumeMl = 0.0
|
||||
displayMaxVolumeMl = 0.0
|
||||
autoVolumeWindow.clear()
|
||||
resetMeasurement()
|
||||
android.widget.Toast.makeText(context, "Recorded", android.widget.Toast.LENGTH_SHORT).show()
|
||||
// 빌드 108: Toast → 48sp Dialog 오버레이 (1.5초 자동)
|
||||
showRecordedDialog = true
|
||||
},
|
||||
enabled = !isSpotInProgress && !isAutoMeasuring,
|
||||
enabled = !isSpotInProgress,
|
||||
modifier = Modifier.weight(1f).height(btnHeight),
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
@@ -1093,7 +1133,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
)
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text("Void/", fontSize = (13 * fontScale).sp, fontWeight = FontWeight.SemiBold, color = MlPrimary)
|
||||
Text("Voiding/", fontSize = (13 * fontScale).sp, fontWeight = FontWeight.SemiBold, color = MlPrimary)
|
||||
Text("Catheterization", fontSize = (13 * fontScale).sp, fontWeight = FontWeight.SemiBold, color = MlPrimary)
|
||||
}
|
||||
}
|
||||
@@ -1270,14 +1310,14 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
.verticalScroll(rememberScrollState()), // Charles: dev panel 길어져도 Save 도달
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
// Max Volume
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Bladder Capacity", fontSize = 14.sp, fontWeight = FontWeight.SemiBold)
|
||||
Text("Bladder Capacity", fontSize = 28.sp, fontWeight = FontWeight.SemiBold)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text("${appState.piezoSettings.maxVolume} mL", fontSize = 14.sp, fontWeight = FontWeight.Bold, color = MlPrimary)
|
||||
Text("${appState.piezoSettings.maxVolume} mL", fontSize = 28.sp, fontWeight = FontWeight.Bold, color = MlPrimary)
|
||||
}
|
||||
Slider(
|
||||
value = appState.piezoSettings.maxVolume.toFloat(),
|
||||
@@ -1287,10 +1327,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary)
|
||||
)
|
||||
|
||||
|
||||
// Ant Offset — 개발자 모드 전용 숫자 입력. BV 계산 시 전위벽 위치를 probe 쪽으로
|
||||
// 이 mm만큼 당기는 보정값. 0~20 mm. 1 mm = 약 0.52 sample이므로 정수 truncation에
|
||||
// 유의 (2 mm마다 1 sample씩 chord가 늘어남).
|
||||
// Charles 833c110: Ant Offset 개발자 입력 (BV 계산 시 전위벽 mm 보정, 0~20mm)
|
||||
if (appState.isDevMode) {
|
||||
HorizontalDivider()
|
||||
var antOffsetText by remember {
|
||||
@@ -1323,9 +1360,9 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
|
||||
// Catheterization Threshold
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Catheter Threshold", fontSize = 14.sp, fontWeight = FontWeight.SemiBold)
|
||||
Text("Catheter Threshold", fontSize = 28.sp, fontWeight = FontWeight.SemiBold)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text("Level ${appState.bladderLevel.catheterizationThreshold}", fontSize = 14.sp, fontWeight = FontWeight.Bold, color = MlPrimary)
|
||||
Text("Level ${appState.bladderLevel.catheterizationThreshold}", fontSize = 28.sp, fontWeight = FontWeight.Bold, color = MlPrimary)
|
||||
}
|
||||
Slider(
|
||||
value = appState.bladderLevel.catheterizationThreshold.toFloat(),
|
||||
@@ -1337,13 +1374,13 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
|
||||
// ── 개발자 전용 설정 ──
|
||||
if (appState.isDevMode) {
|
||||
HorizontalDivider()
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 28.dp))
|
||||
|
||||
// Low Echo Threshold (Otsu / manual)
|
||||
var otsuEnabled by remember { mutableStateOf(analyzer.useAdaptiveThreshold) }
|
||||
var currentThreshold by remember { mutableStateOf(GreenZoneConstants.lowEchoAmp) }
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Threshold", fontSize = 14.sp, fontWeight = FontWeight.SemiBold)
|
||||
Text("Threshold", fontSize = 28.sp, fontWeight = FontWeight.SemiBold)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
if (otsuEnabled) "Otsu: ${"%.0f".format(analyzer.lastOtsuThreshold)}" else "${"%.0f".format(currentThreshold)}",
|
||||
@@ -1367,12 +1404,12 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 28.dp))
|
||||
|
||||
// DPS
|
||||
var dpsText by remember { mutableStateOf("%.3f".format(PiezoHW.distancePerSample)) }
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("DPS", fontSize = 14.sp, fontWeight = FontWeight.SemiBold)
|
||||
Text("DPS", fontSize = 28.sp, fontWeight = FontWeight.SemiBold)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
OutlinedTextField(
|
||||
value = dpsText,
|
||||
@@ -1384,12 +1421,12 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 28.dp))
|
||||
|
||||
// Detection Method A/B/C
|
||||
var selectedMethod by remember { mutableStateOf(com.example.medilightv2android.managers.GreenZoneConstants.detectionMethod) }
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Detection", fontSize = 14.sp, fontWeight = FontWeight.SemiBold)
|
||||
Text("Detection", fontSize = 28.sp, fontWeight = FontWeight.SemiBold)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
val methods = listOf(
|
||||
com.example.medilightv2android.managers.DetectionMethod.METHOD_A to "A",
|
||||
@@ -1416,12 +1453,12 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 28.dp))
|
||||
|
||||
// BV Method (Frustum / V41)
|
||||
var selectedBvMethod by remember { mutableStateOf(com.example.medilightv2android.managers.GreenZoneConstants.bvMethod) }
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("BV", fontSize = 14.sp, fontWeight = FontWeight.SemiBold)
|
||||
Text("BV", fontSize = 28.sp, fontWeight = FontWeight.SemiBold)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
val bvMethods = listOf(
|
||||
com.example.medilightv2android.managers.BvMethod.FRUSTUM to "Frustum",
|
||||
@@ -1447,23 +1484,23 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 28.dp))
|
||||
|
||||
// SG Filter
|
||||
var sgEnabled by remember { mutableStateOf(analyzer.useSgFilter) }
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("SG Filter", fontSize = 14.sp, fontWeight = FontWeight.SemiBold)
|
||||
Text("SG Filter", fontSize = 28.sp, fontWeight = FontWeight.SemiBold)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Switch(checked = sgEnabled, onCheckedChange = { sgEnabled = it; analyzer.useSgFilter = it },
|
||||
colors = SwitchDefaults.colors(checkedTrackColor = MlPrimary))
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 28.dp))
|
||||
|
||||
// Post Max Index (바닥반사 무시 경계)
|
||||
var postMaxVal by remember { mutableStateOf(GreenZoneConstants.postMaxIdx.toFloat()) }
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Post Max", fontSize = 14.sp, fontWeight = FontWeight.SemiBold)
|
||||
Text("Post Max", fontSize = 28.sp, fontWeight = FontWeight.SemiBold)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("${postMaxVal.toInt()} / 100", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = MlPrimary)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
@@ -1477,7 +1514,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 28.dp))
|
||||
|
||||
// Disconnect
|
||||
var showDisconnectConfirm by remember { mutableStateOf(false) }
|
||||
@@ -1507,7 +1544,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
HorizontalDivider()
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 28.dp))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Version (BuildConfig.VERSION_NAME) — read-only
|
||||
|
||||
Reference in New Issue
Block a user