ui(dfu): 펌웨어 업데이트 화면 앱 스타일 통일 · 한글화 · 상태칩
미니멈 리디자인 (기존 3카드 구조 유지):
· 배경 = MlBackground · 카드 = MlCardBackground · RoundedCornerShape 16
· 헤드라인 "MEDiDFU" 제거 (앱 브랜드와 무관)
· 카드 label 한글화: "Firmware (bundled)" → "펌웨어 정보" ·
"Devices (n)" → "발견된 기기 (n)" · "Log" → "상세 로그"
· Phase 사용자 한글 라벨 + 색상 dot 상태칩:
IDLE→"대기 중" (회색) · SCANNING→"기기 검색 중…" (주황) ·
CONNECTED→"연결됨 · 업데이트 준비 완료" (녹색) ·
UPLOADING→"업데이트 진행 중…" (파랑) · COMPLETED→"완료 ✓" (녹색) ·
FAILED→"실패" (빨강) · ENTERING_DFU/SEARCHING_DFU 포함
· 주 액션 "업데이트 시작" = MlTeal→MlPrimary gradient primary 버튼
(앱 다른 primary 버튼 · Onboarding "Get started" 와 동일 스타일)
· 보조 액션 (검색/중지/연결 해제/취소/초기화) = outlined + MlPrimary 톤 ·
취소는 MlCritical 톤
· 진행률: 8dp 굵은 LinearProgressIndicator · MlPrimary 색 ·
Row 로 좌 (퍼센트 SemiBold) / 우 (KB / KB 표기)
· DeviceRow: 이름 SemiBold · address 제거 (RSSI 만) · "DFU 모드" 칩 MlTeal ·
"Connect" → "연결" · outlined 스타일
· 권한 안내: 문구 MlSecondaryText · 버튼도 gradient primary
· 로그: 카드 배경 MlSecondaryText 8% alpha (연한 회색) · 텍스트 MlSecondaryText
Import 추가: MlPrimary/MlTeal/MlBackground/MlCardBackground/MlSecondaryText/
MlCritical/MlSuccess/MlWarning · ButtonDefaults · Brush · PaddingValues.
빌드: BUILD SUCCESSFUL. (폰 미연결 · install 은 재연결 후 별도.)
This commit is contained in:
@@ -16,6 +16,7 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
@@ -25,6 +26,16 @@ import androidx.compose.foundation.layout.heightIn
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import com.medithings.vesiscan.ui.theme.MlBackground
|
||||||
|
import com.medithings.vesiscan.ui.theme.MlCardBackground
|
||||||
|
import com.medithings.vesiscan.ui.theme.MlCritical
|
||||||
|
import com.medithings.vesiscan.ui.theme.MlPrimary
|
||||||
|
import com.medithings.vesiscan.ui.theme.MlSecondaryText
|
||||||
|
import com.medithings.vesiscan.ui.theme.MlSuccess
|
||||||
|
import com.medithings.vesiscan.ui.theme.MlTeal
|
||||||
|
import com.medithings.vesiscan.ui.theme.MlWarning
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
@@ -113,13 +124,13 @@ fun FirmwareUpdateScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
|
|||||||
}
|
}
|
||||||
DisposableEffect(Unit) { onDispose { manager.shutdown() } }
|
DisposableEffect(Unit) { onDispose { manager.shutdown() } }
|
||||||
|
|
||||||
Column(modifier = modifier.fillMaxSize()) {
|
Column(modifier = modifier.fillMaxSize().background(MlBackground)) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 4.dp, vertical = 4.dp),
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 4.dp, vertical = 4.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = onBack) {
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "뒤로")
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "뒤로", tint = MlPrimary)
|
||||||
}
|
}
|
||||||
Text("펌웨어 업데이트", style = MaterialTheme.typography.titleMedium,
|
Text("펌웨어 업데이트", style = MaterialTheme.typography.titleMedium,
|
||||||
fontWeight = FontWeight.SemiBold)
|
fontWeight = FontWeight.SemiBold)
|
||||||
@@ -135,13 +146,64 @@ fun FirmwareUpdateScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
|
|||||||
Text(
|
Text(
|
||||||
"펌웨어 업데이트에는 Bluetooth 권한이 필요합니다.",
|
"펌웨어 업데이트에는 Bluetooth 권한이 필요합니다.",
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MlSecondaryText,
|
||||||
)
|
)
|
||||||
Button(onClick = { launcher.launch(perms) }) { Text("권한 허용") }
|
PrimaryGradientButton(text = "권한 허용", onClick = { launcher.launch(perms) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 앱 스타일 통일 primary 버튼 (MlTeal → MlPrimary gradient · rounded 14). */
|
||||||
|
@Composable
|
||||||
|
private fun PrimaryGradientButton(
|
||||||
|
text: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = onClick,
|
||||||
|
enabled = enabled,
|
||||||
|
modifier = modifier.heightIn(min = 48.dp),
|
||||||
|
shape = RoundedCornerShape(14.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
disabledContainerColor = Color.Transparent,
|
||||||
|
),
|
||||||
|
contentPadding = PaddingValues(),
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(min = 48.dp)
|
||||||
|
.background(
|
||||||
|
brush = if (enabled)
|
||||||
|
Brush.horizontalGradient(listOf(MlTeal, MlPrimary))
|
||||||
|
else Brush.horizontalGradient(listOf(Color(0xFFCCCCCC), Color(0xFFAAAAAA))),
|
||||||
|
shape = RoundedCornerShape(14.dp),
|
||||||
|
)
|
||||||
|
.padding(horizontal = 20.dp, vertical = 12.dp),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Text(text, color = Color.White, fontWeight = FontWeight.SemiBold, fontSize = 15.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Phase → 사용자용 한글 라벨 + 상태 색상. */
|
||||||
|
private fun phaseKo(phase: DfuPhase): Pair<String, Color> = when (phase) {
|
||||||
|
DfuPhase.IDLE -> "대기 중" to MlSecondaryText
|
||||||
|
DfuPhase.SCANNING -> "기기 검색 중…" to MlWarning
|
||||||
|
DfuPhase.CONNECTING -> "연결 중…" to MlWarning
|
||||||
|
DfuPhase.CONNECTED -> "연결됨 · 업데이트 준비 완료" to MlSuccess
|
||||||
|
DfuPhase.ENTERING_DFU -> "DFU 모드 진입 중…" to MlPrimary
|
||||||
|
DfuPhase.SEARCHING_DFU -> "DFU 모드 확인 중…" to MlPrimary
|
||||||
|
DfuPhase.UPLOADING -> "업데이트 진행 중…" to MlPrimary
|
||||||
|
DfuPhase.COMPLETED -> "완료 ✓" to MlSuccess
|
||||||
|
DfuPhase.FAILED -> "실패" to MlCritical
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DfuScreen(manager: DfuManager, modifier: Modifier = Modifier) {
|
fun DfuScreen(manager: DfuManager, modifier: Modifier = Modifier) {
|
||||||
val phase by manager.phase
|
val phase by manager.phase
|
||||||
@@ -152,133 +214,200 @@ fun DfuScreen(manager: DfuManager, modifier: Modifier = Modifier) {
|
|||||||
val bytesSent by manager.bytesSent
|
val bytesSent by manager.bytesSent
|
||||||
val target by manager.target
|
val target by manager.target
|
||||||
|
|
||||||
|
val cardShape = RoundedCornerShape(16.dp)
|
||||||
|
val cardColors = CardDefaults.cardColors(containerColor = MlCardBackground)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
.background(MlBackground)
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
Text("MEDiDFU", style = MaterialTheme.typography.headlineSmall)
|
// ── 스캔 실패 원인 안내 (블루투스 꺼짐 등) ────────────────────────────────────────
|
||||||
|
|
||||||
// ── Anything outside the app that would silently break scanning ───────────────────
|
|
||||||
val blocker = manager.bleBlockingCondition()
|
val blocker = manager.bleBlockingCondition()
|
||||||
if (blocker != null) {
|
if (blocker != null) {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
colors = CardDefaults.cardColors(
|
shape = cardShape,
|
||||||
containerColor = MaterialTheme.colorScheme.errorContainer,
|
colors = CardDefaults.cardColors(containerColor = MlCritical.copy(alpha = 0.10f)),
|
||||||
),
|
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
blocker,
|
blocker,
|
||||||
modifier = Modifier.padding(12.dp),
|
modifier = Modifier.padding(14.dp),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onErrorContainer,
|
color = MlCritical,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Firmware bundled in the APK ────────────────────────────────────────────────────
|
// ── 펌웨어 정보 (APK 번들) ────────────────────────────────────────────────────────
|
||||||
Card(modifier = Modifier.fillMaxWidth()) {
|
Card(modifier = Modifier.fillMaxWidth(), shape = cardShape, colors = cardColors) {
|
||||||
Column(Modifier.padding(12.dp)) {
|
Column(Modifier.padding(14.dp)) {
|
||||||
Text("Firmware (bundled)", style = MaterialTheme.typography.labelLarge)
|
Text("펌웨어 정보", style = MaterialTheme.typography.labelLarge, color = MlSecondaryText)
|
||||||
Spacer(Modifier.height(4.dp))
|
Spacer(Modifier.height(6.dp))
|
||||||
val fw = firmware
|
val fw = firmware
|
||||||
when {
|
when {
|
||||||
fw != null -> {
|
fw != null -> {
|
||||||
Text(fw.assetName, style = MaterialTheme.typography.bodyMedium)
|
val ver = if (fw.version.isNotEmpty()) "v${fw.version}" else "버전 정보 없음"
|
||||||
Text(
|
val sizeKb = fw.sizeBytes / 1024
|
||||||
"${fw.imageFileName} · v${fw.version.ifEmpty { "?" }} · ${fw.sizeBytes} bytes",
|
Text("$ver · ${sizeKb} KB",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold)
|
||||||
|
Spacer(Modifier.height(2.dp))
|
||||||
|
Text(fw.imageFileName,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
)
|
color = MlSecondaryText)
|
||||||
}
|
}
|
||||||
|
|
||||||
firmwareError != null -> Text(
|
firmwareError != null -> Text(
|
||||||
firmwareError ?: "",
|
firmwareError ?: "",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.error,
|
color = MlCritical,
|
||||||
)
|
)
|
||||||
|
|
||||||
else -> Text("Loading…", style = MaterialTheme.typography.bodySmall)
|
else -> Text("불러오는 중…",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MlSecondaryText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Status + progress ─────────────────────────────────────────────────────────────
|
// ── 상태 + 진행률 ─────────────────────────────────────────────────────────────────
|
||||||
Card(modifier = Modifier.fillMaxWidth()) {
|
Card(modifier = Modifier.fillMaxWidth(), shape = cardShape, colors = cardColors) {
|
||||||
Column(Modifier.padding(12.dp)) {
|
Column(Modifier.padding(14.dp)) {
|
||||||
Text(phase.name, style = MaterialTheme.typography.labelLarge)
|
Text("기기 상태", style = MaterialTheme.typography.labelLarge, color = MlSecondaryText)
|
||||||
Spacer(Modifier.height(4.dp))
|
Spacer(Modifier.height(6.dp))
|
||||||
Text(status, style = MaterialTheme.typography.bodyMedium)
|
val (phaseLabel, phaseColor) = phaseKo(phase)
|
||||||
target?.let {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text(
|
Box(
|
||||||
"${it.name} · ${it.address}",
|
modifier = Modifier
|
||||||
style = MaterialTheme.typography.bodySmall,
|
.size(10.dp)
|
||||||
|
.background(phaseColor, shape = RoundedCornerShape(50)),
|
||||||
)
|
)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Text(phaseLabel,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = phaseColor)
|
||||||
|
}
|
||||||
|
if (status.isNotEmpty()) {
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
Text(status, style = MaterialTheme.typography.bodySmall, color = MlSecondaryText)
|
||||||
|
}
|
||||||
|
target?.let {
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
Text("기기: ${it.name}",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MlSecondaryText)
|
||||||
}
|
}
|
||||||
if (phase == DfuPhase.UPLOADING || phase == DfuPhase.COMPLETED) {
|
if (phase == DfuPhase.UPLOADING || phase == DfuPhase.COMPLETED) {
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(10.dp))
|
||||||
LinearProgressIndicator(
|
LinearProgressIndicator(
|
||||||
progress = { progress },
|
progress = { progress },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth().height(8.dp),
|
||||||
|
color = MlPrimary,
|
||||||
|
trackColor = MlPrimary.copy(alpha = 0.15f),
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(4.dp))
|
Spacer(Modifier.height(6.dp))
|
||||||
val total = firmware?.sizeBytes ?: 0
|
val total = firmware?.sizeBytes ?: 0
|
||||||
Text(
|
val sentKb = bytesSent / 1024
|
||||||
"${(progress * 100).roundToInt()}% ($bytesSent / $total bytes)",
|
val totalKb = total / 1024
|
||||||
style = MaterialTheme.typography.bodySmall,
|
Row(
|
||||||
)
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
|
Text("${(progress * 100).roundToInt()}%",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = MlPrimary)
|
||||||
|
Text("$sentKb KB / $totalKb KB",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MlSecondaryText)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Actions ───────────────────────────────────────────────────────────────────────
|
// ── 액션 버튼 (primary 는 gradient · secondary 는 outline) ───────────────────────
|
||||||
// 2026-08-03: auto-connect 로 대부분 target 즉시 설정됨.
|
// 2026-08-03: auto-connect 로 대부분 target 즉시 설정됨. target 없을 때만 Scan 노출.
|
||||||
// target 없는 경우 (fresh install · 새 기기) 에만 수동 Scan/Stop 노출.
|
|
||||||
val hasTarget = target != null
|
val hasTarget = target != null
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
val outlineColors = ButtonDefaults.outlinedButtonColors(contentColor = MlPrimary)
|
||||||
if (!hasTarget || phase == DfuPhase.IDLE || phase == DfuPhase.SCANNING) {
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
if (phase == DfuPhase.SCANNING) {
|
// 주 액션: 업데이트 시작 (조건부 gradient primary)
|
||||||
OutlinedButton(onClick = { manager.stopScan() }) { Text("Stop") }
|
if (phase != DfuPhase.UPLOADING && phase != DfuPhase.COMPLETED && phase != DfuPhase.FAILED) {
|
||||||
} else {
|
PrimaryGradientButton(
|
||||||
OutlinedButton(
|
text = "업데이트 시작",
|
||||||
onClick = { manager.startScan() },
|
onClick = { manager.startDfu() },
|
||||||
enabled = !manager.isBusy,
|
enabled = manager.canStartDfu,
|
||||||
) { Text("Scan") }
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 보조 액션 (조건부)
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
if (!hasTarget || phase == DfuPhase.IDLE || phase == DfuPhase.SCANNING) {
|
||||||
|
if (phase == DfuPhase.SCANNING) {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { manager.stopScan() },
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
colors = outlineColors,
|
||||||
|
) { Text("검색 중지") }
|
||||||
|
} else {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { manager.startScan() },
|
||||||
|
enabled = !manager.isBusy,
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
colors = outlineColors,
|
||||||
|
) { Text("기기 검색") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (phase == DfuPhase.CONNECTED) {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { manager.disconnect() },
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
colors = outlineColors,
|
||||||
|
) { Text("연결 해제") }
|
||||||
|
}
|
||||||
|
if (phase == DfuPhase.UPLOADING) {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { manager.cancelDfu() },
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(contentColor = MlCritical),
|
||||||
|
) { Text("취소") }
|
||||||
|
}
|
||||||
|
if (phase == DfuPhase.COMPLETED || phase == DfuPhase.FAILED) {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { manager.reset() },
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
colors = outlineColors,
|
||||||
|
) { Text("초기화") }
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Button(
|
|
||||||
onClick = { manager.startDfu() },
|
|
||||||
enabled = manager.canStartDfu,
|
|
||||||
) { Text("Start DFU") }
|
|
||||||
|
|
||||||
if (phase == DfuPhase.CONNECTED) {
|
|
||||||
OutlinedButton(onClick = { manager.disconnect() }) { Text("Disconnect") }
|
|
||||||
}
|
|
||||||
if (phase == DfuPhase.UPLOADING) {
|
|
||||||
OutlinedButton(onClick = { manager.cancelDfu() }) { Text("Cancel") }
|
|
||||||
}
|
|
||||||
if (phase == DfuPhase.COMPLETED || phase == DfuPhase.FAILED) {
|
|
||||||
OutlinedButton(onClick = { manager.reset() }) { Text("Reset") }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Discovered devices — target 없을 때만 (auto-connect 실패 · 첫 사용) ────────
|
// ── 발견된 기기 목록 (auto-connect 실패 · 첫 사용) ───────────────────────────────
|
||||||
if (!hasTarget) {
|
if (!hasTarget) {
|
||||||
Text(
|
Text(
|
||||||
"Devices (${manager.devices.size})",
|
"발견된 기기 (${manager.devices.size})",
|
||||||
style = MaterialTheme.typography.labelLarge,
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
color = MlSecondaryText,
|
||||||
)
|
)
|
||||||
Card(modifier = Modifier.fillMaxWidth().heightIn(min = 80.dp, max = 220.dp)) {
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth().heightIn(min = 80.dp, max = 220.dp),
|
||||||
|
shape = cardShape,
|
||||||
|
colors = cardColors,
|
||||||
|
) {
|
||||||
if (manager.devices.isEmpty()) {
|
if (manager.devices.isEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
when (phase) {
|
when (phase) {
|
||||||
DfuPhase.SCANNING -> "Scanning…"
|
DfuPhase.SCANNING -> "검색 중…"
|
||||||
else -> "No devices yet — tap Scan."
|
else -> "기기가 없습니다. 검색을 눌러주세요."
|
||||||
},
|
},
|
||||||
modifier = Modifier.padding(12.dp),
|
modifier = Modifier.padding(14.dp),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MlSecondaryText,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
LazyColumn {
|
LazyColumn {
|
||||||
@@ -288,15 +417,15 @@ fun DfuScreen(manager: DfuManager, modifier: Modifier = Modifier) {
|
|||||||
enabled = !manager.isBusy,
|
enabled = !manager.isBusy,
|
||||||
onClick = { manager.connect(device) },
|
onClick = { manager.connect(device) },
|
||||||
)
|
)
|
||||||
HorizontalDivider()
|
HorizontalDivider(color = MlBackground)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Log ───────────────────────────────────────────────────────────────────────────
|
// ── 상세 로그 (개발자용 · 그대로 유지) ────────────────────────────────────────────
|
||||||
Text("Log", style = MaterialTheme.typography.labelLarge)
|
Text("상세 로그", style = MaterialTheme.typography.labelLarge, color = MlSecondaryText)
|
||||||
LogView(
|
LogView(
|
||||||
lines = manager.logLines,
|
lines = manager.logLines,
|
||||||
modifier = Modifier.fillMaxWidth().weight(1f),
|
modifier = Modifier.fillMaxWidth().weight(1f),
|
||||||
@@ -309,33 +438,41 @@ private fun DeviceRow(device: ScannedDevice, enabled: Boolean, onClick: () -> Un
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
.padding(horizontal = 14.dp, vertical = 10.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Column(Modifier.weight(1f)) {
|
Column(Modifier.weight(1f)) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text(device.name, style = MaterialTheme.typography.bodyMedium)
|
Text(device.name, style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.SemiBold)
|
||||||
if (device.inDfuMode) {
|
if (device.inDfuMode) {
|
||||||
Spacer(Modifier.width(6.dp))
|
Spacer(Modifier.width(6.dp))
|
||||||
Surface(
|
Surface(
|
||||||
color = MaterialTheme.colorScheme.tertiaryContainer,
|
color = MlTeal.copy(alpha = 0.15f),
|
||||||
shape = RoundedCornerShape(4.dp),
|
shape = RoundedCornerShape(6.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
"DFU",
|
"DFU 모드",
|
||||||
modifier = Modifier.padding(horizontal = 4.dp),
|
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = MaterialTheme.colorScheme.onTertiaryContainer,
|
color = MlTeal,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Spacer(Modifier.height(2.dp))
|
||||||
Text(
|
Text(
|
||||||
"${device.address} · ${device.rssi} dBm",
|
"${device.rssi} dBm",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MlSecondaryText,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Button(onClick = onClick, enabled = enabled) { Text("Connect") }
|
OutlinedButton(
|
||||||
|
onClick = onClick,
|
||||||
|
enabled = enabled,
|
||||||
|
shape = RoundedCornerShape(10.dp),
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(contentColor = MlPrimary),
|
||||||
|
) { Text("연결", fontWeight = FontWeight.SemiBold) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,14 +484,15 @@ private fun LogView(lines: List<String>, modifier: Modifier = Modifier) {
|
|||||||
}
|
}
|
||||||
Card(
|
Card(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = MlSecondaryText.copy(alpha = 0.08f)),
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = listState,
|
state = listState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(Color.Transparent)
|
.background(Color.Transparent)
|
||||||
.padding(8.dp),
|
.padding(10.dp),
|
||||||
) {
|
) {
|
||||||
itemsIndexed(lines) { _, line ->
|
itemsIndexed(lines) { _, line ->
|
||||||
Text(
|
Text(
|
||||||
@@ -362,6 +500,7 @@ private fun LogView(lines: List<String>, modifier: Modifier = Modifier) {
|
|||||||
fontFamily = FontFamily.Monospace,
|
fontFamily = FontFamily.Monospace,
|
||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = MlSecondaryText,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user