Browse Source

feat: recovery state card

dev_ios
AnranYus 3 weeks ago
parent
commit
84fdca1554
  1. 2
      gradle/libs.versions.toml
  2. 2
      shared/build.gradle.kts
  3. BIN
      shared/src/commonMain/composeResources/drawable/ic_state_heart_rate.png
  4. BIN
      shared/src/commonMain/composeResources/drawable/ic_state_oxygen.png
  5. BIN
      shared/src/commonMain/composeResources/drawable/ic_state_pressure.png
  6. BIN
      shared/src/commonMain/composeResources/drawable/ic_state_sleep.png
  7. BIN
      shared/src/commonMain/composeResources/drawable/ic_state_temperature.png
  8. 6
      shared/src/commonMain/kotlin/com/whitefish/ring/bean/ui/Oximetry.kt
  9. 4
      shared/src/commonMain/kotlin/com/whitefish/ring/bean/ui/SleepState.kt
  10. 6
      shared/src/commonMain/kotlin/com/whitefish/ring/bean/ui/Temperature.kt
  11. 15
      shared/src/commonMain/kotlin/com/whitefish/ring/bean/ui/TimeComponents.kt
  12. 4
      shared/src/commonMain/kotlin/com/whitefish/ring/data/DeviceDataProvider.kt
  13. 4
      shared/src/commonMain/kotlin/com/whitefish/ring/ui/chart/ComposeMultiplatformBasicLineChart.kt
  14. 4
      shared/src/commonMain/kotlin/com/whitefish/ring/ui/chart/RecoveryChart.kt
  15. 16
      shared/src/commonMain/kotlin/com/whitefish/ring/ui/chart/RecoveryChartWithTimeLabels.kt
  16. 122
      shared/src/commonMain/kotlin/com/whitefish/ring/ui/chart/RecoveryStateCard.kt
  17. 44
      shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/recovery/RecoveryScreen.kt
  18. 93
      shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/recovery/RecoveryViewModel.kt
  19. 129
      shared/src/commonMain/kotlin/com/whitefish/ring/utils/StyledTextBuilder.kt
  20. 83
      shared/src/iosMain/kotlin/com/whitefish/ring/data/DeviceDataProvider.ios.kt

2
gradle/libs.versions.toml

