UI: 페어링 다이얼로그 + 태블릿 폰트스케일 + Placement 힌트/버튼 확대
- DeviceScanView: 페어링 안내 다이얼로그 추가 (bonding 완료 대기 중 표시) - PiezoMonitoringView: fontScale(screenWidth/360) 적용, 도넛 비율 조정, 버튼 높이/텍스트 확대, InfoCard 폰트스케일, Void/Catheterization 텍스트 - PlacementGuideView: 힌트 카드 28sp + 패딩 확대, Start Alignment/ Start Scanning 버튼 96dp + 28sp, 센서 BiasAlignment 0.55 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+19
-2
@@ -95,24 +95,31 @@ fun DeviceScanView(appState: AppState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 서비스 준비 + 페어링(bonding) 완료 확인 후 다음 화면
|
// 서비스 준비 + 페어링(bonding) 완료 확인 후 다음 화면
|
||||||
|
var showPairingHint by remember { mutableStateOf(false) }
|
||||||
LaunchedEffect(isServiceReady) {
|
LaunchedEffect(isServiceReady) {
|
||||||
if (isServiceReady) {
|
if (isServiceReady) {
|
||||||
connectingDeviceAddress = null
|
connectingDeviceAddress = null
|
||||||
// 페어링 완료 대기 (최대 30초, 500ms 간격 폴링)
|
if (bleManager.isBonded) {
|
||||||
|
// 이미 페어링됨 → 바로 진행
|
||||||
|
appState.deviceConnected()
|
||||||
|
} else {
|
||||||
|
// 페어링 필요 → 안내 표시 + 대기
|
||||||
|
showPairingHint = true
|
||||||
var waited = 0
|
var waited = 0
|
||||||
while (!bleManager.isBonded && waited < 30000) {
|
while (!bleManager.isBonded && waited < 30000) {
|
||||||
kotlinx.coroutines.delay(500)
|
kotlinx.coroutines.delay(500)
|
||||||
waited += 500
|
waited += 500
|
||||||
}
|
}
|
||||||
|
showPairingHint = false
|
||||||
if (bleManager.isBonded) {
|
if (bleManager.isBonded) {
|
||||||
appState.deviceConnected()
|
appState.deviceConnected()
|
||||||
} else {
|
} else {
|
||||||
// 페어링 거부/타임아웃 → 연결 해제
|
|
||||||
bleManager.disconnect()
|
bleManager.disconnect()
|
||||||
bleManager.connectionError.value = "Pairing required. Please pair the device."
|
bleManager.connectionError.value = "Pairing required. Please pair the device."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(connectionError) {
|
LaunchedEffect(connectionError) {
|
||||||
if (connectionError != null) connectingDeviceAddress = null
|
if (connectionError != null) connectingDeviceAddress = null
|
||||||
@@ -120,6 +127,16 @@ fun DeviceScanView(appState: AppState) {
|
|||||||
|
|
||||||
DisposableEffect(Unit) { onDispose { bleManager.stopScan() } }
|
DisposableEffect(Unit) { onDispose { bleManager.stopScan() } }
|
||||||
|
|
||||||
|
// 페어링 안내 다이얼로그
|
||||||
|
if (showPairingHint) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = {},
|
||||||
|
title = { Text("Pairing Required", fontWeight = FontWeight.Bold) },
|
||||||
|
text = { Text("A Bluetooth pairing request will appear.\nTap 'Pair', then enter PIN if prompted.") },
|
||||||
|
confirmButton = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|||||||
+18
-15
@@ -92,6 +92,8 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
|
|
||||||
val screenWidth = LocalConfiguration.current.screenWidthDp
|
val screenWidth = LocalConfiguration.current.screenWidthDp
|
||||||
val isCompact = screenWidth < 400
|
val isCompact = screenWidth < 400
|
||||||
|
// 태블릿(600dp+) 글자 스케일: 폰=1.0, 태블릿=~1.6
|
||||||
|
val fontScale = (screenWidth / 360f).coerceIn(1.0f, 1.8f)
|
||||||
|
|
||||||
val isDemoMode = appState.isDemoMode
|
val isDemoMode = appState.isDemoMode
|
||||||
val urgency = appState.bladderLevel.urgency
|
val urgency = appState.bladderLevel.urgency
|
||||||
@@ -769,7 +771,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
cornerRadius = androidx.compose.ui.geometry.CornerRadius(2f))
|
cornerRadius = androidx.compose.ui.geometry.CornerRadius(2f))
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Text("$batteryLvl%", fontSize = 12.sp, fontWeight = FontWeight.Bold, color = batteryColor)
|
Text("$batteryLvl%", fontSize = (12 * fontScale).sp, fontWeight = FontWeight.Bold, color = batteryColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -789,7 +791,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
) {
|
) {
|
||||||
Icon(Icons.Default.LocalHospital, "Catheter", tint = MlPrimary, modifier = Modifier.size(16.dp))
|
Icon(Icons.Default.LocalHospital, "Catheter", tint = MlPrimary, modifier = Modifier.size(16.dp))
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Text("$cathCount", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = MlPrimary)
|
Text("$cathCount", fontSize = (13 * fontScale).sp, fontWeight = FontWeight.Bold, color = MlPrimary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (showAddCatheter) {
|
if (showAddCatheter) {
|
||||||
@@ -862,7 +864,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
val statusColor = urgency.accentColor
|
val statusColor = urgency.accentColor
|
||||||
Text(
|
Text(
|
||||||
text = statusText,
|
text = statusText,
|
||||||
fontSize = 14.sp,
|
fontSize = (14 * fontScale).sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = statusColor,
|
color = statusColor,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -873,7 +875,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
|
|
||||||
// Donut chart with Bladdy — 화면 비율 기반
|
// Donut chart with Bladdy — 화면 비율 기반
|
||||||
val screenWidthDp = screenWidth.dp
|
val screenWidthDp = screenWidth.dp
|
||||||
val donutRatio = if (appState.isDevMode) 0.65f else 0.85f
|
val donutRatio = if (appState.isDevMode) 0.65f else 0.75f
|
||||||
val donutSize = screenWidthDp * donutRatio
|
val donutSize = screenWidthDp * donutRatio
|
||||||
val lineWidthRatio = if (appState.isDevMode) 0.07f else 0.08f
|
val lineWidthRatio = if (appState.isDevMode) 0.07f else 0.08f
|
||||||
val donutLineWidth = donutSize * lineWidthRatio
|
val donutLineWidth = donutSize * lineWidthRatio
|
||||||
@@ -917,7 +919,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info cards (Fill/Level은 maxVolume 기준)
|
// Info cards (Fill/Level은 maxVolume 기준)ㅋ
|
||||||
val maxVol = appState.piezoSettings.maxVolume.toDouble()
|
val maxVol = appState.piezoSettings.maxVolume.toDouble()
|
||||||
// fill/level은 maxVolumeMl(세션 최대) 기준
|
// fill/level은 maxVolumeMl(세션 최대) 기준
|
||||||
val fillPct = if (maxVol > 0 && maxVolumeMl > 0) (maxVolumeMl / maxVol * 100).coerceAtMost(100.0) else 0.0
|
val fillPct = if (maxVol > 0 && maxVolumeMl > 0) (maxVolumeMl / maxVol * 100).coerceAtMost(100.0) else 0.0
|
||||||
@@ -934,7 +936,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Control buttons: Void/Catheterize / Auto / Spot
|
// Control buttons: Void/Catheterize / Auto / Spot
|
||||||
val btnHeight = (screenWidth * 0.15f).dp.coerceIn(50.dp, 70.dp)
|
val btnHeight = (screenWidth * 0.18f).dp.coerceIn(56.dp, 80.dp)
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(if (appState.isDevMode) 8.dp else 10.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(if (appState.isDevMode) 8.dp else 10.dp)) {
|
||||||
// Void / Catheterize — 즉시 카테터 1개 감소 + 기록
|
// Void / Catheterize — 즉시 카테터 1개 감소 + 기록
|
||||||
Button(
|
Button(
|
||||||
@@ -952,7 +954,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
android.widget.Toast.makeText(context, "Recorded", android.widget.Toast.LENGTH_SHORT).show()
|
android.widget.Toast.makeText(context, "Recorded", android.widget.Toast.LENGTH_SHORT).show()
|
||||||
},
|
},
|
||||||
enabled = !isSpotInProgress && !isAutoMeasuring,
|
enabled = !isSpotInProgress && !isAutoMeasuring,
|
||||||
modifier = Modifier.weight(1.3f).height(btnHeight),
|
modifier = Modifier.weight(1f).height(btnHeight),
|
||||||
shape = RoundedCornerShape(14.dp),
|
shape = RoundedCornerShape(14.dp),
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = MlPrimary.copy(alpha = 0.1f),
|
containerColor = MlPrimary.copy(alpha = 0.1f),
|
||||||
@@ -960,8 +962,8 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
Text("Voided", fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = MlPrimary)
|
Text("Void/", fontSize = (13 * fontScale).sp, fontWeight = FontWeight.SemiBold, color = MlPrimary)
|
||||||
Text("Catheterized", fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = MlPrimary)
|
Text("Catheterization", fontSize = (13 * fontScale).sp, fontWeight = FontWeight.SemiBold, color = MlPrimary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -979,9 +981,9 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
if (isAutoMeasuring) {
|
if (isAutoMeasuring) {
|
||||||
Text("Stop Scan", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
Text("Stop Scan", fontSize = (14 * fontScale).sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
} else {
|
} else {
|
||||||
Text("Auto Scan", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
Text("Auto Scan", fontSize = (14 * fontScale).sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1032,7 +1034,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
if (isSpotInProgress) {
|
if (isSpotInProgress) {
|
||||||
CircularProgressIndicator(color = Color.White, modifier = Modifier.size(18.dp), strokeWidth = 2.dp)
|
CircularProgressIndicator(color = Color.White, modifier = Modifier.size(18.dp), strokeWidth = 2.dp)
|
||||||
} else {
|
} else {
|
||||||
Text("Single Scan", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
Text("Single Scan", fontSize = (14 * fontScale).sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1788,19 +1790,20 @@ private fun InfoCard(title: String, value: String, unit: String, color: Color, m
|
|||||||
shape = RoundedCornerShape(14.dp),
|
shape = RoundedCornerShape(14.dp),
|
||||||
colors = CardDefaults.cardColors(containerColor = MlCardBackground)
|
colors = CardDefaults.cardColors(containerColor = MlCardBackground)
|
||||||
) {
|
) {
|
||||||
|
val fs = (LocalConfiguration.current.screenWidthDp / 360f).coerceIn(1.0f, 1.8f)
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(vertical = 14.dp).fillMaxWidth(),
|
modifier = Modifier.padding(vertical = 14.dp).fillMaxWidth(),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
Row(verticalAlignment = Alignment.Bottom) {
|
Row(verticalAlignment = Alignment.Bottom) {
|
||||||
Text(value, fontSize = 20.sp, fontWeight = FontWeight.Bold, color = Color(0xFF1C1B1F))
|
Text(value, fontSize = (20 * fs).sp, fontWeight = FontWeight.Bold, color = Color(0xFF1C1B1F))
|
||||||
if (unit.isNotEmpty()) {
|
if (unit.isNotEmpty()) {
|
||||||
Spacer(modifier = Modifier.width(3.dp))
|
Spacer(modifier = Modifier.width(3.dp))
|
||||||
Text(unit, fontSize = 12.sp, color = MlSecondaryText, modifier = Modifier.padding(bottom = 2.dp))
|
Text(unit, fontSize = (12 * fs).sp, color = MlSecondaryText, modifier = Modifier.padding(bottom = 2.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Text(title, fontSize = 11.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText)
|
Text(title, fontSize = (11 * fs).sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-12
@@ -235,14 +235,14 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = displayHint,
|
text = displayHint,
|
||||||
fontSize = 15.sp,
|
fontSize = 28.sp,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
color = hintFg,
|
color = hintFg,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(hintBg, RoundedCornerShape(12.dp))
|
.background(hintBg, RoundedCornerShape(16.dp))
|
||||||
.padding(12.dp)
|
.padding(horizontal = 20.dp, vertical = 18.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── 하복부 Canvas 실루엣 + 센서 + 방향 가이드 ──
|
// ── 하복부 Canvas 실루엣 + 센서 + 방향 가이드 ──
|
||||||
@@ -401,7 +401,7 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
contentDescription = "Sensor",
|
contentDescription = "Sensor",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(width = sensorW, height = sensorH)
|
.size(width = sensorW, height = sensorH)
|
||||||
.align(androidx.compose.ui.BiasAlignment(0f, 0.44f)),
|
.align(androidx.compose.ui.BiasAlignment(0f, 0.55f)),
|
||||||
contentScale = androidx.compose.ui.layout.ContentScale.Fit,
|
contentScale = androidx.compose.ui.layout.ContentScale.Fit,
|
||||||
alpha = 0.95f
|
alpha = 0.95f
|
||||||
)
|
)
|
||||||
@@ -802,11 +802,11 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(52.dp),
|
.height(96.dp),
|
||||||
shape = RoundedCornerShape(14.dp),
|
shape = RoundedCornerShape(18.dp),
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFFF9800))
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFFF9800))
|
||||||
) {
|
) {
|
||||||
Text("Start Alignment", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = Color.White)
|
Text("Start Alignment", fontSize = 28.sp, fontWeight = FontWeight.Bold, color = Color.White)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -819,16 +819,16 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
enabled = isLocked, // GREEN(Optimal)일 때만 활성
|
enabled = isLocked, // GREEN(Optimal)일 때만 활성
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(52.dp),
|
.height(96.dp),
|
||||||
shape = RoundedCornerShape(14.dp),
|
shape = RoundedCornerShape(18.dp),
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = if (isLocked) Color(0xFF4CAF50) else MlPrimary,
|
containerColor = if (isLocked) Color(0xFF4CAF50) else MlPrimary,
|
||||||
disabledContainerColor = Color.Gray.copy(alpha = 0.3f)
|
disabledContainerColor = Color.Gray.copy(alpha = 0.3f)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Filled.CheckCircle, contentDescription = null, tint = Color.White)
|
Icon(Icons.Filled.CheckCircle, contentDescription = null, tint = Color.White, modifier = Modifier.size(32.dp))
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(10.dp))
|
||||||
Text("Start Scanning", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = Color.White)
|
Text("Start Scanning", fontSize = 28.sp, fontWeight = FontWeight.Bold, color = Color.White)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip button
|
// Skip button
|
||||||
|
|||||||
Reference in New Issue
Block a user