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