@ -10,6 +10,7 @@ vico = "2.1.3"
androidDatabaseSqlcipher = "4.5.4" androidDatabaseSqlcipher = "4.5.4"
roomKtx = "2.6.1" roomKtx = "2.6.1"
activityKtx = "1.10.1" activityKtx = "1.10.1"
uiTooling = "1.8.2"
[libraries] [libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
@ -28,6 +29,7 @@ androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref =
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomKtx" } androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomKtx" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomKtx" } androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomKtx" }
androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activityKtx" } androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activityKtx" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "uiTooling" }
[plugins] [plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" } androidApplication = { id = "com.android.application", version.ref = "agp" }

2
shared/build.gradle.kts

@ -93,6 +93,7 @@ android {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8
} }
buildToolsVersion = "36.0.0"
dependencies { dependencies {
implementation(libs.androidx.room.runtime) implementation(libs.androidx.room.runtime)
ksp(libs.androidx.room.compiler) ksp(libs.androidx.room.compiler)
@ -103,4 +104,5 @@ android {
} }
dependencies { dependencies {
implementation(libs.androidx.activity.ktx) implementation(libs.androidx.activity.ktx)
debugImplementation(libs.androidx.ui.tooling)
} }

BIN
shared/src/commonMain/composeResources/drawable/ic_state_heart_rate.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 741 B

BIN
shared/src/commonMain/composeResources/drawable/ic_state_oxygen.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 720 B

BIN
shared/src/commonMain/composeResources/drawable/ic_state_pressure.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 B

BIN
shared/src/commonMain/composeResources/drawable/ic_state_sleep.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 773 B

BIN
shared/src/commonMain/composeResources/drawable/ic_state_temperature.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 725 B

6
shared/src/commonMain/kotlin/com/whitefish/ring/bean/ui/Oximetry.kt

@ -0,0 +1,6 @@
package com.whitefish.ring.bean.ui
data class Oximetry(
val time: Long,
val value: Float,
)

4
shared/src/commonMain/kotlin/com/whitefish/ring/bean/ui/SleepState.kt

@ -5,5 +5,7 @@ import kotlinx.datetime.Clock
data class SleepState ( data class SleepState (
val totalSeconds: Int, //秒 val totalSeconds: Int, //秒
val segments: List<SleepSegment> val segments: List<SleepSegment>,
val start: Long,
val end: Long
) )

6
shared/src/commonMain/kotlin/com/whitefish/ring/bean/ui/Temperature.kt

@ -0,0 +1,6 @@
package com.whitefish.ring.bean.ui
data class Temperature (
val time: Long,
val value: Float,
)

15
shared/src/commonMain/kotlin/com/whitefish/ring/bean/ui/TimeComponents.kt

@ -0,0 +1,15 @@
package com.whitefish.ring.bean.ui
data class TimeComponents(
val hours: Int,
val minutes: Int,
val seconds: Int
)
fun Int.convertSecondsToTime(): TimeComponents {
val hours = this / 3600
val minutes = (this % 3600) / 60
val seconds = this % 60
return TimeComponents(hours, minutes, seconds)
}

4
shared/src/commonMain/kotlin/com/whitefish/ring/data/DeviceDataProvider.kt

@ -1,10 +1,14 @@
package com.whitefish.ring.data package com.whitefish.ring.data
import com.whitefish.ring.bean.ui.HeartRate import com.whitefish.ring.bean.ui.HeartRate
import com.whitefish.ring.bean.ui.Oximetry
import com.whitefish.ring.bean.ui.SleepState import com.whitefish.ring.bean.ui.SleepState
import com.whitefish.ring.bean.ui.Temperature
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
suspend fun mac() = obtainDataStore().data.map { it[address] }.first() suspend fun mac() = obtainDataStore().data.map { it[address] }.first()
expect suspend fun getHeartRate(start: Long,end: Long): List<HeartRate> expect suspend fun getHeartRate(start: Long,end: Long): List<HeartRate>
expect suspend fun getSleep(start: Long, end: Long): SleepState expect suspend fun getSleep(start: Long, end: Long): SleepState
expect suspend fun getOximetry(start: Long, end: Long): List<Oximetry>
expect suspend fun getTemperature(start: Long, end: Long): List<Temperature>

4
shared/src/commonMain/kotlin/com/whitefish/ring/ui/chart/ComposeMultiplatformBasicLineChart.kt

@ -19,7 +19,7 @@ import com.patrykandpatrick.vico.multiplatform.common.Fill
import com.patrykandpatrick.vico.multiplatform.common.fill import com.patrykandpatrick.vico.multiplatform.common.fill
@Composable @Composable
fun ComposeMultiplatformBasicLineChart(data: List<Int>, modifier: Modifier = Modifier) { fun ComposeMultiplatformBasicLineChart(data: List<Number>, modifier: Modifier = Modifier) {
val modelProducer = remember { CartesianChartModelProducer() } val modelProducer = remember { CartesianChartModelProducer() }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
modelProducer.runTransaction { modelProducer.runTransaction {
@ -52,6 +52,6 @@ fun ComposeMultiplatformBasicLineChart(data: List<Int>, modifier: Modifier = Mod
), ),
modelProducer = modelProducer, modelProducer = modelProducer,
modifier = modifier, modifier = modifier,
zoomState = VicoZoomState(false, Zoom.Content,Zoom.Content,Zoom.Content) zoomState = VicoZoomState(false, Zoom.Content, Zoom.Content, Zoom.Content)
) )
} }

4
shared/src/commonMain/kotlin/com/whitefish/ring/ui/chart/RecoveryChart.kt

