feat(locale): Phase 3 — 전수 sweep, 노인 사용자 기준 영어 잔여 텍스트 한글화
이전 Phase 2 에서 'already English-only' 라며 skip 했던 화면들 — 노인 사용자에게는 영어로 보이면 안 됨. 전체 다시 훑어 모든 user-visible 영어 hardcoded text 를 strings.xml 로 추출. 11 파일 수정: - MainActivity (Exit dialog) - HomeView (Smart Bladder Monitoring / Start / Connected / Clinical Mode / dev toast) - OnboardingView (Welcome, Get Started 등) - CalibrationView (모든 status + Retry/Skip/Continue) - SensorSelectView (title, descriptions) - ShuffledKeypad (Delete contentDescription) - MeasurementHistoryView (title, tabs, empty state, Today/Yesterday) - VivamyoMonitoringView (전체 status) - PiezoMonitoringView (사용자 영역 — Auto/Single Scan, Voiding/Catheterization, 측정 카드, dialog 등) - PlacementGuideView (V1 hints, Step indicator, Start/Skip/Complete Alignment, In position/Position lost) 추출 strings: ~170 (각 언어). strings.xml 194 → 365 라인. skip 유지: - ClinicalHomeView / ClinicalLiveView (R&D dev mode) - PiezoMonitoring dev panel sliders (3-tap 활성화 dev 도구) - PlacementGuide dev settings overlay + Echo Waveforms (isDevMode gated) - BLE Debug Log / Scan Log dialog (dev 전용) - 브랜드명 (VesiScan-Basic, Vivamyo, Bladdy) - DEMO 배지, IMU debug 문자열, Log.d / debugLogger 출력 Korean coverage: ~95% (일반 사용자가 절대 보지 않는 dev surface 만 영어 유지). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -18,9 +18,11 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.rive.runtime.kotlin.core.Rive
|
||||
import com.example.medilightv2android.services.KeychainService
|
||||
import com.example.medilightv2android.services.NotificationService
|
||||
import com.example.medilightv2android.R
|
||||
import com.example.medilightv2android.ui.theme.Medilightv2androidTheme
|
||||
import com.example.medilightv2android.ui.views.calibration.CalibrationView
|
||||
import com.example.medilightv2android.ui.views.connection.DeviceScanView
|
||||
@@ -81,16 +83,16 @@ fun MedilightApp(modifier: Modifier = Modifier) {
|
||||
if (showExitDialog) {
|
||||
androidx.compose.material3.AlertDialog(
|
||||
onDismissRequest = { showExitDialog = false },
|
||||
title = { androidx.compose.material3.Text("Exit App") },
|
||||
text = { androidx.compose.material3.Text("Are you sure you want to exit?") },
|
||||
title = { androidx.compose.material3.Text(stringResource(R.string.exit_app_title)) },
|
||||
text = { androidx.compose.material3.Text(stringResource(R.string.exit_app_message)) },
|
||||
confirmButton = {
|
||||
androidx.compose.material3.TextButton(onClick = { (context as? android.app.Activity)?.finish() }) {
|
||||
androidx.compose.material3.Text("Exit", color = Color.Red)
|
||||
androidx.compose.material3.Text(stringResource(R.string.exit_app_confirm), color = Color.Red)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
androidx.compose.material3.TextButton(onClick = { showExitDialog = false }) {
|
||||
androidx.compose.material3.Text("Cancel")
|
||||
androidx.compose.material3.Text(stringResource(R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
+22
-13
@@ -17,11 +17,13 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.medilightv2android.AppState
|
||||
import com.example.medilightv2android.R
|
||||
import com.example.medilightv2android.ble.BleManager
|
||||
import com.example.medilightv2android.managers.NirsManager
|
||||
import com.example.medilightv2android.models.NirsMagResponse
|
||||
@@ -35,7 +37,14 @@ fun CalibrationView(appState: AppState) {
|
||||
val nirsManager = remember { NirsManager.instance }
|
||||
val handler = remember { Handler(Looper.getMainLooper()) }
|
||||
|
||||
var statusMessage by remember { mutableStateOf("Initializing...") }
|
||||
val initMsg = stringResource(R.string.calibration_initializing)
|
||||
val completeMsg = stringResource(R.string.calibration_complete_status)
|
||||
val noResponseMsg = stringResource(R.string.calibration_no_response)
|
||||
val poweringMsg = stringResource(R.string.calibration_powering_on)
|
||||
val activatingMsg = stringResource(R.string.calibration_activating)
|
||||
val readingGainMsg = stringResource(R.string.calibration_reading_gain)
|
||||
|
||||
var statusMessage by remember { mutableStateOf(initMsg) }
|
||||
var isComplete by remember { mutableStateOf(false) }
|
||||
var isFailed by remember { mutableStateOf(false) }
|
||||
var didReceiveResponse by remember { mutableStateOf(false) }
|
||||
@@ -44,27 +53,27 @@ fun CalibrationView(appState: AppState) {
|
||||
|
||||
fun completeCalibration() {
|
||||
if (isComplete) return
|
||||
statusMessage = "NIRS Calibrated"
|
||||
statusMessage = completeMsg
|
||||
isComplete = true
|
||||
}
|
||||
|
||||
fun failCalibration() {
|
||||
if (isComplete || isFailed) return
|
||||
statusMessage = "No response from device. Check the device is powered on and nearby."
|
||||
statusMessage = noResponseMsg
|
||||
isFailed = true
|
||||
}
|
||||
|
||||
fun startCalibration() {
|
||||
statusMessage = "Powering on NIRS transducer..."
|
||||
statusMessage = poweringMsg
|
||||
|
||||
bleManager.onNirsPowerOnReceived = {
|
||||
didReceiveResponse = true
|
||||
statusMessage = "Activating sensor..."
|
||||
statusMessage = activatingMsg
|
||||
handler.postDelayed({ bleManager.sendMqqQuery() }, 2000)
|
||||
}
|
||||
|
||||
bleManager.onNirsSensorActivated = {
|
||||
statusMessage = "Reading gain calibration..."
|
||||
statusMessage = readingGainMsg
|
||||
handler.postDelayed({ bleManager.sendMagQuery() }, 300)
|
||||
}
|
||||
|
||||
@@ -87,7 +96,7 @@ fun CalibrationView(appState: AppState) {
|
||||
isFailed = false
|
||||
isComplete = false
|
||||
didReceiveResponse = false
|
||||
statusMessage = "Initializing..."
|
||||
statusMessage = initMsg
|
||||
startCalibration()
|
||||
}
|
||||
|
||||
@@ -124,9 +133,9 @@ fun CalibrationView(appState: AppState) {
|
||||
|
||||
Text(
|
||||
text = when {
|
||||
isFailed -> "Calibration Failed"
|
||||
isComplete -> "Calibration Complete"
|
||||
else -> "Calibrating NIRS..."
|
||||
isFailed -> stringResource(R.string.calibration_failed)
|
||||
isComplete -> stringResource(R.string.calibration_complete)
|
||||
else -> stringResource(R.string.calibration_in_progress)
|
||||
},
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
@@ -167,7 +176,7 @@ fun CalibrationView(appState: AppState) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(Icons.Default.Refresh, null, tint = Color.White)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Retry", fontSize = 18.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||
Text(stringResource(R.string.retry), fontSize = 18.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -176,7 +185,7 @@ fun CalibrationView(appState: AppState) {
|
||||
onClick = { appState.calibrationComplete() },
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
) {
|
||||
Text("Skip", fontSize = 16.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText)
|
||||
Text(stringResource(R.string.calibration_skip), fontSize = 16.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText)
|
||||
}
|
||||
}
|
||||
} else if (isComplete) {
|
||||
@@ -197,7 +206,7 @@ fun CalibrationView(appState: AppState) {
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("Continue", fontSize = 18.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||
Text(stringResource(R.string.calibration_continue), fontSize = 18.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+30
-25
@@ -27,10 +27,12 @@ import androidx.compose.ui.graphics.nativeCanvas
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.medilightv2android.AppState
|
||||
import com.example.medilightv2android.R
|
||||
import com.example.medilightv2android.models.*
|
||||
import com.example.medilightv2android.services.MeasurementHistoryStore
|
||||
import com.example.medilightv2android.ui.theme.*
|
||||
@@ -54,17 +56,17 @@ fun MeasurementHistoryView(appState: AppState) {
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
IconButton(onClick = { appState.backToMonitoring() }) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back", tint = MlPrimary)
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.back), tint = MlPrimary)
|
||||
}
|
||||
Text("History", fontSize = 20.sp, fontWeight = FontWeight.Bold, modifier = Modifier.padding(start = 8.dp))
|
||||
Text(stringResource(R.string.history_title), fontSize = 20.sp, fontWeight = FontWeight.Bold, modifier = Modifier.padding(start = 8.dp))
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text("${records.size} records", fontSize = 13.sp, color = MlSecondaryText)
|
||||
Text(stringResource(R.string.history_records_count, records.size), fontSize = 13.sp, color = MlSecondaryText)
|
||||
}
|
||||
|
||||
// Tab picker
|
||||
TabRow(selectedTabIndex = selectedTab, modifier = Modifier.padding(horizontal = 24.dp)) {
|
||||
Tab(selected = selectedTab == 0, onClick = { selectedTab = 0 }) { Text("History", modifier = Modifier.padding(12.dp)) }
|
||||
Tab(selected = selectedTab == 1, onClick = { selectedTab = 1 }) { Text("Trends", modifier = Modifier.padding(12.dp)) }
|
||||
Tab(selected = selectedTab == 0, onClick = { selectedTab = 0 }) { Text(stringResource(R.string.history_tab_history), modifier = Modifier.padding(12.dp)) }
|
||||
Tab(selected = selectedTab == 1, onClick = { selectedTab = 1 }) { Text(stringResource(R.string.history_tab_trends), modifier = Modifier.padding(12.dp)) }
|
||||
}
|
||||
|
||||
if (records.isEmpty()) {
|
||||
@@ -73,9 +75,9 @@ fun MeasurementHistoryView(appState: AppState) {
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text("No measurements yet", fontSize = 16.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText)
|
||||
Text(stringResource(R.string.history_no_measurements), fontSize = 16.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text("Tap Spot to start measuring", fontSize = 14.sp, color = MlSecondaryText.copy(alpha = 0.7f))
|
||||
Text(stringResource(R.string.history_tap_spot_hint), fontSize = 14.sp, color = MlSecondaryText.copy(alpha = 0.7f))
|
||||
}
|
||||
} else if (selectedTab == 0) {
|
||||
LazyColumn(
|
||||
@@ -98,7 +100,7 @@ fun MeasurementHistoryView(appState: AppState) {
|
||||
// Level chart
|
||||
item {
|
||||
InteractiveChartCard(
|
||||
title = "Bladder Level",
|
||||
title = stringResource(R.string.history_chart_bladder_level),
|
||||
measurements = measurements,
|
||||
valueExtractor = { it.level.toFloat() },
|
||||
maxValue = BladderLevel.MAX_LEVEL.toFloat(),
|
||||
@@ -113,7 +115,7 @@ fun MeasurementHistoryView(appState: AppState) {
|
||||
if (measurements.isNotEmpty()) {
|
||||
item {
|
||||
InteractiveChartCard(
|
||||
title = "Estimated Volume",
|
||||
title = stringResource(R.string.history_chart_estimated_volume),
|
||||
measurements = measurements,
|
||||
valueExtractor = { it.estimatedVolume.toFloat() },
|
||||
maxValue = measurements.maxOf { it.estimatedVolume }.toFloat().coerceAtLeast(1f),
|
||||
@@ -133,13 +135,14 @@ fun MeasurementHistoryView(appState: AppState) {
|
||||
modifier = Modifier.shadow(4.dp, RoundedCornerShape(16.dp))
|
||||
) {
|
||||
Column(modifier = Modifier.padding(20.dp)) {
|
||||
Text("Catheterization Log", fontSize = 14.sp, fontWeight = FontWeight.SemiBold)
|
||||
Text(stringResource(R.string.history_catheterization_log), fontSize = 14.sp, fontWeight = FontWeight.SemiBold)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
cathEvents.forEach { event ->
|
||||
val yesterdayLabel = stringResource(R.string.history_yesterday)
|
||||
Row(modifier = Modifier.padding(vertical = 4.dp)) {
|
||||
Text(formatDate(event.timestamp), fontSize = 13.sp)
|
||||
Text(formatDate(event.timestamp, yesterdayLabel), fontSize = 13.sp)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text("at Lv.${event.level}", fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText)
|
||||
Text(stringResource(R.string.history_at_level, event.level), fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -191,6 +194,8 @@ private fun InteractiveChartCard(
|
||||
if (selectedIndex in measurements.indices) {
|
||||
val record = measurements[selectedIndex]
|
||||
val value = valueExtractor(record)
|
||||
val todayLabel = stringResource(R.string.history_today)
|
||||
val yesterdayLabel = stringResource(R.string.history_yesterday)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -200,7 +205,7 @@ private fun InteractiveChartCard(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
formatDateTime(record.timestamp),
|
||||
formatDateTime(record.timestamp, todayLabel, yesterdayLabel),
|
||||
fontSize = 11.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MlPrimary
|
||||
@@ -224,7 +229,7 @@ private fun InteractiveChartCard(
|
||||
}
|
||||
|
||||
if (measurements.isEmpty()) {
|
||||
Text("No measurement data", fontSize = 12.sp, color = MlSecondaryText)
|
||||
Text(stringResource(R.string.history_no_data), fontSize = 12.sp, color = MlSecondaryText)
|
||||
} else {
|
||||
val density = LocalDensity.current
|
||||
val textPaint = remember {
|
||||
@@ -346,7 +351,7 @@ private fun InteractiveChartCard(
|
||||
// Hint text
|
||||
if (measurements.size > 3 && scale < 1.05f && selectedIndex < 0) {
|
||||
Text(
|
||||
"Pinch to zoom \u2022 Tap point for details",
|
||||
stringResource(R.string.history_pinch_hint),
|
||||
fontSize = 10.sp,
|
||||
color = MlSecondaryText.copy(alpha = 0.5f),
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
@@ -390,10 +395,10 @@ private fun RecordCard(record: MeasurementRecord) {
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
if (isCath) "Catheterization" else "Measurement",
|
||||
if (isCath) stringResource(R.string.history_catheterization) else stringResource(R.string.history_measurement),
|
||||
fontSize = 14.sp, fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Text(formatDate(record.timestamp), fontSize = 12.sp, color = MlSecondaryText)
|
||||
Text(formatDate(record.timestamp, stringResource(R.string.history_yesterday)), fontSize = 12.sp, color = MlSecondaryText)
|
||||
}
|
||||
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
@@ -408,7 +413,7 @@ private fun RecordCard(record: MeasurementRecord) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatDate(timestamp: Long): String {
|
||||
private fun formatDate(timestamp: Long, yesterdayLabel: String = "Yesterday"): String {
|
||||
val cal = Calendar.getInstance()
|
||||
val now = Calendar.getInstance()
|
||||
cal.timeInMillis = timestamp
|
||||
@@ -416,18 +421,18 @@ private fun formatDate(timestamp: Long): String {
|
||||
return when {
|
||||
cal.get(Calendar.DAY_OF_YEAR) == now.get(Calendar.DAY_OF_YEAR) &&
|
||||
cal.get(Calendar.YEAR) == now.get(Calendar.YEAR) -> {
|
||||
SimpleDateFormat("h:mm a", Locale.US).format(Date(timestamp))
|
||||
SimpleDateFormat("h:mm a", Locale.getDefault()).format(Date(timestamp))
|
||||
}
|
||||
now.timeInMillis - timestamp < 2 * 24 * 60 * 60 * 1000 -> {
|
||||
"Yesterday " + SimpleDateFormat("h:mm a", Locale.US).format(Date(timestamp))
|
||||
"$yesterdayLabel " + SimpleDateFormat("h:mm a", Locale.getDefault()).format(Date(timestamp))
|
||||
}
|
||||
else -> {
|
||||
SimpleDateFormat("MMM d, h:mm a", Locale.US).format(Date(timestamp))
|
||||
SimpleDateFormat("MMM d, h:mm a", Locale.getDefault()).format(Date(timestamp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatDateTime(timestamp: Long): String {
|
||||
private fun formatDateTime(timestamp: Long, todayLabel: String = "Today", yesterdayLabel: String = "Yesterday"): String {
|
||||
val cal = Calendar.getInstance()
|
||||
val now = Calendar.getInstance()
|
||||
cal.timeInMillis = timestamp
|
||||
@@ -435,13 +440,13 @@ private fun formatDateTime(timestamp: Long): String {
|
||||
return when {
|
||||
cal.get(Calendar.DAY_OF_YEAR) == now.get(Calendar.DAY_OF_YEAR) &&
|
||||
cal.get(Calendar.YEAR) == now.get(Calendar.YEAR) -> {
|
||||
"Today " + SimpleDateFormat("h:mm:ss a", Locale.US).format(Date(timestamp))
|
||||
"$todayLabel " + SimpleDateFormat("h:mm:ss a", Locale.getDefault()).format(Date(timestamp))
|
||||
}
|
||||
now.timeInMillis - timestamp < 2 * 24 * 60 * 60 * 1000 -> {
|
||||
"Yesterday " + SimpleDateFormat("h:mm:ss a", Locale.US).format(Date(timestamp))
|
||||
"$yesterdayLabel " + SimpleDateFormat("h:mm:ss a", Locale.getDefault()).format(Date(timestamp))
|
||||
}
|
||||
else -> {
|
||||
SimpleDateFormat("MMM d, h:mm:ss a", Locale.US).format(Date(timestamp))
|
||||
SimpleDateFormat("MMM d, h:mm:ss a", Locale.getDefault()).format(Date(timestamp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ fun HomeView(appState: AppState) {
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(8.dp)
|
||||
) {
|
||||
Icon(Icons.Default.Language, contentDescription = "Language",
|
||||
Icon(Icons.Default.Language, contentDescription = stringResource(R.string.home_language_icon_desc),
|
||||
tint = MlSecondaryText.copy(alpha = 0.7f))
|
||||
}
|
||||
|
||||
@@ -82,7 +82,8 @@ fun HomeView(appState: AppState) {
|
||||
if (tapCount >= 3) {
|
||||
tapCount = 0
|
||||
appState.isDevMode = !appState.isDevMode
|
||||
val msg = if (appState.isDevMode) "Developer mode ON" else "Developer mode OFF"
|
||||
val msg = if (appState.isDevMode) context.getString(R.string.home_dev_mode_on)
|
||||
else context.getString(R.string.home_dev_mode_off)
|
||||
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
@@ -95,7 +96,7 @@ fun HomeView(appState: AppState) {
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
Text("Smart Bladder Monitoring", fontSize = 16.sp, color = MlSecondaryText)
|
||||
Text(stringResource(R.string.home_subtitle), fontSize = 16.sp, color = MlSecondaryText)
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
@@ -107,7 +108,7 @@ fun HomeView(appState: AppState) {
|
||||
// BLE 연결 상태 chip — 연결 유지 정책에서 사용자가 인지 가능하도록
|
||||
if (appState.isDeviceConnected) {
|
||||
Text(
|
||||
"● Connected — Start to resume",
|
||||
stringResource(R.string.home_connected_resume),
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color(0xFF4CAF50),
|
||||
@@ -139,7 +140,7 @@ fun HomeView(appState: AppState) {
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("Start", fontSize = 20.sp, fontWeight = FontWeight.Bold, color = Color.White)
|
||||
Text(stringResource(R.string.home_start), fontSize = 20.sp, fontWeight = FontWeight.Bold, color = Color.White)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +156,7 @@ fun HomeView(appState: AppState) {
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF7C3AED))
|
||||
) {
|
||||
Text("Clinical Mode (Dev)", color = Color.White, fontWeight = FontWeight.SemiBold)
|
||||
Text(stringResource(R.string.home_clinical_mode_dev), color = Color.White, fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
} else {
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
|
||||
+56
-53
@@ -108,7 +108,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
.padding(horizontal = 32.dp, vertical = 64.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("Recorded", fontSize = 48.sp, fontWeight = FontWeight.ExtraBold, color = Color.White)
|
||||
Text(stringResource(R.string.piezo_recorded), fontSize = 48.sp, fontWeight = FontWeight.ExtraBold, color = Color.White)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -812,10 +812,10 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
var addAmount by remember { mutableStateOf("1") }
|
||||
AlertDialog(
|
||||
onDismissRequest = { showAddCatheterDialog = false },
|
||||
title = { Text("Add Catheters") },
|
||||
title = { Text(stringResource(R.string.piezo_add_catheters_title)) },
|
||||
text = {
|
||||
Column {
|
||||
Text("Enter the number of catheters to add:")
|
||||
Text(stringResource(R.string.piezo_add_catheters_message))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = addAmount,
|
||||
@@ -829,10 +829,10 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
val amount = addAmount.toIntOrNull() ?: 0
|
||||
if (amount > 0) appState.addCatheterCount(amount)
|
||||
showAddCatheterDialog = false
|
||||
}) { Text("Add", color = MlPrimary) }
|
||||
}) { Text(stringResource(R.string.piezo_add), color = MlPrimary) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showAddCatheterDialog = false }) { Text("Cancel") }
|
||||
TextButton(onClick = { showAddCatheterDialog = false }) { Text(stringResource(R.string.cancel)) }
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -841,17 +841,17 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
if (showCatheterizeConfirm) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showCatheterizeConfirm = false },
|
||||
title = { Text("Reset bladder level?") },
|
||||
text = { Text("This will reset the bladder level to 0. This action cannot be undone.") },
|
||||
title = { Text(stringResource(R.string.piezo_reset_level_title)) },
|
||||
text = { Text(stringResource(R.string.piezo_reset_level_message)) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
appState.resetLevel()
|
||||
resetMeasurement()
|
||||
showCatheterizeConfirm = false
|
||||
}) { Text("Catheterize", color = Color.Red) }
|
||||
}) { Text(stringResource(R.string.piezo_catheterize), color = Color.Red) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showCatheterizeConfirm = false }) { Text("Cancel") }
|
||||
TextButton(onClick = { showCatheterizeConfirm = false }) { Text(stringResource(R.string.cancel)) }
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -946,7 +946,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
) {
|
||||
Icon(Icons.Default.FlashOn, contentDescription = null, tint = Color(0xFF4CAF50))
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text("Quick Save", fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
|
||||
Text(stringResource(R.string.piezo_quick_save), fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
|
||||
Text("$quickVol mL", color = MlSecondaryText, fontSize = 14.sp)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Icon(Icons.Default.ChevronRight, contentDescription = null, tint = MlSecondaryText)
|
||||
@@ -1075,10 +1075,10 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
val iconSize = if (isCompact) 28.dp else 32.dp
|
||||
// 빌드 122: Home 아이콘 추가 (Placement 옆)
|
||||
IconButton(onClick = { appState.goHome() }, modifier = Modifier.size(btnSize)) {
|
||||
Icon(Icons.Default.Home, "Home", tint = MlPrimary, modifier = Modifier.size(iconSize))
|
||||
Icon(Icons.Default.Home, stringResource(R.string.piezo_home_desc), 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))
|
||||
Icon(Icons.Default.MyLocation, stringResource(R.string.piezo_placement_desc), tint = MlPrimary, modifier = Modifier.size(iconSize))
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
@@ -1088,13 +1088,13 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
Icons.Default.BluetoothDisabled,
|
||||
contentDescription = "Disconnected",
|
||||
contentDescription = stringResource(R.string.piezo_disconnect_desc),
|
||||
tint = Color.Red,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
"Disconnect",
|
||||
stringResource(R.string.piezo_disconnect),
|
||||
fontSize = (12 * fontScale).sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.Red
|
||||
@@ -1175,11 +1175,11 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Default.LocalHospital, "Catheter", tint = MlPrimary, modifier = Modifier.size(16.dp))
|
||||
Icon(Icons.Default.LocalHospital, stringResource(R.string.piezo_catheter_desc), tint = MlPrimary, modifier = Modifier.size(16.dp))
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text("$cathCount", fontSize = (13 * fontScale).sp, fontWeight = FontWeight.Bold, color = MlPrimary)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text("left", fontSize = (10 * fontScale).sp, fontWeight = FontWeight.SemiBold,
|
||||
Text(stringResource(R.string.piezo_catheter_left), fontSize = (10 * fontScale).sp, fontWeight = FontWeight.SemiBold,
|
||||
color = MlPrimary.copy(alpha = 0.7f))
|
||||
}
|
||||
}
|
||||
@@ -1187,12 +1187,12 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
var addCount by remember { mutableStateOf("1") }
|
||||
AlertDialog(
|
||||
onDismissRequest = { showAddCatheter = false },
|
||||
title = { Text("Add Catheters") },
|
||||
title = { Text(stringResource(R.string.piezo_add_catheters_title)) },
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = addCount,
|
||||
onValueChange = { addCount = it.filter { c -> c.isDigit() } },
|
||||
label = { Text("Count") },
|
||||
label = { Text(stringResource(R.string.piezo_add_catheters_count_label)) },
|
||||
singleLine = true
|
||||
)
|
||||
},
|
||||
@@ -1201,10 +1201,10 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
val n = addCount.toIntOrNull() ?: 0
|
||||
if (n > 0) appState.addCatheterCount(n)
|
||||
showAddCatheter = false
|
||||
}) { Text("Add") }
|
||||
}) { Text(stringResource(R.string.piezo_add)) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showAddCatheter = false }) { Text("Cancel") }
|
||||
TextButton(onClick = { showAddCatheter = false }) { Text(stringResource(R.string.cancel)) }
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1213,7 +1213,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
|
||||
// Settings button
|
||||
IconButton(onClick = { showSettings = !showSettings }, modifier = Modifier.size(btnSize)) {
|
||||
Icon(Icons.Default.Settings, "Settings", tint = if (showSettings) MlPrimary else MlSecondaryText, modifier = Modifier.size(iconSize))
|
||||
Icon(Icons.Default.Settings, stringResource(R.string.piezo_settings_desc), tint = if (showSettings) MlPrimary else MlSecondaryText, modifier = Modifier.size(iconSize))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1282,10 +1282,10 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
com.example.medilightv2android.managers.Posture.UNKNOWN -> Color(0xFF616161) // 진한 회색 — 가시성 확보
|
||||
}
|
||||
val label = when (posture) {
|
||||
com.example.medilightv2android.managers.Posture.LYING -> "Lying"
|
||||
com.example.medilightv2android.managers.Posture.NON_LYING -> "Upright"
|
||||
com.example.medilightv2android.managers.Posture.WALKING -> "Walking"
|
||||
com.example.medilightv2android.managers.Posture.UNKNOWN -> "Detecting"
|
||||
com.example.medilightv2android.managers.Posture.LYING -> stringResource(R.string.piezo_posture_lying)
|
||||
com.example.medilightv2android.managers.Posture.NON_LYING -> stringResource(R.string.piezo_posture_upright)
|
||||
com.example.medilightv2android.managers.Posture.WALKING -> stringResource(R.string.piezo_posture_walking)
|
||||
com.example.medilightv2android.managers.Posture.UNKNOWN -> stringResource(R.string.piezo_posture_detecting)
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -1371,7 +1371,8 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
if (devTapCount >= 3) {
|
||||
devTapCount = 0
|
||||
appState.isDevMode = !appState.isDevMode
|
||||
val msg = if (appState.isDevMode) "Developer mode ON" else "Developer mode OFF"
|
||||
val msg = if (appState.isDevMode) context.getString(R.string.home_dev_mode_on)
|
||||
else context.getString(R.string.home_dev_mode_off)
|
||||
android.widget.Toast.makeText(context, msg, android.widget.Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
@@ -1401,7 +1402,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
// 좌측: Current Measurement (회의 #7)
|
||||
val showMeasurement = if (isAutoMeasuring) autoVolumeWindow.size >= 5 && lastVolumeMl > 0 else lastVolumeMl > 0
|
||||
val currentMeasurement = if (showMeasurement) "%.0f".format(lastVolumeMl) else "—"
|
||||
InfoCard("Current Measurement", currentMeasurement, "mL", urgency.accentColor, Modifier.weight(1f))
|
||||
InfoCard(stringResource(R.string.piezo_current_measurement), currentMeasurement, "mL", urgency.accentColor, Modifier.weight(1f))
|
||||
// 우측: 측정 max / max capacity 비율 (회의 #6) — 위에 XX%, 아래 raw 분수
|
||||
val maxMeasured = if (displayMaxVolumeMl > 0) "%.0f".format(displayMaxVolumeMl) else "—"
|
||||
val capacityVal = appState.piezoSettings.maxVolume
|
||||
@@ -1447,7 +1448,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
disabledContainerColor = Color.Gray.copy(alpha = 0.3f)
|
||||
)
|
||||
) {
|
||||
Text("Voiding / Catheterization", fontSize = (12 * fontScale).sp,
|
||||
Text(stringResource(R.string.piezo_voiding_catheterization), fontSize = (12 * fontScale).sp,
|
||||
fontWeight = FontWeight.SemiBold, color = Color.White,
|
||||
textAlign = TextAlign.Center, maxLines = 2)
|
||||
}
|
||||
@@ -1466,9 +1467,9 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
)
|
||||
) {
|
||||
if (isAutoMeasuring) {
|
||||
Text("Stop Scan", fontSize = (14 * fontScale).sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||
Text(stringResource(R.string.piezo_stop_scan), fontSize = (14 * fontScale).sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||
} else {
|
||||
Text("Auto Scan", fontSize = (14 * fontScale).sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||
Text(stringResource(R.string.piezo_auto_scan), fontSize = (14 * fontScale).sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1532,7 +1533,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
if (isSpotInProgress) {
|
||||
CircularProgressIndicator(color = Color.White, modifier = Modifier.size(18.dp), strokeWidth = 2.dp)
|
||||
} else {
|
||||
Text("Single Scan", fontSize = (14 * fontScale).sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||
Text(stringResource(R.string.piezo_single_scan), fontSize = (14 * fontScale).sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1550,7 +1551,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
) {
|
||||
Icon(Icons.Default.Build, "Logs", tint = Color.White, modifier = Modifier.size(14.dp))
|
||||
Text(
|
||||
if (appState.isDevMode) "Developer Tools" else "Logs & Charts",
|
||||
if (appState.isDevMode) stringResource(R.string.piezo_developer_tools) else stringResource(R.string.piezo_logs_charts),
|
||||
fontSize = 12.sp, fontWeight = FontWeight.SemiBold, color = Color.White
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
@@ -1648,7 +1649,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
) {
|
||||
// Max Volume
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Bladder Capacity", fontSize = labelSize, fontWeight = FontWeight.SemiBold)
|
||||
Text(stringResource(R.string.piezo_settings_bladder_capacity), fontSize = labelSize, fontWeight = FontWeight.SemiBold)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text("${appState.piezoSettings.maxVolume} mL", fontSize = labelSize, fontWeight = FontWeight.Bold, color = MlPrimary)
|
||||
}
|
||||
@@ -1693,9 +1694,9 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
|
||||
// Catheterization Threshold
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Catheter Threshold", fontSize = labelSize, fontWeight = FontWeight.SemiBold)
|
||||
Text(stringResource(R.string.piezo_settings_catheter_threshold), fontSize = labelSize, fontWeight = FontWeight.SemiBold)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text("Level ${appState.bladderLevel.catheterizationThreshold}", fontSize = labelSize, fontWeight = FontWeight.Bold, color = MlPrimary)
|
||||
Text(stringResource(R.string.piezo_settings_level_format, appState.bladderLevel.catheterizationThreshold), fontSize = labelSize, fontWeight = FontWeight.Bold, color = MlPrimary)
|
||||
}
|
||||
LockableSlider(
|
||||
value = appState.bladderLevel.catheterizationThreshold.toFloat(),
|
||||
@@ -1715,14 +1716,16 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
val displayValue = if (unitMode == "sec") currentMs / 1000.0 else currentMs / 60_000.0
|
||||
val valueClamped = displayValue.coerceIn(0.5, 10.0)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Auto Scan Interval", fontSize = labelSize, fontWeight = FontWeight.SemiBold)
|
||||
Text(stringResource(R.string.piezo_settings_auto_scan_interval), fontSize = labelSize, fontWeight = FontWeight.SemiBold)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text("%.1f".format(valueClamped),
|
||||
fontSize = labelSize, fontWeight = FontWeight.Bold, color = MlPrimary)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
// sec/min 토글 (우측)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
listOf("sec", "min").forEach { mode ->
|
||||
val secLabel = stringResource(R.string.piezo_settings_unit_sec)
|
||||
val minLabel = stringResource(R.string.piezo_settings_unit_min)
|
||||
listOf("sec" to secLabel, "min" to minLabel).forEach { (mode, label) ->
|
||||
val sel = unitMode == mode
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@@ -1738,7 +1741,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
}
|
||||
.padding(horizontal = 10.dp, vertical = 2.dp)
|
||||
) {
|
||||
Text(mode, fontSize = 11.sp, fontWeight = FontWeight.Bold,
|
||||
Text(label, fontSize = 11.sp, fontWeight = FontWeight.Bold,
|
||||
color = if (sel) Color.White else Color.Black)
|
||||
}
|
||||
}
|
||||
@@ -2174,7 +2177,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
) {
|
||||
Icon(Icons.Default.LinkOff, null, tint = Color(0xFF333333), modifier = Modifier.size(16.dp))
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text("Disconnect", fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = Color(0xFF333333))
|
||||
Text(stringResource(R.string.piezo_disconnect), fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = Color(0xFF333333))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
@@ -2189,13 +2192,13 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
) {
|
||||
Icon(Icons.Default.LinkOff, null, tint = Color.Red, modifier = Modifier.size(16.dp))
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text("Delete Pairing", fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = Color.Red)
|
||||
Text(stringResource(R.string.piezo_delete_pairing_title), fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = Color.Red)
|
||||
}
|
||||
if (showDisconnectConfirm) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showDisconnectConfirm = false },
|
||||
title = { Text("Delete Pairing") },
|
||||
text = { Text("Disconnect and remove pairing.") },
|
||||
title = { Text(stringResource(R.string.piezo_delete_pairing_title)) },
|
||||
text = { Text(stringResource(R.string.piezo_delete_pairing_message)) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
showDisconnectConfirm = false; showSettings = false
|
||||
@@ -2204,9 +2207,9 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
// 명시적 Unpair는 끊은 게 맞으므로 여기서 직접 false 처리.
|
||||
appState.isDeviceConnected = false
|
||||
appState.goHome()
|
||||
}) { Text("Delete", color = Color.Red) }
|
||||
}) { Text(stringResource(R.string.delete), color = Color.Red) }
|
||||
},
|
||||
dismissButton = { TextButton(onClick = { showDisconnectConfirm = false }) { Text("Cancel") } }
|
||||
dismissButton = { TextButton(onClick = { showDisconnectConfirm = false }) { Text(stringResource(R.string.cancel)) } }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2219,7 +2222,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text("App", fontSize = 13.sp, color = MlSecondaryText)
|
||||
Text(stringResource(R.string.piezo_settings_app), fontSize = 13.sp, color = MlSecondaryText)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
com.example.medilightv2android.BuildConfig.VERSION_NAME,
|
||||
@@ -2235,7 +2238,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text("Device", fontSize = 13.sp, color = MlSecondaryText)
|
||||
Text(stringResource(R.string.piezo_settings_device), fontSize = 13.sp, color = MlSecondaryText)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(devName,
|
||||
fontSize = 13.sp,
|
||||
@@ -2250,7 +2253,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text("Firmware", fontSize = 13.sp, color = MlSecondaryText)
|
||||
Text(stringResource(R.string.piezo_settings_firmware), fontSize = 13.sp, color = MlSecondaryText)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
if (fwVersion.isNotEmpty()) fwVersion else "—",
|
||||
@@ -2290,7 +2293,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
TextButton(onClick = { showSettings = false }) {
|
||||
Text("Cancel", color = MlSecondaryText)
|
||||
Text(stringResource(R.string.cancel), color = MlSecondaryText)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Button(
|
||||
@@ -2301,7 +2304,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
colors = ButtonDefaults.buttonColors(containerColor = MlPrimary)
|
||||
) {
|
||||
Text("Save", color = Color.White, fontWeight = FontWeight.Bold)
|
||||
Text(stringResource(R.string.save), color = Color.White, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2313,18 +2316,18 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
if (showRealignPrompt) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showRealignPrompt = false },
|
||||
title = { Text("Position Lost or Insufficient Gel") },
|
||||
text = { Text("Position lost or gel may be insufficient. Please check.") },
|
||||
title = { Text(stringResource(R.string.piezo_realign_title)) },
|
||||
text = { Text(stringResource(R.string.piezo_realign_message)) },
|
||||
confirmButton = {
|
||||
Button(onClick = {
|
||||
showRealignPrompt = false
|
||||
autoFailCount = 0
|
||||
appState.currentScreen = com.example.medilightv2android.AppScreen.PLACEMENT_GUIDE
|
||||
}) { Text("Realign") }
|
||||
}) { Text(stringResource(R.string.piezo_realign_button)) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showRealignPrompt = false; autoFailCount = 0 }) {
|
||||
Text("Dismiss")
|
||||
Text(stringResource(R.string.piezo_dismiss))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
+89
-72
@@ -83,8 +83,8 @@ fun PlacementGuideView(appState: AppState) {
|
||||
val isSweepMode = com.example.medilightv2android.managers.GreenZoneConstants.placementGuideMode ==
|
||||
com.example.medilightv2android.managers.PlacementGuideMode.SWEEP
|
||||
var directionHint by remember { mutableStateOf(
|
||||
if (isSweepMode) "Place sensor near navel, slide down ↓"
|
||||
else "Place the sensor just above the pubic bone"
|
||||
if (isSweepMode) context.getString(R.string.placement_sweep_initial)
|
||||
else context.getString(R.string.placement_initial_hint)
|
||||
) }
|
||||
var directionIcon by remember { mutableStateOf("") }
|
||||
var placementScore by remember { mutableIntStateOf(0) }
|
||||
@@ -148,7 +148,7 @@ fun PlacementGuideView(appState: AppState) {
|
||||
lastHintChangeMs = 0L
|
||||
lastGuidePhase = PlacementPhase.VERTICAL
|
||||
phaseHoldScans = 0
|
||||
directionHint = "Place VesiScan above the pubic bone, then press Start"
|
||||
directionHint = context.getString(R.string.placement_press_start)
|
||||
directionIcon = ""
|
||||
lastCv = Double.NaN
|
||||
for (i in 0 until 6) { channelDetected[i] = false; channelUrineLen[i] = 0 }
|
||||
@@ -265,24 +265,24 @@ fun PlacementGuideView(appState: AppState) {
|
||||
}) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
contentDescription = stringResource(R.string.placement_back_desc),
|
||||
tint = MlPrimary
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
if (isLocked) {
|
||||
Icon(Icons.Filled.CheckCircle, "Ready", tint = Color(0xFF4CAF50), modifier = Modifier.size(24.dp))
|
||||
Icon(Icons.Filled.CheckCircle, stringResource(R.string.placement_ready_desc), tint = Color(0xFF4CAF50), modifier = Modifier.size(24.dp))
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
}
|
||||
if (appState.isDevMode) {
|
||||
IconButton(onClick = { showPlacementSettings = !showPlacementSettings }) {
|
||||
Icon(Icons.Default.Settings, "Settings", tint = if (showPlacementSettings) MlPrimary else MlSecondaryText)
|
||||
Icon(Icons.Default.Settings, stringResource(R.string.piezo_settings_desc), tint = if (showPlacementSettings) MlPrimary else MlSecondaryText)
|
||||
}
|
||||
}
|
||||
}
|
||||
// (B) 타이틀 — 태블릿 기준 화면폭 ≈ 60% 차지 (32 → 56sp).
|
||||
Text(
|
||||
text = "Sensor Alignment",
|
||||
text = stringResource(R.string.placement_title),
|
||||
fontSize = 56.sp,
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
color = Color.Black
|
||||
@@ -307,11 +307,18 @@ fun PlacementGuideView(appState: AppState) {
|
||||
|
||||
// ─── (1) Hint 1 — 박스는 항상 자리(높이 1.5×), 텍스트는 waitingForStart 시만 ───
|
||||
val hint1Bg = Color(0xFFFFF3E0)
|
||||
val hint1Text = remember {
|
||||
val hint1Full = stringResource(R.string.placement_press_start)
|
||||
val hint1KeyEn = "pubic bone"
|
||||
val hint1KeyKo = "치골"
|
||||
val hint1Text = remember(hint1Full) {
|
||||
buildAnnotatedString {
|
||||
val full = "Place VesiScan above the pubic bone, then press Start"
|
||||
val key = "pubic bone"
|
||||
val idx = full.indexOf(key, ignoreCase = true)
|
||||
val full = hint1Full
|
||||
val key = when {
|
||||
full.contains(hint1KeyEn, ignoreCase = true) -> hint1KeyEn
|
||||
full.contains(hint1KeyKo) -> hint1KeyKo
|
||||
else -> ""
|
||||
}
|
||||
val idx = if (key.isNotEmpty()) full.indexOf(key, ignoreCase = true) else -1
|
||||
if (idx >= 0) {
|
||||
append(full.substring(0, idx))
|
||||
withStyle(SpanStyle(color = Color.Red, fontWeight = FontWeight.ExtraBold)) {
|
||||
@@ -356,6 +363,8 @@ fun PlacementGuideView(appState: AppState) {
|
||||
if (hint.startsWith("Slide") || hint.startsWith("Optimizing") ||
|
||||
hint.startsWith("Checking") || hint.startsWith("Adjust") ||
|
||||
hint.contains("이동") || hint.contains("천천히") || hint.contains("그대로") ||
|
||||
hint.contains("내려") || hint.contains("올려") || hint.contains("확인") ||
|
||||
hint.contains("조정") || hint.contains("최적화") ||
|
||||
hint.contains("Hold") || hint.contains("slowly") || hint.contains("Slightly")) {
|
||||
append(dots)
|
||||
}
|
||||
@@ -364,14 +373,20 @@ fun PlacementGuideView(appState: AppState) {
|
||||
}
|
||||
}
|
||||
val displayHint = buildAnnotatedString {
|
||||
val key = "pubic bone"
|
||||
val idx = rawHint.indexOf(key, ignoreCase = true)
|
||||
val keyEn = "pubic bone"
|
||||
val keyKo = "치골"
|
||||
val matchedKey = when {
|
||||
rawHint.contains(keyEn, ignoreCase = true) -> keyEn
|
||||
rawHint.contains(keyKo) -> keyKo
|
||||
else -> ""
|
||||
}
|
||||
val idx = if (matchedKey.isNotEmpty()) rawHint.indexOf(matchedKey, ignoreCase = true) else -1
|
||||
if (idx >= 0) {
|
||||
append(rawHint.substring(0, idx))
|
||||
withStyle(SpanStyle(color = Color.Red, fontWeight = FontWeight.ExtraBold)) {
|
||||
append(rawHint.substring(idx, idx + key.length))
|
||||
append(rawHint.substring(idx, idx + matchedKey.length))
|
||||
}
|
||||
append(rawHint.substring(idx + key.length))
|
||||
append(rawHint.substring(idx + matchedKey.length))
|
||||
} else {
|
||||
append(rawHint)
|
||||
}
|
||||
@@ -400,7 +415,7 @@ fun PlacementGuideView(appState: AppState) {
|
||||
// 1) 하복부 일러스트 (배경 — 박스 가득)
|
||||
Image(
|
||||
painter = painterResource(id = com.example.medilightv2android.R.drawable.body_silhouette),
|
||||
contentDescription = "Body silhouette",
|
||||
contentDescription = stringResource(R.string.placement_body_desc),
|
||||
modifier = Modifier.matchParentSize(),
|
||||
contentScale = ContentScale.FillBounds
|
||||
)
|
||||
@@ -559,7 +574,7 @@ fun PlacementGuideView(appState: AppState) {
|
||||
// waitingForStart 일 때는 "Step 0/N" + 모든 점 숨김 (체크포인트 포함).
|
||||
val displayStepIdx = if (waitingForStart) -1 else currentStepIdx
|
||||
Text(
|
||||
text = "Step ${displayStepIdx + 1}/$totalSteps",
|
||||
text = stringResource(R.string.placement_step_format, displayStepIdx + 1, totalSteps),
|
||||
fontSize = 28.sp, fontWeight = FontWeight.Bold, color = Color.Black
|
||||
)
|
||||
if (!waitingForStart) {
|
||||
@@ -846,7 +861,7 @@ fun PlacementGuideView(appState: AppState) {
|
||||
prevDetached = nowDetached
|
||||
|
||||
if (detachResult != null && detachResult.isDetached) {
|
||||
directionHint = "Sensor detached. Restart above pubic bone."
|
||||
directionHint = context.getString(R.string.placement_detached_restart)
|
||||
directionIcon = "" // 화살표 없음
|
||||
placementScore = 0
|
||||
isContinuousMode = false
|
||||
@@ -938,7 +953,7 @@ fun PlacementGuideView(appState: AppState) {
|
||||
val vs = VerticalState(maxCenterSeen, stableAtMax, boundaryHit,
|
||||
lastDetectedCount = sweepLastDetected, everSawAllFour = sweepEverSawAll)
|
||||
val guideResult = computePlacementGuide(
|
||||
detected, urineLens, allWalls, scanCount, placementPhase, vs,
|
||||
context, detected, urineLens, allWalls, scanCount, placementPhase, vs,
|
||||
currentRMm = currentRMm,
|
||||
bestRMm = bestRMm,
|
||||
sagittalArgMaxCh = sagittalArgMaxCh,
|
||||
@@ -1016,7 +1031,7 @@ fun PlacementGuideView(appState: AppState) {
|
||||
|
||||
// GREEN hold 중이면 힌트 강제 유지
|
||||
if (greenHeld) {
|
||||
directionHint = "In position!"
|
||||
directionHint = context.getString(R.string.placement_in_position)
|
||||
directionIcon = "checkmark.circle"
|
||||
placementPhase = PlacementPhase.GREEN
|
||||
if (!guideResult.cv.isNaN()) lastCv = guideResult.cv
|
||||
@@ -1033,7 +1048,7 @@ fun PlacementGuideView(appState: AppState) {
|
||||
greenFailCount++
|
||||
if (greenFailCount < 3) {
|
||||
// 아직 이탈 안 함 — GREEN 유지
|
||||
directionHint = "In position!"
|
||||
directionHint = context.getString(R.string.placement_in_position)
|
||||
directionIcon = "checkmark.circle"
|
||||
placementPhase = PlacementPhase.GREEN
|
||||
} else {
|
||||
@@ -1042,7 +1057,7 @@ fun PlacementGuideView(appState: AppState) {
|
||||
greenLockedAtMs = 0L
|
||||
greenFailCount = 0
|
||||
if (lastLedMode != 5) { bleManager.sendLedMode(5); lastLedMode = 5 }
|
||||
directionHint = "Position lost. Restart above pubic bone."
|
||||
directionHint = context.getString(R.string.placement_position_lost)
|
||||
directionIcon = "" // 화살표 없음
|
||||
placementScore = 10
|
||||
placementPhase = PlacementPhase.VERTICAL
|
||||
@@ -1190,7 +1205,7 @@ fun PlacementGuideView(appState: AppState) {
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFFF9800))
|
||||
) {
|
||||
Text("Start Alignment", fontSize = 28.sp, fontWeight = FontWeight.Bold, color = Color.Black)
|
||||
Text(stringResource(R.string.placement_start_alignment), fontSize = 28.sp, fontWeight = FontWeight.Bold, color = Color.Black)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1228,7 +1243,7 @@ fun PlacementGuideView(appState: AppState) {
|
||||
Icon(Icons.Filled.CheckCircle, contentDescription = null, tint = Color.White, modifier = Modifier.size(32.dp))
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Text(
|
||||
if (isClinicalAlign) "Complete Alignment" else "Start Scanning",
|
||||
if (isClinicalAlign) stringResource(R.string.placement_complete_alignment) else stringResource(R.string.placement_start_scanning),
|
||||
fontSize = 28.sp, fontWeight = FontWeight.Bold, color = Color.White
|
||||
)
|
||||
}
|
||||
@@ -1249,7 +1264,7 @@ fun PlacementGuideView(appState: AppState) {
|
||||
}
|
||||
}) {
|
||||
Text(
|
||||
text = "Skip",
|
||||
text = stringResource(R.string.placement_skip),
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MlSecondaryText
|
||||
@@ -1675,6 +1690,7 @@ data class VerticalState(
|
||||
)
|
||||
|
||||
private fun computePlacementGuide(
|
||||
context: android.content.Context,
|
||||
detected: List<Boolean>,
|
||||
urineLens: List<Int>,
|
||||
walls: List<Pair<Int, Int>?> = emptyList(),
|
||||
@@ -1688,9 +1704,10 @@ private fun computePlacementGuide(
|
||||
lateralOffsetMm: Double? = null,
|
||||
): PlacementGuideResult {
|
||||
val detectedCount = detected.count { it }
|
||||
fun s(id: Int) = context.getString(id)
|
||||
|
||||
if (detectedCount == 0) {
|
||||
return PlacementGuideResult("Slide up ↑", "arrow.up", 0,
|
||||
return PlacementGuideResult(s(R.string.placement_slide_up), "arrow.up", 0,
|
||||
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL)
|
||||
}
|
||||
|
||||
@@ -1715,9 +1732,9 @@ private fun computePlacementGuide(
|
||||
// Phase 1: nothing detected — start instruction
|
||||
if (centerDetected == 0) {
|
||||
val hint = if (vs.everSawAllFour)
|
||||
"Signal lost — slide up ↑"
|
||||
s(R.string.placement_signal_lost)
|
||||
else
|
||||
"Start near navel, slide down slowly ↓"
|
||||
s(R.string.placement_sweep_start)
|
||||
val icon = if (vs.everSawAllFour) "arrow.up" else "arrow.down"
|
||||
return PlacementGuideResult(hint, icon, 5,
|
||||
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL)
|
||||
@@ -1726,7 +1743,7 @@ private fun computePlacementGuide(
|
||||
// Phase 4: was at all-4, now lost a channel → overshoot (boundary)
|
||||
if (vs.everSawAllFour && centerDetected < prev && centerDetected < 4) {
|
||||
vs.boundaryHit = true
|
||||
return PlacementGuideResult("Too far — slide up ↑", "arrow.up", 50,
|
||||
return PlacementGuideResult(s(R.string.placement_too_far), "arrow.up", 50,
|
||||
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL)
|
||||
}
|
||||
|
||||
@@ -1741,14 +1758,14 @@ private fun computePlacementGuide(
|
||||
val ch5ok = detected.getOrElse(5) { false }
|
||||
// 좌우도 이미 균등하면 바로 GREEN
|
||||
if (ch4ok && ch5ok && kotlin.math.abs(l4 - l5) <= 5) {
|
||||
PlacementGuideResult("Position OK! Final check...",
|
||||
PlacementGuideResult(s(R.string.placement_position_ok),
|
||||
"checkmark.circle", 90,
|
||||
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.GREEN)
|
||||
} else {
|
||||
val lrHint = when {
|
||||
l4 > 0 && l5 > 0 && l4 > l5 -> "Slide left ←"
|
||||
l4 > 0 && l5 > 0 && l5 > l4 -> "Slide right →"
|
||||
else -> "Checking"
|
||||
l4 > 0 && l5 > 0 && l4 > l5 -> s(R.string.placement_slide_left)
|
||||
l4 > 0 && l5 > 0 && l5 > l4 -> s(R.string.placement_slide_right)
|
||||
else -> s(R.string.placement_checking)
|
||||
}
|
||||
val lrIcon = when {
|
||||
l4 > 0 && l5 > 0 && l4 > l5 -> "arrow.left"
|
||||
@@ -1759,19 +1776,19 @@ private fun computePlacementGuide(
|
||||
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.LATERAL)
|
||||
}
|
||||
}
|
||||
1 -> PlacementGuideResult("Slide down slightly ↓", "arrow.down", 75,
|
||||
1 -> PlacementGuideResult(s(R.string.placement_slide_down_slight), "arrow.down", 75,
|
||||
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL)
|
||||
2 -> PlacementGuideResult("Slide down ↓", "arrow.down", 70,
|
||||
2 -> PlacementGuideResult(s(R.string.placement_slide_down), "arrow.down", 70,
|
||||
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL)
|
||||
3 -> PlacementGuideResult("Slide further down ↓", "arrow.down", 65,
|
||||
3 -> PlacementGuideResult(s(R.string.placement_slide_further_down), "arrow.down", 65,
|
||||
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL)
|
||||
else -> PlacementGuideResult("Hold still", "checkmark.circle", 80,
|
||||
else -> PlacementGuideResult(s(R.string.placement_hold_still), "checkmark.circle", 80,
|
||||
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: channels coming online (1..3)
|
||||
return PlacementGuideResult("Slide down ↓", "arrow.down",
|
||||
return PlacementGuideResult(s(R.string.placement_slide_down), "arrow.down",
|
||||
20 + centerDetected * 10,
|
||||
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL)
|
||||
}
|
||||
@@ -1784,13 +1801,13 @@ private fun computePlacementGuide(
|
||||
val hint: String; val icon: String
|
||||
when {
|
||||
(ch0det || ch1det) && !ch2det && !ch3det -> {
|
||||
hint = "Slide up ↑"; icon = "arrow.up"
|
||||
hint = s(R.string.placement_slide_up); icon = "arrow.up"
|
||||
}
|
||||
(ch3det || ch2det) && !ch0det && !ch1det -> {
|
||||
hint = "Slide down ↓"; icon = "arrow.down"
|
||||
hint = s(R.string.placement_slide_down); icon = "arrow.down"
|
||||
}
|
||||
else -> {
|
||||
hint = "Slide up ↑"; icon = "arrow.up"
|
||||
hint = s(R.string.placement_slide_up); icon = "arrow.up"
|
||||
}
|
||||
}
|
||||
return PlacementGuideResult(hint, icon, centerDetected * 10,
|
||||
@@ -1835,7 +1852,7 @@ private fun computePlacementGuide(
|
||||
val ch5ok = detected.getOrElse(5) { false }
|
||||
|
||||
// 항상 LATERAL 단계를 거침 — 전환 시 항상 Checking 표시
|
||||
val lrHint = "Checking"
|
||||
val lrHint = s(R.string.placement_checking)
|
||||
val lrIcon = "checkmark.circle"
|
||||
val score = (40 + ((1.0 - kotlin.math.abs(gradient) / gradientThr) * 20).toInt())
|
||||
.coerceIn(40, 60)
|
||||
@@ -1852,16 +1869,16 @@ private fun computePlacementGuide(
|
||||
val ch4ok = detected.getOrElse(4) { false }
|
||||
val ch5ok = detected.getOrElse(5) { false }
|
||||
// 항상 LATERAL 단계를 거침
|
||||
return PlacementGuideResult("Checking",
|
||||
return PlacementGuideResult(s(R.string.placement_checking),
|
||||
"checkmark.circle", 50,
|
||||
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.LATERAL)
|
||||
} else if (gradient > 0) {
|
||||
val intensity = if (gradient > 0.8) "Slide down ↓" else "Slide down slightly ↓"
|
||||
val intensity = if (gradient > 0.8) s(R.string.placement_slide_down) else s(R.string.placement_slide_down_slight)
|
||||
return PlacementGuideResult(intensity, "arrow.down",
|
||||
(20 + centerDetected * 5).coerceAtMost(35),
|
||||
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL)
|
||||
} else {
|
||||
val intensity = if (gradient < -0.8) "Slide up ↑" else "Slide up slightly ↑"
|
||||
val intensity = if (gradient < -0.8) s(R.string.placement_slide_up) else s(R.string.placement_slide_up_slight)
|
||||
return PlacementGuideResult(intensity, "arrow.up",
|
||||
(20 + centerDetected * 5).coerceAtMost(35),
|
||||
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL)
|
||||
@@ -1877,14 +1894,14 @@ private fun computePlacementGuide(
|
||||
|
||||
// 상하 유지 확인 — 좌우 이동 중 center 채널 소실
|
||||
if (centerDetected < 2) {
|
||||
return PlacementGuideResult("Start over from above the pubic bone", "arrow.up", 10,
|
||||
return PlacementGuideResult(s(R.string.placement_restart_pubic), "arrow.up", 10,
|
||||
phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.VERTICAL)
|
||||
}
|
||||
|
||||
// 절대 차이 10 sample 이하 → 좌우 충분히 균등, 바로 GREEN
|
||||
if (ch4det && ch5det && kotlin.math.abs(len4 - len5) <= 10) {
|
||||
return PlacementGuideResult(
|
||||
"Final check...",
|
||||
s(R.string.placement_final_check),
|
||||
"checkmark.circle", 60,
|
||||
phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.GREEN)
|
||||
}
|
||||
@@ -1894,15 +1911,15 @@ private fun computePlacementGuide(
|
||||
val absOff = kotlin.math.abs(lateralOffsetMm)
|
||||
if (absOff <= 8.0) {
|
||||
return PlacementGuideResult(
|
||||
"Final check...",
|
||||
s(R.string.placement_final_check),
|
||||
"checkmark.circle", 60,
|
||||
phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.GREEN)
|
||||
}
|
||||
return if (lateralOffsetMm > 0) {
|
||||
PlacementGuideResult("Slide right →", "arrow.right", 45,
|
||||
PlacementGuideResult(s(R.string.placement_slide_right), "arrow.right", 45,
|
||||
phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.LATERAL)
|
||||
} else {
|
||||
PlacementGuideResult("Slide left ←", "arrow.left", 45,
|
||||
PlacementGuideResult(s(R.string.placement_slide_left), "arrow.left", 45,
|
||||
phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.LATERAL)
|
||||
}
|
||||
}
|
||||
@@ -1910,16 +1927,16 @@ private fun computePlacementGuide(
|
||||
// 한쪽만 detect → 반대쪽으로 이동 (잡히는 쪽의 반대로)
|
||||
// ch4(왼쪽)만 잡힘 = 기기가 오른쪽 치우침 = 왼쪽으로
|
||||
if (ch4det && !ch5det) {
|
||||
return PlacementGuideResult("Slide left ←", "arrow.left", 40,
|
||||
return PlacementGuideResult(s(R.string.placement_slide_left), "arrow.left", 40,
|
||||
phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.LATERAL)
|
||||
}
|
||||
// ch5(오른쪽)만 잡힘 = 기기가 왼쪽 치우침 = 오른쪽으로
|
||||
if (ch5det && !ch4det) {
|
||||
return PlacementGuideResult("Slide right →", "arrow.right", 40,
|
||||
return PlacementGuideResult(s(R.string.placement_slide_right), "arrow.right", 40,
|
||||
phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.LATERAL)
|
||||
}
|
||||
if (!ch4det && !ch5det) {
|
||||
return PlacementGuideResult("Try moving right →", "arrow.right", 35,
|
||||
return PlacementGuideResult(s(R.string.placement_try_right), "arrow.right", 35,
|
||||
phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.LATERAL)
|
||||
}
|
||||
|
||||
@@ -1934,14 +1951,14 @@ private fun computePlacementGuide(
|
||||
|
||||
if (lrSymmetry >= 0.5) {
|
||||
return PlacementGuideResult(
|
||||
"Symmetry OK! Final check...",
|
||||
s(R.string.placement_symmetry_ok),
|
||||
"checkmark.circle", 60,
|
||||
phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.GREEN)
|
||||
}
|
||||
|
||||
// ch4 > ch5 → 방광이 오른쪽에 있음 → 오른쪽으로 이동
|
||||
// lrDiff > 0 = ch4 길다 = 방광이 왼쪽 → 왼쪽으로
|
||||
val hint = if (lrDiff > 0) "Move sensor left ←" else "Move sensor right →"
|
||||
val hint = if (lrDiff > 0) s(R.string.placement_move_left) else s(R.string.placement_move_right)
|
||||
val icon = if (lrDiff > 0) "arrow.left" else "arrow.right"
|
||||
return PlacementGuideResult(hint, icon, 45,
|
||||
phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.LATERAL)
|
||||
@@ -1976,13 +1993,13 @@ private fun computePlacementGuide(
|
||||
val navelMissing = 0 in missingCenters // ch0 = 배꼽 쪽 (top of sensor)
|
||||
val (hint, icon) = when {
|
||||
missingCenters.size >= 2 ->
|
||||
Pair("Adjust position — multiple channels missing", "arrow.up")
|
||||
Pair(s(R.string.placement_adjust_multiple), "arrow.up")
|
||||
pubicMissing ->
|
||||
Pair("Slide up slightly ↑ (ch3 missing)", "arrow.up")
|
||||
Pair(s(R.string.placement_slide_up_ch3), "arrow.up")
|
||||
navelMissing ->
|
||||
Pair("Slide down slightly ↓ (ch0 missing)", "arrow.down")
|
||||
Pair(s(R.string.placement_slide_down_ch0), "arrow.down")
|
||||
else ->
|
||||
Pair("Adjust position slightly — center channel missing", "arrow.up")
|
||||
Pair(s(R.string.placement_adjust_center), "arrow.up")
|
||||
}
|
||||
return PlacementGuideResult(hint, icon, 30,
|
||||
phase = PlacementPhase.GREEN, nextPhase = PlacementPhase.VERTICAL)
|
||||
@@ -2041,7 +2058,7 @@ private fun computePlacementGuide(
|
||||
if (isPass) {
|
||||
val score = ((1.0 - cv / cvThr).coerceIn(0.0, 1.0) * 100).toInt().coerceAtLeast(70)
|
||||
return PlacementGuideResult(
|
||||
"In position!",
|
||||
s(R.string.placement_in_position),
|
||||
"checkmark.circle", score, cv, true, lrDev, lrPass,
|
||||
phase = PlacementPhase.GREEN, nextPhase = PlacementPhase.GREEN)
|
||||
} else {
|
||||
@@ -2059,33 +2076,33 @@ private fun computePlacementGuide(
|
||||
when {
|
||||
// ch4/ch5 중 하나만 빠짐 → 반대 방향 안내
|
||||
!lrPass && !ch4det && ch5det -> {
|
||||
adjustHint = "Slide left ←"; adjustIcon = "arrow.left"
|
||||
adjustHint = s(R.string.placement_slide_left); adjustIcon = "arrow.left"
|
||||
}
|
||||
!lrPass && ch4det && !ch5det -> {
|
||||
adjustHint = "Slide right →"; adjustIcon = "arrow.right"
|
||||
adjustHint = s(R.string.placement_slide_right); adjustIcon = "arrow.right"
|
||||
}
|
||||
!lrPass && !ch4det && !ch5det -> {
|
||||
adjustHint = "Adjust left or right"; adjustIcon = "arrow.right"
|
||||
adjustHint = s(R.string.placement_adjust_lr); adjustIcon = "arrow.right"
|
||||
}
|
||||
!lrPass && lrDev > DEFAULT_LR_DEV_THR -> {
|
||||
adjustHint = "Slide left ←"; adjustIcon = "arrow.left"
|
||||
adjustHint = s(R.string.placement_slide_left); adjustIcon = "arrow.left"
|
||||
}
|
||||
!lrPass && lrDev < -DEFAULT_LR_DEV_THR -> {
|
||||
adjustHint = "Slide right →"; adjustIcon = "arrow.right"
|
||||
adjustHint = s(R.string.placement_slide_right); adjustIcon = "arrow.right"
|
||||
}
|
||||
// Edge 비대칭 — ch3 짧음 (센서 너무 아래) → 위로 미세 조정
|
||||
!edgePass && edgeAsym > 0 -> {
|
||||
adjustHint = "Slide up slightly ↑"; adjustIcon = "arrow.up"
|
||||
adjustHint = s(R.string.placement_slide_up_slight); adjustIcon = "arrow.up"
|
||||
}
|
||||
// Edge 비대칭 — ch0 짧음 (센서 너무 위) → 아래로 미세 조정
|
||||
!edgePass && edgeAsym < 0 -> {
|
||||
adjustHint = "Slide down slightly ↓"; adjustIcon = "arrow.down"
|
||||
adjustHint = s(R.string.placement_slide_down_slight); adjustIcon = "arrow.down"
|
||||
}
|
||||
gradient > 0.3 -> {
|
||||
adjustHint = "Slide down ↓"; adjustIcon = "arrow.down"
|
||||
adjustHint = s(R.string.placement_slide_down); adjustIcon = "arrow.down"
|
||||
}
|
||||
gradient < -0.3 -> {
|
||||
adjustHint = "Slide up ↑"; adjustIcon = "arrow.up"
|
||||
adjustHint = s(R.string.placement_slide_up); adjustIcon = "arrow.up"
|
||||
}
|
||||
else -> {
|
||||
// 방향 애매 + CV 미달 → ch별 len 분석으로 방향 제시
|
||||
@@ -2096,13 +2113,13 @@ private fun computePlacementGuide(
|
||||
// 최소 채널이 최대의 60% 미만 → 약한 쪽으로 안내
|
||||
if (minCh <= 1) {
|
||||
// ch0/ch1 약함 → 위로
|
||||
adjustHint = "Slide up slightly ↑"; adjustIcon = "arrow.up"
|
||||
adjustHint = s(R.string.placement_slide_up_slight); adjustIcon = "arrow.up"
|
||||
} else {
|
||||
// ch2/ch3 약함 → 아래로
|
||||
adjustHint = "Slide down slightly ↓"; adjustIcon = "arrow.down"
|
||||
adjustHint = s(R.string.placement_slide_down_slight); adjustIcon = "arrow.down"
|
||||
}
|
||||
} else {
|
||||
adjustHint = "Optimizing"; adjustIcon = "checkmark.circle"
|
||||
adjustHint = s(R.string.placement_optimizing); adjustIcon = "checkmark.circle"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+21
-19
@@ -25,10 +25,12 @@ import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.medilightv2android.AppState
|
||||
import com.example.medilightv2android.R
|
||||
import com.example.medilightv2android.ble.BleManager
|
||||
import com.example.medilightv2android.managers.NirsManager
|
||||
import com.example.medilightv2android.models.NirsMcjPacket
|
||||
@@ -195,16 +197,16 @@ fun VivamyoMonitoringView(appState: AppState) {
|
||||
if (showClearConfirm) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showClearConfirm = false },
|
||||
title = { Text("Clear all data?") },
|
||||
text = { Text("All collected NIRS data will be permanently deleted.") },
|
||||
title = { Text(stringResource(R.string.vivamyo_clear_title)) },
|
||||
text = { Text(stringResource(R.string.vivamyo_clear_message)) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
nirsManager.clearData()
|
||||
showClearConfirm = false
|
||||
}) { Text("Clear Data", color = Color.Red) }
|
||||
}) { Text(stringResource(R.string.vivamyo_clear_button), color = Color.Red) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showClearConfirm = false }) { Text("Cancel") }
|
||||
TextButton(onClick = { showClearConfirm = false }) { Text(stringResource(R.string.cancel)) }
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -215,10 +217,10 @@ fun VivamyoMonitoringView(appState: AppState) {
|
||||
else -> Color.Green
|
||||
}
|
||||
val stO2StatusText = when {
|
||||
currentOxy.stO2 < 50 -> "Low"
|
||||
currentOxy.stO2 < 60 -> "Caution"
|
||||
currentOxy.stO2 <= 85 -> "Normal"
|
||||
else -> "Check"
|
||||
currentOxy.stO2 < 50 -> context.getString(R.string.vivamyo_status_low)
|
||||
currentOxy.stO2 < 60 -> context.getString(R.string.vivamyo_status_caution)
|
||||
currentOxy.stO2 <= 85 -> context.getString(R.string.vivamyo_status_normal)
|
||||
else -> context.getString(R.string.vivamyo_status_check)
|
||||
}
|
||||
|
||||
Column(
|
||||
@@ -236,11 +238,11 @@ fun VivamyoMonitoringView(appState: AppState) {
|
||||
nirsManager.reset()
|
||||
appState.goHome()
|
||||
}, modifier = Modifier.size(48.dp)) {
|
||||
Icon(Icons.Default.Home, "Home", tint = MlPrimary, modifier = Modifier.size(if (isCompact) 20.dp else 24.dp))
|
||||
Icon(Icons.Default.Home, stringResource(R.string.vivamyo_home_desc), tint = MlPrimary, modifier = Modifier.size(if (isCompact) 20.dp else 24.dp))
|
||||
}
|
||||
Column(modifier = Modifier.padding(start = if (isCompact) 4.dp else 8.dp)) {
|
||||
Text("Vivamyo", fontSize = if (isCompact) 16.sp else 20.sp, fontWeight = FontWeight.Bold)
|
||||
if (!isCompact) Text("NIRS Monitoring", fontSize = 13.sp, color = MlSecondaryText)
|
||||
Text(stringResource(R.string.vivamyo_title), fontSize = if (isCompact) 16.sp else 20.sp, fontWeight = FontWeight.Bold)
|
||||
if (!isCompact) Text(stringResource(R.string.vivamyo_subtitle), fontSize = 13.sp, color = MlSecondaryText)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
if (isDemoMode) {
|
||||
@@ -283,9 +285,9 @@ fun VivamyoMonitoringView(appState: AppState) {
|
||||
NirsMetricsCard(nirsManager)
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
Text("Samples: $sampleCount", fontSize = 11.sp, color = MlSecondaryText)
|
||||
Text(stringResource(R.string.vivamyo_samples_label, sampleCount), fontSize = 11.sp, color = MlSecondaryText)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text("4Hz | 60s window", fontSize = 11.sp, color = MlSecondaryText)
|
||||
Text(stringResource(R.string.vivamyo_window_label), fontSize = 11.sp, color = MlSecondaryText)
|
||||
}
|
||||
} else {
|
||||
// Oxy/Deoxy tab
|
||||
@@ -293,14 +295,14 @@ fun VivamyoMonitoringView(appState: AppState) {
|
||||
LineChartCard("HHb (DeOxy)", nirsManager.oxyHbHistory, Color.Blue, "uM")
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Samples: $sampleCount", fontSize = 11.sp, color = MlSecondaryText)
|
||||
Text(stringResource(R.string.vivamyo_samples_label, sampleCount), fontSize = 11.sp, color = MlSecondaryText)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
if (isFilterEnabled) {
|
||||
Text("Filtered", fontSize = 10.sp, fontWeight = FontWeight.Bold, color = Color.White,
|
||||
Text(stringResource(R.string.vivamyo_filtered), fontSize = 10.sp, fontWeight = FontWeight.Bold, color = Color.White,
|
||||
modifier = Modifier.clip(RoundedCornerShape(4.dp)).background(Color(0xFF9966FF)).padding(horizontal = 6.dp, vertical = 2.dp))
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
}
|
||||
Text("4Hz | 60s window", fontSize = 11.sp, color = MlSecondaryText)
|
||||
Text(stringResource(R.string.vivamyo_window_label), fontSize = 11.sp, color = MlSecondaryText)
|
||||
}
|
||||
|
||||
NirsMetricsCard(nirsManager)
|
||||
@@ -321,7 +323,7 @@ fun VivamyoMonitoringView(appState: AppState) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(Icons.Default.Delete, null, tint = Color.Red)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text("Clear", fontWeight = FontWeight.SemiBold, color = Color.Red)
|
||||
Text(stringResource(R.string.vivamyo_clear), fontWeight = FontWeight.SemiBold, color = Color.Red)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,7 +348,7 @@ fun VivamyoMonitoringView(appState: AppState) {
|
||||
null, tint = Color.White
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(if (isStreaming) "Stop" else "Start", fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||
Text(if (isStreaming) stringResource(R.string.vivamyo_stop) else stringResource(R.string.vivamyo_start), fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -424,7 +426,7 @@ private fun LineChartCard(label: String, data: List<Double>, color: Color, unit:
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().height(140.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) { Text("No data", fontSize = 11.sp, color = MlSecondaryText) }
|
||||
) { Text(stringResource(R.string.vivamyo_no_data), fontSize = 11.sp, color = MlSecondaryText) }
|
||||
} else {
|
||||
val minVal = data.min()
|
||||
val maxVal = data.max()
|
||||
|
||||
+5
-3
@@ -11,11 +11,13 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.medilightv2android.AppState
|
||||
import com.example.medilightv2android.R
|
||||
import com.example.medilightv2android.ui.components.BladdyIdleRiveView
|
||||
import com.example.medilightv2android.ui.theme.*
|
||||
|
||||
@@ -34,7 +36,7 @@ fun OnboardingView(appState: AppState) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Text(
|
||||
text = "Welcome to VesiScan-Basic",
|
||||
text = stringResource(R.string.onboarding_welcome),
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center
|
||||
@@ -43,7 +45,7 @@ fun OnboardingView(appState: AppState) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Text(
|
||||
text = "Smart bladder monitoring\nfor better quality of life",
|
||||
text = stringResource(R.string.onboarding_subtitle),
|
||||
fontSize = 16.sp,
|
||||
color = MlSecondaryText,
|
||||
textAlign = TextAlign.Center
|
||||
@@ -71,7 +73,7 @@ fun OnboardingView(appState: AppState) {
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("Get Started", fontSize = 18.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||
Text(stringResource(R.string.onboarding_get_started), fontSize = 18.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,9 +16,11 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.medilightv2android.R
|
||||
import com.example.medilightv2android.ui.theme.MlCardBackground
|
||||
|
||||
@Composable
|
||||
@@ -55,7 +57,7 @@ fun ShuffledKeypad(
|
||||
) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.Backspace,
|
||||
contentDescription = "Delete",
|
||||
contentDescription = stringResource(R.string.pin_delete),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
|
||||
+6
-4
@@ -18,10 +18,12 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.medilightv2android.AppState
|
||||
import com.example.medilightv2android.R
|
||||
import com.example.medilightv2android.models.SensorMode
|
||||
import com.example.medilightv2android.ui.components.BladdyIdleRiveView
|
||||
import com.example.medilightv2android.ui.theme.*
|
||||
@@ -40,11 +42,11 @@ fun SensorSelectView(appState: AppState) {
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
Text("Select Sensor", fontSize = 28.sp, fontWeight = FontWeight.Bold)
|
||||
Text(stringResource(R.string.sensor_select_title), fontSize = 28.sp, fontWeight = FontWeight.Bold)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text("Choose your measurement mode", fontSize = 16.sp, color = MlSecondaryText)
|
||||
Text(stringResource(R.string.sensor_select_subtitle), fontSize = 16.sp, color = MlSecondaryText)
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
@@ -55,7 +57,7 @@ fun SensorSelectView(appState: AppState) {
|
||||
SensorCard(
|
||||
mode = SensorMode.NIRS,
|
||||
title = "NIRS",
|
||||
description = "Near-Infrared Spectroscopy\nTissue oxygenation monitoring",
|
||||
description = stringResource(R.string.sensor_nirs_desc),
|
||||
gradientColors = listOf(Color(0xFF00DBA1), Color(0xFF008FFF)),
|
||||
iconText = "NIRS",
|
||||
onClick = { appState.selectSensor(SensorMode.NIRS) }
|
||||
@@ -64,7 +66,7 @@ fun SensorSelectView(appState: AppState) {
|
||||
SensorCard(
|
||||
mode = SensorMode.PIEZO,
|
||||
title = "Piezo",
|
||||
description = "Ultrasound Echo\nBladder volume measurement",
|
||||
description = stringResource(R.string.sensor_piezo_desc),
|
||||
gradientColors = listOf(Color(0xFF8F44FC), Color(0xFFF04A8F)),
|
||||
iconText = "Piezo",
|
||||
onClick = { appState.selectSensor(SensorMode.PIEZO) }
|
||||
|
||||
Reference in New Issue
Block a user