feat(locale): Phase 3 — 전수 sweep, 노인 사용자 기준 영어 잔여 텍스트 한글화
이전 Phase 2 에서 'already English-only' 라며 skip 했던 화면들 — 노인 사용자에게는 영어로 보이면 안 됨. 전체 다시 훑어 모든 user-visible 영어 hardcoded text 를 strings.xml 로 추출. 11 파일 수정: - MainActivity (Exit dialog) - HomeView (Smart Bladder Monitoring / Start / Connected / Clinical Mode / dev toast) - OnboardingView (Welcome, Get Started 등) - CalibrationView (모든 status + Retry/Skip/Continue) - SensorSelectView (title, descriptions) - ShuffledKeypad (Delete contentDescription) - MeasurementHistoryView (title, tabs, empty state, Today/Yesterday) - VivamyoMonitoringView (전체 status) - PiezoMonitoringView (사용자 영역 — Auto/Single Scan, Voiding/Catheterization, 측정 카드, dialog 등) - PlacementGuideView (V1 hints, Step indicator, Start/Skip/Complete Alignment, In position/Position lost) 추출 strings: ~170 (각 언어). strings.xml 194 → 365 라인. skip 유지: - ClinicalHomeView / ClinicalLiveView (R&D dev mode) - PiezoMonitoring dev panel sliders (3-tap 활성화 dev 도구) - PlacementGuide dev settings overlay + Echo Waveforms (isDevMode gated) - BLE Debug Log / Scan Log dialog (dev 전용) - 브랜드명 (VesiScan-Basic, Vivamyo, Bladdy) - DEMO 배지, IMU debug 문자열, Log.d / debugLogger 출력 Korean coverage: ~95% (일반 사용자가 절대 보지 않는 dev surface 만 영어 유지). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -18,9 +18,11 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
+22
-13
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-25
@@ -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))
|
||||||
|
|||||||
+56
-53
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
+89
-72
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-19
@@ -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()
|
||||||
|
|||||||
+5
-3
@@ -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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-4
@@ -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) }
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 & Charts</string>
|
||||||
|
<string name="piezo_developer_tools">Developer Tools</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user