@ -115,7 +115,7 @@ private fun DrawScope.drawRecoveryChart(
// 绘制水平虚线 // 绘制水平虚线
val midLineY = chartBottom - chartHeight / 2 val midLineY = chartBottom - chartHeight / 2
drawDashedLine( drawDashLine(
start = Offset(padding, midLineY), start = Offset(padding, midLineY),
end = Offset(size.width - padding, midLineY), end = Offset(size.width - padding, midLineY),
color = Color.White.copy(alpha = 0.4f) color = Color.White.copy(alpha = 0.4f)
@ -185,7 +185,7 @@ private fun DrawScope.drawRecoveryChart(
} }
} }
private fun DrawScope.drawDashedLine( private fun DrawScope.drawDashLine(
start: Offset, start: Offset,
end: Offset, end: Offset,
color: Color, color: Color,

16
shared/src/commonMain/kotlin/com/whitefish/ring/ui/chart/RecoveryChartWithTimeLabels.kt

@ -240,7 +240,9 @@ private fun DrawScope.drawRecoveryChartWithLabels(
drawSmoothCurve( drawSmoothCurve(
points = actualPoints, points = actualPoints,
brush = gradientBrush, brush = gradientBrush,
strokeWidth = 4.dp.toPx() strokeWidth = 2.dp.toPx(),
color = Color.Transparent,
isDashed = false,
) )
} }
@ -248,8 +250,10 @@ private fun DrawScope.drawRecoveryChartWithLabels(
if (predictedPoints.isNotEmpty()) { if (predictedPoints.isNotEmpty()) {
drawSmoothCurve( drawSmoothCurve(
points = predictedPoints, points = predictedPoints,
brush = null,
color = Color.White.copy(alpha = 0.9f), color = Color.White.copy(alpha = 0.9f),
strokeWidth = 3.dp.toPx() strokeWidth = 2.dp.toPx(),
isDashed = false,
) )
} }
@ -309,10 +313,10 @@ private fun DrawScope.drawDashedLine(
private fun DrawScope.drawSmoothCurve( private fun DrawScope.drawSmoothCurve(
points: List<Offset>, points: List<Offset>,
brush: Brush? = null, brush: Brush?,
color: Color = Color.Transparent, color: Color,
strokeWidth: Float = 4f, strokeWidth: Float,
isDashed: Boolean = false isDashed: Boolean
) { ) {
if (points.size < 2) return if (points.size < 2) return

122
shared/src/commonMain/kotlin/com/whitefish/ring/ui/chart/RecoveryStateCard.kt

@ -0,0 +1,122 @@
package com.whitefish.ring.ui.chart
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.whitefish.app.ui.chart.sleep.SleepChart
import com.whitefish.ring.bean.ui.SleepState
import com.whitefish.ring.utils.buildStyledText
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.ui.tooling.preview.Preview
import ring.shared.generated.resources.Res
data class RecoveryStateCardItem(
val icon: DrawableResource,
val title: String,
val tip: String,
val value: AnnotatedString,
val type: RecoveryStateCardType
)
sealed class RecoveryStateCardType() {
class Sleep(val state: SleepState) : RecoveryStateCardType()
class HeartRate(val values: List<Int>) : RecoveryStateCardType()
class Oximetry(val values: List<Float>) : RecoveryStateCardType()
class Pressure(val values: List<Int>) : RecoveryStateCardType()
class Temperature(val values: List<Float>) : RecoveryStateCardType()
}
@Composable
fun RecoveryStateCard(states: List<RecoveryStateCardItem>) {
Card(
shape = RoundedCornerShape(40.dp),
modifier = Modifier.fillMaxWidth().wrapContentHeight()
) {
Spacer(modifier = Modifier.height(6.dp))
Box(modifier = Modifier.width(92.dp).height(6.dp).clip(RoundedCornerShape(15.dp)).background(Color(0xff352764)).align(
Alignment.CenterHorizontally))
Column(modifier = Modifier.padding(top = 28.dp)) {
states.forEach {
StateItem(painterResource(it.icon), it.title, it.tip, it.value) { modifier ->
when (it.type) {
is RecoveryStateCardType.Sleep -> {
SleepChart(
it.type.state.segments,modifier = modifier
)
}
is RecoveryStateCardType.HeartRate -> {
ComposeMultiplatformBasicLineChart(it.type.values,modifier = modifier)
}
is RecoveryStateCardType.Oximetry -> {
ComposeMultiplatformBasicLineChart(it.type.values,modifier = modifier)
}
is RecoveryStateCardType.Pressure -> TODO()
is RecoveryStateCardType.Temperature -> {
ComposeMultiplatformBasicLineChart(it.type.values,modifier = modifier)
}
}
}
Spacer(modifier = Modifier.height(38.dp))
}
}
}
}
@Composable
fun StateItem(
icon: Painter,
title: String,
tip: String,
value: AnnotatedString,
chart: @Composable (Modifier) -> Unit
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().padding(start = 20.dp).height(72.dp)
) {
Image(painter = icon, contentDescription = null, modifier = Modifier.size(40.dp))
Spacer(modifier = Modifier.width(21.dp))
Column {
Text(title, fontSize = 18.sp)
Spacer(modifier = Modifier.height(7.dp))
Text(tip, fontSize = 12.sp, color = Color(0xff61B7C8), modifier = Modifier.width(76.dp))
}
Spacer(modifier = Modifier.width(20.dp))
chart(Modifier.fillMaxHeight().width(72.dp))
Spacer(modifier = Modifier.width(28.dp))
Text(value)
}
}

44
shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/recovery/RecoveryScreen.kt

@ -1,18 +1,20 @@
package com.whitefish.ring.ui.home.recovery package com.whitefish.ring.ui.home.recovery
import androidx.compose.foundation.background import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.* import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.* import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.whitefish.ring.ui.chart.RecoveryChartWithTimeLabels import com.whitefish.ring.ui.chart.RecoveryChartWithTimeLabels
import com.whitefish.ring.ui.components.TopNavigationBar import com.whitefish.ring.ui.chart.RecoveryStateCard
import com.whitefish.ring.ui.components.systemBarsPadding
import com.whitefish.ring.ui.components.CircularProgressCard import com.whitefish.ring.ui.components.CircularProgressCard
import com.whitefish.ring.ui.components.TopNavigationBar
import org.jetbrains.compose.ui.tooling.preview.Preview import org.jetbrains.compose.ui.tooling.preview.Preview
@Composable @Composable
@ -20,21 +22,14 @@ fun RecoveryScreen(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: RecoveryViewModel = viewModel { RecoveryViewModel() } viewModel: RecoveryViewModel = viewModel { RecoveryViewModel() }
) { ) {
Column( val states by viewModel.uiState.collectAsState()
LazyColumn(
modifier = modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
.background(
brush = Brush.verticalGradient(
colors = listOf(
Color(0xFF6B73FF).copy(alpha = 0.1f),
Color(0xFF9DD5EA).copy(alpha = 0.05f),
MaterialTheme.colorScheme.background
)
)
)
.systemBarsPadding()
) { ) {
// 顶部导航栏 // 顶部导航栏
item {
TopNavigationBar( TopNavigationBar(
title = "恢复得分", title = "恢复得分",
month = 7, month = 7,
@ -43,20 +38,31 @@ fun RecoveryScreen(
// TODO: 实现导航逻辑 // TODO: 实现导航逻辑
} }
) )
}
// 圆弧进度卡片 // 圆弧进度卡片
item {
CircularProgressCard( CircularProgressCard(
currentMinutes = 75, currentMinutes = 75,
totalMinutes = 120, // 假设总时长为120分钟 totalMinutes = 120, // 假设总时长为120分钟
completionTime = "预计7/15日18:36后完全恢复" completionTime = "预计7/15日18:36后完全恢复"
) )
}
item {
RecoveryChartWithTimeLabels( RecoveryChartWithTimeLabels(
modifier = Modifier.fillMaxWidth().height(300.dp), modifier = Modifier.fillMaxWidth().height(300.dp),
actualData = arrayListOf(1.0f, 0.8f, 0.6f, 0.4f, 0.2f, 0.1f,1.0f, 0.8f, 0.6f, 0.4f, 0.2f, 0.1f,1.0f, 0.8f, 0.6f, 0.4f, 0.2f, 0.1f,1.0f, 0.8f, 0.6f, 0.4f, 0.2f, 0.1f), actualData = arrayListOf(1.0f, 0.8f, 0.6f, 0.4f, 0.2f, 0.1f,1.0f, 0.8f, 0.6f, 0.4f, 0.2f, 0.1f,1.0f, 0.8f, 0.6f, 0.4f, 0.2f, 0.1f,1.0f, 0.8f, 0.6f, 0.4f, 0.2f, 0.1f),
predictedData = arrayListOf() predictedData = arrayListOf()
) )
}
item { Spacer(modifier = Modifier.height(7.dp)) }
item {
RecoveryStateCard(
states = states.stateCards
)
}
} }
} }

93
shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/recovery/RecoveryViewModel.kt

@ -1,9 +1,102 @@
package com.whitefish.ring.ui.home.recovery package com.whitefish.ring.ui.home.recovery
import androidx.compose.ui.unit.sp
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.whitefish.ring.bean.ui.convertSecondsToTime
import com.whitefish.ring.data.getHeartRate
import com.whitefish.ring.data.getOximetry
import com.whitefish.ring.data.getSleep
import com.whitefish.ring.data.getTemperature
import com.whitefish.ring.obtainDeviceManager
import com.whitefish.ring.ui.chart.RecoveryStateCardItem
import com.whitefish.ring.ui.chart.RecoveryStateCardType
import com.whitefish.ring.utils.buildStyledText
import com.whitefish.ring.utils.getPastDayStartMillis
import com.whitefish.ring.utils.getTodayEndMillis
import com.whitefish.ring.utils.today
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import ring.shared.generated.resources.Res
import ring.shared.generated.resources.ic_state_heart_rate
import ring.shared.generated.resources.ic_state_oxygen
import ring.shared.generated.resources.ic_state_sleep
import ring.shared.generated.resources.ic_state_temperature
class RecoveryViewModel : ViewModel() { class RecoveryViewModel : ViewModel() {
private val _uiState = MutableStateFlow(RecoveryUiState(emptyList()))
val uiState = _uiState.asStateFlow()
init {
viewModelScope.launch {
obtainDeviceManager().bleReadyStateFlow.collectLatest {
if (it) {
val sleepState = getSleep(getPastDayStartMillis(1), getTodayEndMillis())
val heartRate = getHeartRate(sleepState.start, sleepState.end)
val oximetry = getOximetry(sleepState.start, sleepState.end)
val temperature = getTemperature(sleepState.start, sleepState.end)
_uiState.value = RecoveryUiState(
listOf(
RecoveryStateCardItem(
icon = Res.drawable.ic_state_sleep,
title = "睡眠",
tip = "睡眠质量较好",
value = buildStyledText {
val time = sleepState.totalSeconds.convertSecondsToTime()
append(time.hours.toString(),20.sp)
append("小时",11.sp)
append(time.minutes.toString(),20.sp)
append("分钟",11.sp)
},
type = RecoveryStateCardType.Sleep(sleepState)
),
RecoveryStateCardItem(
icon = Res.drawable.ic_state_heart_rate,
title = "心率",
tip = "睡眠质量较好",
value = buildStyledText {
append("${heartRate.lastOrNull()?.value}", 20.sp)
append("次/分钟", 11.sp)
},
type = RecoveryStateCardType.HeartRate(heartRate.map { it.value })
),
RecoveryStateCardItem(
icon = Res.drawable.ic_state_oxygen,
title = "血氧",
tip = "血氧水平正常",
value = buildStyledText {
append("${oximetry.lastOrNull()?.value}", 20.sp)
append("%", 11.sp)
},
type = RecoveryStateCardType.Oximetry(oximetry.map { it.value }
)
),
RecoveryStateCardItem(
icon = Res.drawable.ic_state_temperature,
title = "体温",
tip = "体温水平正常",
value = buildStyledText {
append("${temperature.lastOrNull()?.value}", 20.sp)
append("°C", 11.sp)
},
type = RecoveryStateCardType.Temperature(temperature.map { it.value })
)
)
)
}
}
}
}
} }
data class RecoveryUiState(
val stateCards: List<RecoveryStateCardItem>
)

129
shared/src/commonMain/kotlin/com/whitefish/ring/utils/StyledTextBuilder.kt

@ -0,0 +1,129 @@
package com.whitefish.ring.utils
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.sp
/**
*
* 便AnnotatedString
*/
class StyledTextBuilder {
private val builder = AnnotatedString.Builder()
/**
*
*/
fun append(text: String): StyledTextBuilder {
builder.append(text)
return this
}
/**
*
*/
fun append(
text: String,
fontSize: TextUnit
): StyledTextBuilder {
builder.withStyle(
style = SpanStyle(fontSize = fontSize)
) {
append(text)
}
return this
}
/**
*
*/
fun append(
text: String,
fontSize: TextUnit,
color: Color
): StyledTextBuilder {
builder.withStyle(
style = SpanStyle(
fontSize = fontSize,
color = color
)
) {
append(text)
}
return this
}
/**
*
*/
fun append(
text: String,
fontSize: TextUnit = TextUnit.Unspecified,
color: Color = Color.Unspecified,
fontWeight: FontWeight? = null,
fontStyle: FontStyle? = null
): StyledTextBuilder {
builder.withStyle(
style = SpanStyle(
fontSize = fontSize,
color = color,
fontWeight = fontWeight,
fontStyle = fontStyle
)
) {
append(text)
}
return this
}
/**
*
*/
fun appendLarge(text: String, color: Color = Color.Unspecified): StyledTextBuilder {
return append(text, 24.sp, color)
}
/**
*
*/
fun appendMedium(text: String, color: Color = Color.Unspecified): StyledTextBuilder {
return append(text, 18.sp, color)
}
/**
*
*/
fun appendSmall(text: String, color: Color = Color.Unspecified): StyledTextBuilder {
return append(text, 14.sp, color)
}
/**
*
*/
fun appendBold(text: String, fontSize: TextUnit = TextUnit.Unspecified): StyledTextBuilder {
return append(text, fontSize, Color.Unspecified, FontWeight.Bold)
}
/**
*
*/
fun appendItalic(text: String, fontSize: TextUnit = TextUnit.Unspecified): StyledTextBuilder {
return append(text, fontSize, Color.Unspecified, null, FontStyle.Italic)
}
/**
* AnnotatedString
*/
fun build(): AnnotatedString {
return builder.toAnnotatedString()
}
}
fun buildStyledText(block: StyledTextBuilder.() -> Unit): AnnotatedString {
return StyledTextBuilder().apply(block).build()
}

83
shared/src/iosMain/kotlin/com/whitefish/ring/data/DeviceDataProvider.ios.kt

@ -3,9 +3,13 @@ package com.whitefish.ring.data
import com.whitefish.app.ui.chart.sleep.SleepSegment import com.whitefish.app.ui.chart.sleep.SleepSegment
import com.whitefish.ring.DeviceManager import com.whitefish.ring.DeviceManager
import com.whitefish.ring.bean.ui.HeartRate import com.whitefish.ring.bean.ui.HeartRate
import com.whitefish.ring.bean.ui.Oximetry
import com.whitefish.ring.bean.ui.SleepState import com.whitefish.ring.bean.ui.SleepState
import com.whitefish.ring.bean.ui.Temperature
import com.whitefish.ring.objc.DBHeartRate import com.whitefish.ring.objc.DBHeartRate
import com.whitefish.ring.objc.DBOxygen
import com.whitefish.ring.objc.DBSleepData import com.whitefish.ring.objc.DBSleepData
import com.whitefish.ring.objc.DBThermemoter
import com.whitefish.ring.objc.StagingListObj import com.whitefish.ring.objc.StagingListObj
import com.whitefish.ring.objc.StagingSubObj import com.whitefish.ring.objc.StagingSubObj
import com.whitefish.ring.obtainDeviceManager import com.whitefish.ring.obtainDeviceManager
@ -69,6 +73,8 @@ actual suspend fun getSleep(
end.timestampToDate().timeIntervalSince1970 end.timestampToDate().timeIntervalSince1970
) { result -> ) { result ->
var totalSeconds = 0 var totalSeconds = 0
var startTime = 0L
var endTime = 0L
result?.let { result?.let {
it.forEach { it.forEach {
@ -94,13 +100,17 @@ actual suspend fun getSleep(
sleepDataList?.let { dataList -> sleepDataList?.let { dataList ->
dataList.forEach { sleepData -> dataList.forEach { sleepData ->
if (!sleepData.isNap) { if (!sleepData.isNap) {
startTime = (sleepData.stagingData.startTime * 1000).toLong() // 转换为毫秒
endTime = (sleepData.stagingData.endTime * 1000).toLong() // 转换为毫秒
// 处理睡眠分期数据 // 处理睡眠分期数据
sleepData.stagingData.ousideStagingList?.let { stagingList -> sleepData.stagingData.ousideStagingList?.let { stagingList ->
for (i in 0 until stagingList.size) { for (i in 0 until stagingList.size) {
val stagingObj = stagingList[i] as StagingSubObj val stagingObj = stagingList[i] as StagingSubObj
val stageDuration = (stagingObj.list.last() as StagingListObj).time.doubleValue - (stagingObj.list.first() as StagingListObj).time.doubleValue / 60 val stageDuration =
val mappedState = mapSleepStageToState(stagingObj.type.value.toInt()) (stagingObj.list.last() as StagingListObj).time.doubleValue - (stagingObj.list.first() as StagingListObj).time.doubleValue / 60
val mappedState =
mapSleepStageToState(stagingObj.type.value.toInt())
segments.add(SleepSegment(mappedState, stageDuration.toFloat())) segments.add(SleepSegment(mappedState, stageDuration.toFloat()))
} }
} }
@ -108,7 +118,14 @@ actual suspend fun getSleep(
} }
} }
coroutine.resume(SleepState(totalSeconds =totalSeconds,segments = segments)) coroutine.resume(
SleepState(
totalSeconds = totalSeconds,
segments = segments,
start = startTime,
end = endTime
)
)
} }
} }
} }
@ -130,3 +147,63 @@ private fun mapSleepStageToState(originalType: Int): Int {
else -> -1 else -> -1
} }
} }
@OptIn(ExperimentalForeignApi::class)
actual suspend fun getOximetry(
start: Long,
end: Long
): List<Oximetry> = withContext(Dispatchers.IO) {
val macAddress = mac()
return@withContext suspendCoroutine { coroutine ->
macAddress?.let {
DBOxygen.query(
it,
start.timestampToDate().timeIntervalSince1970,
end.timestampToDate().timeIntervalSince1970,
false
) { result ->
if (!result.isNullOrEmpty()) {
val oximetry = result.map { it as DBOxygen }
Napier.i { "oximetry -> ${oximetry}" }
coroutine.resume(oximetry.map {
Oximetry(
it.time.getCurrentTimestamp(),
it.value.floatValue
)
})
}else{
coroutine.resume(emptyList())
}
}
}
}
}
@OptIn(ExperimentalForeignApi::class)
actual suspend fun getTemperature(
start: Long,
end: Long
): List<Temperature> = withContext(Dispatchers.IO) {
val macAddress = mac()
return@withContext suspendCoroutine { coroutine ->
macAddress?.let {
DBThermemoter.queryBy(
it,
start.timestampToDate(),
end.timestampToDate(),
false
) { result, max, min, avg ->
if (!result.isNullOrEmpty()){
val temperatures = result.map { it as DBThermemoter }.map {
Temperature(it.time.getCurrentTimestamp(), it.value.floatValue)
}
coroutine.resume(temperatures)
}else{
coroutine.resume(emptyList())
}
}
}
}
}
Loading…
Cancel
Save