Browse Source

feat: data process

AnranYus 4 weeks ago
parent
commit
6f1cf5ff2d
  1. BIN
      .kotlin/metadata/kotlinCInteropLibraries/shared-iosArm64Cinterop-cinMain-c9Gnmg.klib
  2. BIN
      .kotlin/metadata/kotlinTransformedMetadataLibraries/androidx.datastore-datastore-1.1.7-commonMain-X_H3Ng.klib
  3. BIN
      .kotlin/metadata/kotlinTransformedMetadataLibraries/androidx.datastore-datastore-core-1.1.7-commonMain-3X5hEg.klib
  4. BIN
      .kotlin/metadata/kotlinTransformedMetadataLibraries/androidx.datastore-datastore-core-okio-1.1.7-commonMain-o1WRng.klib
  5. BIN
      .kotlin/metadata/kotlinTransformedMetadataLibraries/androidx.datastore-datastore-preferences-1.1.7-commonMain-f3oI8w.klib
  6. BIN
      .kotlin/metadata/kotlinTransformedMetadataLibraries/androidx.datastore-datastore-preferences-core-1.1.7-commonMain-hildbA.klib
  7. BIN
      .kotlin/metadata/kotlinTransformedMetadataLibraries/com.squareup.okio-okio-3.4.0-commonMain-FFE3Tg.klib
  8. BIN
      .kotlin/metadata/kotlinTransformedMetadataLibraries/io.insert-koin-koin-core-4.0.3-commonMain-LKCCMw.klib
  9. BIN
      .kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.kotlinx-kotlinx-datetime-0.6.2-commonMain-fgfANw.klib
  10. 0
      .kotlin/sessions/kotlin-compiler-7621307791481117302.salive
  11. 1
      androidApp/build.gradle.kts
  12. 1
      androidApp/src/main/java/com/whitefish/ring/android/MainActivity.kt
  13. 3
      iosApp/iosApp/Libs/Modules/DeviceCenter.h
  14. 14
      shared/build.gradle.kts
  15. 56
      shared/src/androidMain/kotlin/com/whitefish/ring/DeviceManager.kt
  16. 1
      shared/src/androidMain/kotlin/com/whitefish/ring/Platform.android.kt
  17. 2
      shared/src/androidMain/kotlin/com/whitefish/ring/bt/BleManager.kt
  18. 19
      shared/src/androidMain/kotlin/com/whitefish/ring/data/DataStore.android.kt
  19. 29
      shared/src/androidMain/kotlin/com/whitefish/ring/data/DeviceDataProvider.android.kt
  20. 23
      shared/src/commonMain/kotlin/com/whitefish/ring/App.kt
  21. 3
      shared/src/commonMain/kotlin/com/whitefish/ring/bean/ui/HeartRate.kt
  22. 9
      shared/src/commonMain/kotlin/com/whitefish/ring/bean/ui/SleepState.kt
  23. 20
      shared/src/commonMain/kotlin/com/whitefish/ring/data/DataStore.kt
  24. 11
      shared/src/commonMain/kotlin/com/whitefish/ring/data/DeviceDataProvider.kt
  25. 14
      shared/src/commonMain/kotlin/com/whitefish/ring/data/MockDataProvider.kt
  26. 13
      shared/src/commonMain/kotlin/com/whitefish/ring/device/IDeviceManager.kt
  27. 10
      shared/src/commonMain/kotlin/com/whitefish/ring/ui/chart/ComposeMultiplatformBasicLineChart.kt
  28. 18
      shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/DeviceScreen.kt
  29. 33
      shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/DeviceViewModel.kt
  30. 37
      shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/GuideNavigationScreen.kt
  31. 6
      shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/GuideScreensPreviews.kt
  32. 41
      shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/NavigationViewModel.kt
  33. 85
      shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/RegisterScreen.kt
  34. 67
      shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/SearchingScreen.kt
  35. 62
      shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/WelcomeScreen.kt
  36. 20
      shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/HomeViewModel.kt
  37. 2
      shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/state/StateScreen.kt
  38. 44
      shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/state/StateViewModel.kt
  39. 26
      shared/src/commonMain/kotlin/com/whitefish/ring/utils/Utils.kt
  40. 87
      shared/src/iosMain/kotlin/com/whitefish/ring/DeviceManager.kt
  41. 38
      shared/src/iosMain/kotlin/com/whitefish/ring/Platform.ios.kt
  42. 33
      shared/src/iosMain/kotlin/com/whitefish/ring/data/DataStore.ios.kt
  43. 123
      shared/src/iosMain/kotlin/com/whitefish/ring/data/DeviceDataProvider.ios.kt
  44. 15
      shared/src/iosMain/kotlin/com/whitefish/ring/utils/Utils.kt

BIN
.kotlin/metadata/kotlinCInteropLibraries/shared-iosArm64Cinterop-cinMain-c9Gnmg.klib

Binary file not shown.

BIN
.kotlin/metadata/kotlinTransformedMetadataLibraries/androidx.datastore-datastore-1.1.7-commonMain-X_H3Ng.klib

Binary file not shown.

BIN
.kotlin/metadata/kotlinTransformedMetadataLibraries/androidx.datastore-datastore-core-1.1.7-commonMain-3X5hEg.klib

Binary file not shown.

BIN
.kotlin/metadata/kotlinTransformedMetadataLibraries/androidx.datastore-datastore-core-okio-1.1.7-commonMain-o1WRng.klib

Binary file not shown.

BIN
.kotlin/metadata/kotlinTransformedMetadataLibraries/androidx.datastore-datastore-preferences-1.1.7-commonMain-f3oI8w.klib

Binary file not shown.

BIN
.kotlin/metadata/kotlinTransformedMetadataLibraries/androidx.datastore-datastore-preferences-core-1.1.7-commonMain-hildbA.klib

Binary file not shown.

BIN
.kotlin/metadata/kotlinTransformedMetadataLibraries/com.squareup.okio-okio-3.4.0-commonMain-FFE3Tg.klib

Binary file not shown.

BIN
.kotlin/metadata/kotlinTransformedMetadataLibraries/io.insert-koin-koin-core-4.0.3-commonMain-LKCCMw.klib

Binary file not shown.

BIN
.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.kotlinx-kotlinx-datetime-0.6.2-commonMain-fgfANw.klib

Binary file not shown.

0
.kotlin/sessions/kotlin-compiler-7621307791481117302.salive

1
androidApp/build.gradle.kts

@ -37,6 +37,7 @@ android {
}
dependencies {
implementation(fileTree("../shared/libs"))
implementation(projects.shared)
implementation(libs.compose.ui)
implementation(libs.compose.ui.tooling.preview)

1
androidApp/src/main/java/com/whitefish/ring/android/MainActivity.kt

@ -24,6 +24,7 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
PermissionManager.permissionChecker = permissionChecker
PermissionManager.checkPermission(this)
(obtainDeviceManager() as DeviceManager).init(this)
setContent {
App()
}

3
iosApp/iosApp/Libs/Modules/DeviceCenter.h

@ -56,8 +56,7 @@ typedef NS_ENUM(NSUInteger, SYNC_FINISH) {
// 受限模式回调
//@property(copy, nonatomic)void (^ _Nullable bindFinishCbk)(BOOL isBindLimit);
-(void)registWithisCustomBleManage:(BOOL)isCustomBleManage
;
-(void)registWithisCustomBleManage:(BOOL)isCustomBleManage;

14
shared/build.gradle.kts

@ -44,15 +44,19 @@ kotlin {
framework {
baseName = "shared"
isStatic = true
freeCompilerArgs += listOf(
"-linker-option", "-L${project.projectDir.parent}/iosApp/iosApp/Libs",
"-linker-option", "-lRingSDK_2.0.2"
freeCompilerArgs +=
listOf(
"-linker-option",
"-L${project.projectDir.parent}/iosApp/iosApp/Libs",
"-linker-option",
"-lRingSDK_2.0.2",
)
}
}
sourceSets {
commonMain.dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.2")
implementation("io.github.aakira:napier:2.7.1")
implementation(compose.runtime)
implementation(compose.foundation)
@ -64,6 +68,10 @@ kotlin {
implementation(libs.androidx.lifecycle.runtimeCompose)
implementation(libs.lifecycle.viewmodel.compose)
implementation(libs.vico.multiplatform)
implementation("androidx.datastore:datastore:1.1.7")
implementation("androidx.datastore:datastore-preferences:1.1.7")
implementation(project.dependencies.platform("io.insert-koin:koin-bom:4.0.3"))
implementation("io.insert-koin:koin-core")
}
commonTest.dependencies {

56
shared/src/androidMain/kotlin/com/whitefish/ring/DeviceManager.kt

@ -32,17 +32,21 @@ class DeviceManager() : IDeviceManager(), OnBleConnectionListener, OnSleepDataLo
const val STATE_DEVICE_CONNECTING = -2
const val STATE_DEVICE_CONNECTED = -1
}
private val context = Application.INSTANTS!!
private val app = Application.INSTANTS!!
private var isRegisterBattery = false
val batteryLevel = MutableLiveData(STATE_DEVICE_DISCONNECTED to 0)
private val sycProgress = MutableLiveData(0)
var isSyncingData: Boolean = false
private lateinit var context: Context
private val scope = CoroutineScope(Dispatchers.IO)
// var homeViewModel: demo.linktop.nexring.ui.HomeViewModel? = null
// var workoutDetailViewModel: demo.linktop.nexring.ui.workout.WorkoutDetailViewModel? = null
init {
fun init(context: Context){
registerCb()
this.context = context
}
override fun onBleState(state: Int) {
bleStateListeners().forEach {
@ -131,18 +135,18 @@ class DeviceManager() : IDeviceManager(), OnBleConnectionListener, OnSleepDataLo
}
fun registerCb() {
context.bleManager.addOnBleConnectionListener(this)
app.bleManager.addOnBleConnectionListener(this)
NexRingManager.get().sleepApi().setOnSleepDataLoadListener(this)
}
fun unregisterCb() {
NexRingManager.get().sleepApi().setOnSleepDataLoadListener(null)
context.bleManager.removeOnBleConnectionListener(this)
app.bleManager.removeOnBleConnectionListener(this)
}
override fun connect(address: String) {
with(context.bleManager) {
with(app.bleManager) {
when (bleState.value) {
BluetoothProfile.STATE_DISCONNECTED -> {
batteryLevel.postValue(STATE_DEVICE_CONNECTING to 0)
@ -171,47 +175,45 @@ class DeviceManager() : IDeviceManager(), OnBleConnectionListener, OnSleepDataLo
}
}
override fun bind() {
override fun bind(onBound: () -> Unit) {
NexRingManager.get()
.deviceApi()
.getBindState {
if (it) {
//todo bind dialog
// AlertDialog.Builder(this@DeviceActivity)
// .setCancelable(false)
// .setTitle(R.string.dialog_title_restricted_mode)
// .setMessage(R.string.dialog_msg_restricted_mode)
// .setNegativeButton(android.R.string.cancel) { _, _ ->
//
// }.setPositiveButton(android.R.string.ok) { _, _ ->
// Logger.i("reset device")
// deviceAdapter.clear()
// NexRingManager.get()
// .deviceApi()
// .factoryReset()
// switchUI(true)
// postDelay {
// Thread.sleep(200)
// DeviceManager.INSTANCE.scan(this@DeviceActivity)
// }
// isConnecting = false
// }.create().show()
AlertDialog.Builder(context)
.setCancelable(false)
.setTitle(R.string.dialog_title_restricted_mode)
.setMessage(R.string.dialog_msg_restricted_mode)
.setNegativeButton(android.R.string.cancel) { _, _ ->
}.setPositiveButton(android.R.string.ok) { _, _ ->
onResetDeviceWhenBind.invoke()
NexRingManager.get()
.deviceApi()
.factoryReset()
}.create().show()
} else {
NexRingManager.get()
.deviceApi()
.bind {
onBound.invoke()
//todo bind result
}
}
}
}
override fun getCurrentMac(): String? {
return app.bleManager.connectedDevice?.address
}
override fun startScan() {
val bluetoothAdapter =
(context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter
if (bluetoothAdapter.isEnabled) {
if (!context.bleManager.isScanning) {
context.bleManager.startScan(20 * 1000L,
if (!app.bleManager.isScanning) {
app.bleManager.startScan(20 * 1000L,
object : OnBleScanCallback {
@SuppressLint("MissingPermission")
override fun onScanning(result: BleDevice) {

1
shared/src/androidMain/kotlin/com/whitefish/ring/Platform.android.kt

@ -65,3 +65,4 @@ private val DeviceInstance = DeviceManager()
actual fun obtainDeviceManager(): IDeviceManager {
return DeviceInstance
}

2
shared/src/androidMain/kotlin/com/whitefish/ring/bt/BleManager.kt

@ -97,6 +97,7 @@ class BleManager(val app: Application) {
var bleState = MutableStateFlow(0)
var connectedDevice: BluetoothDevice? = null
private set
private val _oemStepComplete = MutableStateFlow(false)
val oemStepComplete = _oemStepComplete.asStateFlow()
@ -386,6 +387,7 @@ class BleManager(val app: Application) {
}
}
_oemStepComplete.value = true
Napier.i("OEM_STEP_PROCESS_COMPLETED")
}
}

19
shared/src/androidMain/kotlin/com/whitefish/ring/data/DataStore.android.kt

@ -0,0 +1,19 @@
package com.whitefish.ring.data
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import com.whitefish.ring.Application
private var dataStoreInstance: DataStore<Preferences>? = null
private val lock = Any()
actual fun obtainDataStore(): DataStore<Preferences> = synchronized(lock) {
dataStoreInstance ?: createDataStore {
Application.INSTANTS!!.filesDir.resolve(dataStoreFileName).absolutePath
}.also { dataStoreInstance = it }
}

29
shared/src/androidMain/kotlin/com/whitefish/ring/data/DeviceDataProvider.android.kt

@ -0,0 +1,29 @@
package com.whitefish.ring.data
import com.whitefish.ring.bean.ui.HeartRate
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.withContext
import lib.linktop.nexring.api.NexRingManager
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
actual suspend fun getHeartRate(
start: Long,
end: Long
): List<HeartRate> = withContext(Dispatchers.IO){
val macAddress = mac() // 先挂起获取mac
suspendCoroutine { coroutine ->
macAddress?.let {
NexRingManager.get().sleepApi().getHrList(
it,
start,
end,
) {
val result = it?.second?.map { HeartRate(it.ts,it.value) }
coroutine.resume(result?:emptyList())
}
}
}
}

23
shared/src/commonMain/kotlin/com/whitefish/ring/App.kt

@ -1,23 +1,30 @@
@file:Suppress("ktlint:standard:no-wildcard-imports")
package com.whitefish.ring
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import com.whitefish.ring.ui.guide.DeviceScreen
import com.whitefish.ring.device.IDeviceManager
import com.whitefish.ring.ui.guide.GuideNavigationScreen
import com.whitefish.ring.ui.home.HomeScreen
import org.jetbrains.compose.ui.tooling.preview.Preview
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
@Composable
@Preview
fun App() {
MaterialTheme {
// HomeScreen(
// modifier = Modifier
// )
// DeviceScreen{
//
// }
GuideNavigationScreen()
var guideComplete by remember { mutableStateOf(false) }
if (guideComplete){
HomeScreen()
}else{
GuideNavigationScreen {
guideComplete = true
}
}
}
}

3
shared/src/commonMain/kotlin/com/whitefish/ring/bean/ui/HeartRate.kt

@ -0,0 +1,3 @@
package com.whitefish.ring.bean.ui
data class HeartRate (val time: Long,val value: Int)

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

@ -0,0 +1,9 @@
package com.whitefish.ring.bean.ui
import com.whitefish.app.ui.chart.sleep.SleepSegment
import kotlinx.datetime.Clock
data class SleepState (
val totalSeconds: Int, //秒
val segments: List<SleepSegment>
)

20
shared/src/commonMain/kotlin/com/whitefish/ring/data/DataStore.kt

@ -0,0 +1,20 @@
package com.whitefish.ring.data
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.stringPreferencesKey
import okio.Path.Companion.toPath
expect fun obtainDataStore(): DataStore<Preferences>
internal const val dataStoreFileName = "dice.preferences_pb"
val address = stringPreferencesKey("mac")
fun createDataStore(producePath: () -> String): DataStore<Preferences> =
PreferenceDataStoreFactory.createWithPath(
produceFile = { producePath().toPath() }
)

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

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

14
shared/src/commonMain/kotlin/com/whitefish/ring/data/MockDataProvider.kt

@ -0,0 +1,14 @@
package com.whitefish.ring.data
object MockDataProvider {
fun heartRate(start: Long, end: Long): List<com.whitefish.ring.bean.ui.HeartRate> {
return listOf(
com.whitefish.ring.bean.ui.HeartRate(value = 60, time = start),
com.whitefish.ring.bean.ui.HeartRate(value = 65, time = start + 1000),
com.whitefish.ring.bean.ui.HeartRate(value = 70, time = start + 2000),
com.whitefish.ring.bean.ui.HeartRate(value = 75, time = end - 1000),
com.whitefish.ring.bean.ui.HeartRate(value = 80, time = end)
)
}
}

13
shared/src/commonMain/kotlin/com/whitefish/ring/device/IDeviceManager.kt

@ -1,9 +1,7 @@
package com.whitefish.ring.device
import com.whitefish.ring.bean.ui.Device
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
abstract class IDeviceManager {
@ -15,6 +13,9 @@ abstract class IDeviceManager {
val blePowerState = MutableStateFlow<Boolean>(false) // ios的蓝牙是懒加载的,安卓则无此特性
private val bleStateListeners = arrayListOf<(Int) -> Unit>()
protected var onResetDeviceWhenBind:()-> Unit = {}
protected var autoConnect = false
fun bleStateListeners() = bleStateListeners
@ -22,9 +23,15 @@ abstract class IDeviceManager {
bleStateListeners.add(event)
}
fun setOnResetDeviceOnBind(event:() -> Unit) {
onResetDeviceWhenBind = event
}
abstract fun startScan()
abstract fun stopScan()
abstract fun connect(mac: String)
abstract fun bind()
abstract fun bind(onBound:() -> Unit = {})
abstract fun getCurrentMac(): String?
abstract fun setAutoConnect(enable: Boolean)
}

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

@ -7,6 +7,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import com.patrykandpatrick.vico.multiplatform.cartesian.CartesianChartHost
import com.patrykandpatrick.vico.multiplatform.cartesian.VicoZoomState
import com.patrykandpatrick.vico.multiplatform.cartesian.Zoom
import com.patrykandpatrick.vico.multiplatform.cartesian.data.CartesianChartModelProducer
import com.patrykandpatrick.vico.multiplatform.cartesian.data.lineSeries
import com.patrykandpatrick.vico.multiplatform.cartesian.layer.LineCartesianLayer
@ -17,12 +19,14 @@ import com.patrykandpatrick.vico.multiplatform.common.Fill
import com.patrykandpatrick.vico.multiplatform.common.fill
@Composable
fun ComposeMultiplatformBasicLineChart(modifier: Modifier = Modifier) {
fun ComposeMultiplatformBasicLineChart(data: List<Int>, modifier: Modifier = Modifier) {
val modelProducer = remember { CartesianChartModelProducer() }
LaunchedEffect(Unit) {
modelProducer.runTransaction {
// Learn more: https://patrykandpatrick.com/z5ah6v.
lineSeries { series(13, 8, 7, 12, 0, 1, 15, 14, 0, 11, 6, 12, 0, 11, 12, 11) }
if (data.isNotEmpty()) {
lineSeries { series(data) }
}
}
}
CartesianChartHost(
@ -48,6 +52,6 @@ fun ComposeMultiplatformBasicLineChart(modifier: Modifier = Modifier) {
),
modelProducer = modelProducer,
modifier = modifier,
zoomState = VicoZoomState(false, Zoom.Content,Zoom.Content,Zoom.Content)
)
}

18
shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/DeviceScreen.kt

@ -22,13 +22,19 @@ import com.whitefish.ring.device.IDeviceManager
import org.jetbrains.compose.ui.tooling.preview.Preview
@Composable
fun DeviceScreen(onBind:() -> Unit){
val viewModel: DeviceViewModel = viewModel { DeviceViewModel() }
fun DeviceScreen(
viewModel: DeviceViewModel = viewModel { DeviceViewModel() }, onDeviceReset: () -> Unit,
onBind: () -> Unit
) {
viewModel.init()
viewModel.setOnDeviceReset(onDeviceReset)
val uiState by viewModel.uiState.collectAsState()
val bindState by viewModel.manager.bleReadyStateFlow.collectAsState()
val bindState by viewModel.bindSuccess.collectAsState()
if (bindState) {
onBind.invoke()
return
}
Box(
@ -144,7 +150,7 @@ private fun DeviceItem(device: Device,onClick:(Device)-> Unit) {
@Composable
@Preview
fun Device() {
DeviceScreen{
}
// DeviceScreen {
//
// }
}

33
shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/DeviceViewModel.kt

@ -1,13 +1,12 @@
package com.whitefish.ring.ui.guide
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.datastore.preferences.core.edit
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.whitefish.ring.bean.ui.Device
import com.whitefish.ring.device.IDeviceManager
import com.whitefish.ring.data.address
import com.whitefish.ring.data.mac
import com.whitefish.ring.data.obtainDataStore
import com.whitefish.ring.obtainDeviceManager
import io.github.aakira.napier.Napier
import kotlinx.coroutines.flow.MutableStateFlow
@ -16,7 +15,6 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
class DeviceViewModel : ViewModel() {
// var currentStep by remember { mutableStateOf(GuideStep.WELCOME) }
class UiState(
val deviceList: List<Device> = emptyList()
@ -26,11 +24,11 @@ class DeviceViewModel: ViewModel() {
private val _uiState = MutableStateFlow(UiState())
val uiState = _uiState.asStateFlow()
init {
Napier.i { "DeviceViewModel initializing..." }
val bindSuccess = MutableStateFlow(false)
fun init() {
viewModelScope.launch {
_uiState.value = UiState(emptyList())
launch {
manager.deviceList.collectLatest {
Napier.i { "new device:${it}" }
@ -49,8 +47,17 @@ class DeviceViewModel: ViewModel() {
launch {
manager.bleReadyStateFlow.collectLatest {
Napier.i { "ble ready:${it}" }
if (it){
manager.bind()
if (it && mac().isNullOrEmpty()) {
manager.bind {
bindSuccess.value = true
viewModelScope.launch {
obtainDataStore().edit { settings ->
manager.getCurrentMac()?.let {
settings[address] = it
}
}
}
}
}
}
}
@ -61,4 +68,8 @@ class DeviceViewModel: ViewModel() {
manager.connect(mac)
}
fun setOnDeviceReset(event: () -> Unit) {
manager.setOnResetDeviceOnBind(event)
}
}

37
shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/GuideNavigationScreen.kt

@ -2,32 +2,45 @@ package com.whitefish.ring.ui.guide
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.compose.viewModel
enum class GuideStep {
WELCOME,
REGISTER,
CONNECTION_GUIDE,
DEVICE_LIST,
// WEIGHT_SELECTION, // 开屏提问设置1 - 体重选择
// FITNESS_GOALS, // 开屏提问设置2 - 运动目标
// EXERCISE_PREFERENCES, // 开屏提问设置3 - 运动方式偏好
// FAVORITE_EXERCISES, // 开屏提问设置4 - 喜欢的运动
// REMINDER_SETUP, // 开屏提问设置5 - 运动提醒设置
// PLAN_GENERATED, // 生成计划
PERSONAL_INFO,
WEARING_FINGER,
DOMINANT_HAND,
}
@Composable
fun GuideNavigationScreen(
modifier: Modifier = Modifier,
viewModel: NavigationViewModel = viewModel { NavigationViewModel() },
onGuideComplete: () -> Unit = {},
) {
var currentStep by remember { mutableStateOf(GuideStep.WELCOME) }
val bound by viewModel.bound.collectAsState()
if (bound) {
// 如果已经连接设备,直接跳转到主界面
onGuideComplete()
return
}
when (currentStep) {
GuideStep.WELCOME -> {
WelcomeScreen(
onStartClick = {
currentStep = GuideStep.REGISTER
}
},
)
}
@ -38,7 +51,7 @@ fun GuideNavigationScreen(
if (phoneNumber.isNotEmpty() && verificationCode.isNotEmpty()) {
currentStep = GuideStep.CONNECTION_GUIDE
}
}
},
)
}
@ -46,15 +59,17 @@ fun GuideNavigationScreen(
ConnectionGuideScreen(
onNextClick = {
currentStep = GuideStep.DEVICE_LIST
}
},
)
}
GuideStep.DEVICE_LIST -> {
DeviceScreen{
DeviceScreen(onDeviceReset = {
viewModel.clearCache()
currentStep = GuideStep.CONNECTION_GUIDE
}, onBind = {
currentStep = GuideStep.PERSONAL_INFO
}
})
}
GuideStep.PERSONAL_INFO -> {
@ -70,7 +85,7 @@ fun GuideNavigationScreen(
},
onHeightClick = {
// 这里可以打开身高选择器
}
},
)
}
@ -81,7 +96,7 @@ fun GuideNavigationScreen(
},
onFingerSelected = { position ->
// 保存选择的佩戴位置
}
},
)
}
@ -93,7 +108,7 @@ fun GuideNavigationScreen(
},
onHandSelected = { hand ->
// 保存选择的惯用手
}
},
)
}
}

6
shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/GuideScreensPreviews.kt

@ -62,9 +62,9 @@ fun AllGuideScreensPreview() {
.fillMaxWidth()
.height(600.dp)
) {
DeviceScreen{
}
// DeviceScreen{
//
// }
}
}

41
shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/NavigationViewModel.kt

@ -0,0 +1,41 @@
package com.whitefish.ring.ui.guide
import androidx.datastore.preferences.core.edit
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.whitefish.ring.data.address
import com.whitefish.ring.data.obtainDataStore
import io.github.aakira.napier.Napier
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
class NavigationViewModel: ViewModel() {
private val _bound = MutableStateFlow(false)
val bound = _bound.asStateFlow()
init {
checkAlreadyConnected()
}
fun checkAlreadyConnected() {
viewModelScope.launch {
obtainDataStore().data.map { it[address] }.collectLatest {
_bound.value = !it.isNullOrEmpty()
Napier.i { "connect state:${!it.isNullOrEmpty()}" }
}
}
}
fun clearCache(){
viewModelScope.launch {
obtainDataStore().edit {
it[address] = ""
}
}
}
}

85
shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/RegisterScreen.kt

@ -3,53 +3,52 @@ package com.whitefish.ring.ui.guide
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.jetbrains.compose.ui.tooling.preview.Preview
@Composable
fun RegisterScreen(
onLoginClick: (phoneNumber: String, verificationCode: String) -> Unit = { _, _ -> }
) {
fun RegisterScreen(onLoginClick: (phoneNumber: String, verificationCode: String) -> Unit = { _, _ -> }) {
var phoneNumber by remember { mutableStateOf("") }
var verificationCode by remember { mutableStateOf("") }
var isCodeSent by remember { mutableStateOf(false) }
Box(
modifier = Modifier
modifier =
Modifier
.fillMaxSize()
.background(Color(0xFFF5F5F5))
.background(Color(0xFFF5F5F5)),
) {
Column(
modifier = Modifier
modifier =
Modifier
.fillMaxSize()
.padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally
horizontalAlignment = Alignment.CenterHorizontally,
) {
Spacer(modifier = Modifier.height(120.dp))
// 戒指图片占位符
Box(
modifier = Modifier
modifier =
Modifier
.size(160.dp)
.background(
Color(0xFFE5E5E5),
RoundedCornerShape(80.dp)
RoundedCornerShape(80.dp),
),
contentAlignment = Alignment.Center
contentAlignment = Alignment.Center,
) {
Text(
text = "💍",
fontSize = 64.sp
fontSize = 64.sp,
)
}
@ -57,14 +56,14 @@ fun RegisterScreen(
// 欢迎标题
Column(
horizontalAlignment = Alignment.CenterHorizontally
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = "Hi,",
fontSize = 28.sp,
fontWeight = FontWeight.Medium,
color = Color(0xFF333333),
textAlign = TextAlign.Center
textAlign = TextAlign.Center,
)
Text(
@ -72,7 +71,7 @@ fun RegisterScreen(
fontSize = 28.sp,
fontWeight = FontWeight.Medium,
color = Color(0xFF333333),
textAlign = TextAlign.Center
textAlign = TextAlign.Center,
)
}
@ -80,14 +79,14 @@ fun RegisterScreen(
// 手机号输入框
Column(
modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth(),
) {
Text(
text = "手机号",
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
color = Color(0xFF333333),
modifier = Modifier.padding(bottom = 8.dp)
modifier = Modifier.padding(bottom = 8.dp),
)
OutlinedTextField(
@ -96,18 +95,19 @@ fun RegisterScreen(
placeholder = {
Text(
text = "请输入您的手机号",
color = Color(0xFF999999)
color = Color(0xFF999999),
)
},
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = OutlinedTextFieldDefaults.colors(
colors =
OutlinedTextFieldDefaults.colors(
focusedBorderColor = Color(0xFF007AFF),
unfocusedBorderColor = Color(0xFFE5E5E5),
focusedContainerColor = Color.White,
unfocusedContainerColor = Color.White
unfocusedContainerColor = Color.White,
),
singleLine = true
singleLine = true,
)
}
@ -115,19 +115,19 @@ fun RegisterScreen(
// 验证码输入框
Column(
modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth(),
) {
Text(
text = "验证码",
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
color = Color(0xFF333333),
modifier = Modifier.padding(bottom = 8.dp)
modifier = Modifier.padding(bottom = 8.dp),
)
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
verticalAlignment = Alignment.CenterVertically,
) {
OutlinedTextField(
value = verificationCode,
@ -135,18 +135,19 @@ fun RegisterScreen(
placeholder = {
Text(
text = "请输入验证码",
color = Color(0xFF999999)
color = Color(0xFF999999),
)
},
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(12.dp),
colors = OutlinedTextFieldDefaults.colors(
colors =
OutlinedTextFieldDefaults.colors(
focusedBorderColor = Color(0xFF007AFF),
unfocusedBorderColor = Color(0xFFE5E5E5),
focusedContainerColor = Color.White,
unfocusedContainerColor = Color.White
unfocusedContainerColor = Color.White,
),
singleLine = true
singleLine = true,
)
Spacer(modifier = Modifier.width(12.dp))
@ -157,12 +158,12 @@ fun RegisterScreen(
isCodeSent = true
}
},
enabled = phoneNumber.isNotEmpty()
enabled = phoneNumber.isNotEmpty(),
) {
Text(
text = if (isCodeSent) "重新发送验证码" else "获取验证码",
fontSize = 14.sp,
color = if (phoneNumber.isNotEmpty()) Color(0xFF007AFF) else Color(0xFF999999)
color = if (phoneNumber.isNotEmpty()) Color(0xFF007AFF) else Color(0xFF999999),
)
}
}
@ -176,7 +177,7 @@ fun RegisterScreen(
fontSize = 12.sp,
color = Color(0xFF999999),
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(40.dp))
@ -185,20 +186,26 @@ fun RegisterScreen(
Button(
onClick = { onLoginClick(phoneNumber, verificationCode) },
enabled = phoneNumber.isNotEmpty() && verificationCode.isNotEmpty(),
modifier = Modifier
modifier =
Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(28.dp),
colors = ButtonDefaults.buttonColors(
containerColor = if (phoneNumber.isNotEmpty() && verificationCode.isNotEmpty())
Color(0xFF007AFF) else Color(0xFFCCCCCC),
contentColor = Color.White
)
colors =
ButtonDefaults.buttonColors(
containerColor =
if (phoneNumber.isNotEmpty() && verificationCode.isNotEmpty()) {
Color(0xFF007AFF)
} else {
Color(0xFFCCCCCC)
},
contentColor = Color.White,
),
) {
Text(
text = "登录",
fontSize = 18.sp,
fontWeight = FontWeight.Medium
fontWeight = FontWeight.Medium,
)
}

67
shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/SearchingScreen.kt

@ -20,17 +20,18 @@ import org.jetbrains.compose.ui.tooling.preview.Preview
@Composable
fun SearchTip(
onDeviceNotFoundClick: () -> Unit = {},
onDeviceFound: () -> Unit = {}
onDeviceFound: () -> Unit = {},
) {
// 旋转动画
val infiniteTransition = rememberInfiniteTransition()
val rotation by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animationSpec =
infiniteRepeatable(
animation = tween(2000, easing = LinearEasing),
repeatMode = RepeatMode.Restart
)
repeatMode = RepeatMode.Restart,
),
)
// 模拟搜索过程,5秒后跳转到设备列表
@ -40,15 +41,17 @@ fun SearchTip(
}
Box(
modifier = Modifier
modifier =
Modifier
.fillMaxSize()
.background(Color(0xFFF5F5F5))
.background(Color(0xFFF5F5F5)),
) {
Column(
modifier = Modifier
modifier =
Modifier
.fillMaxSize()
.padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally
horizontalAlignment = Alignment.CenterHorizontally,
) {
Spacer(modifier = Modifier.height(120.dp))
@ -59,62 +62,65 @@ fun SearchTip(
fontWeight = FontWeight.Medium,
color = Color(0xFF333333),
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(80.dp))
// 搜索动画
Box(
modifier = Modifier
modifier =
Modifier
.size(200.dp)
.background(
Color.White,
RoundedCornerShape(100.dp)
RoundedCornerShape(100.dp),
),
contentAlignment = Alignment.Center
contentAlignment = Alignment.Center,
) {
// 外圆环 - 旋转动画
Box(
modifier = Modifier
modifier =
Modifier
.size(160.dp)
.rotate(rotation)
.background(
Color.Transparent
Color.Transparent,
),
contentAlignment = Alignment.Center
contentAlignment = Alignment.Center,
) {
// 虚线圆环效果
repeat(8) { index ->
Box(
modifier = Modifier
modifier =
Modifier
.size(8.dp)
.background(
Color(0xFF007AFF),
RoundedCornerShape(4.dp)
)
.offset(
RoundedCornerShape(4.dp),
).offset(
x = (70 * kotlin.math.cos(index * 45.0 * kotlin.math.PI / 180)).dp,
y = (70 * kotlin.math.sin(index * 45.0 * kotlin.math.PI / 180)).dp
)
y = (70 * kotlin.math.sin(index * 45.0 * kotlin.math.PI / 180)).dp,
),
)
}
}
// 中心搜索图标
Box(
modifier = Modifier
modifier =
Modifier
.size(80.dp)
.background(
Color(0xFF007AFF),
RoundedCornerShape(40.dp)
RoundedCornerShape(40.dp),
),
contentAlignment = Alignment.Center
contentAlignment = Alignment.Center,
) {
Text(
text = "🔍",
fontSize = 32.sp,
color = Color.White
color = Color.White,
)
}
}
@ -123,11 +129,12 @@ fun SearchTip(
// 进度指示器
LinearProgressIndicator(
modifier = Modifier
modifier =
Modifier
.fillMaxWidth()
.height(4.dp),
color = Color(0xFF007AFF),
trackColor = Color(0xFFE5E5E5)
trackColor = Color(0xFFE5E5E5),
)
Spacer(modifier = Modifier.height(24.dp))
@ -138,7 +145,7 @@ fun SearchTip(
fontSize = 16.sp,
color = Color(0xFF666666),
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth(),
)
Spacer(modifier = Modifier.weight(1f))
@ -146,13 +153,13 @@ fun SearchTip(
// 找不到设备链接
TextButton(
onClick = onDeviceNotFoundClick,
modifier = Modifier.padding(bottom = 40.dp)
modifier = Modifier.padding(bottom = 40.dp),
) {
Text(
text = "找不到设备?",
fontSize = 16.sp,
color = Color(0xFF007AFF),
textDecoration = TextDecoration.Underline
textDecoration = TextDecoration.Underline,
)
}
}

62
shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/WelcomeScreen.kt

@ -1,6 +1,5 @@
package com.whitefish.ring.ui.guide
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
@ -16,21 +15,21 @@ import androidx.compose.ui.unit.sp
import org.jetbrains.compose.ui.tooling.preview.Preview
@Composable
fun WelcomeScreen(
onStartClick: () -> Unit = {}
) {
fun WelcomeScreen(onStartClick: () -> Unit = {}) {
var isChecked by remember { mutableStateOf(false) }
Box(
modifier = Modifier
modifier =
Modifier
.fillMaxSize()
.background(Color(0xFFF5F5F5))
.background(Color(0xFFF5F5F5)),
) {
Column(
modifier = Modifier
modifier =
Modifier
.fillMaxSize()
.padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally
horizontalAlignment = Alignment.CenterHorizontally,
) {
Spacer(modifier = Modifier.height(120.dp))
@ -40,7 +39,7 @@ fun WelcomeScreen(
fontSize = 48.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF333333),
textAlign = TextAlign.Center
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(24.dp))
@ -51,7 +50,7 @@ fun WelcomeScreen(
fontSize = 18.sp,
fontWeight = FontWeight.Normal,
color = Color(0xFF666666),
textAlign = TextAlign.Center
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(16.dp))
@ -62,24 +61,25 @@ fun WelcomeScreen(
fontSize = 16.sp,
fontWeight = FontWeight.Normal,
color = Color(0xFF999999),
textAlign = TextAlign.Center
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.weight(1f))
// 戒指图片占位符
Box(
modifier = Modifier
modifier =
Modifier
.size(200.dp)
.background(
Color(0xFFE5E5E5),
RoundedCornerShape(16.dp)
RoundedCornerShape(16.dp),
),
contentAlignment = Alignment.Center
contentAlignment = Alignment.Center,
) {
Text(
text = "💍",
fontSize = 80.sp
fontSize = 80.sp,
)
}
@ -87,17 +87,19 @@ fun WelcomeScreen(
// 协议同意checkbox
Row(
modifier = Modifier
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
verticalAlignment = Alignment.CenterVertically,
) {
Checkbox(
checked = isChecked,
onCheckedChange = { isChecked = it },
colors = CheckboxDefaults.colors(
checkedColor = Color(0xFF007AFF)
)
colors =
CheckboxDefaults.colors(
checkedColor = Color(0xFF007AFF),
),
)
Spacer(modifier = Modifier.width(8.dp))
@ -105,25 +107,25 @@ fun WelcomeScreen(
Text(
text = "我已阅读并同意",
fontSize = 14.sp,
color = Color(0xFF666666)
color = Color(0xFF666666),
)
Text(
text = "《用户协议》",
fontSize = 14.sp,
color = Color(0xFF007AFF)
color = Color(0xFF007AFF),
)
Text(
text = "",
fontSize = 14.sp,
color = Color(0xFF666666)
color = Color(0xFF666666),
)
Text(
text = "《隐私政策》",
fontSize = 14.sp,
color = Color(0xFF007AFF)
color = Color(0xFF007AFF),
)
}
@ -133,19 +135,21 @@ fun WelcomeScreen(
Button(
onClick = onStartClick,
enabled = isChecked,
modifier = Modifier
modifier =
Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(28.dp),
colors = ButtonDefaults.buttonColors(
colors =
ButtonDefaults.buttonColors(
containerColor = if (isChecked) Color(0xFF007AFF) else Color(0xFFCCCCCC),
contentColor = Color.White
)
contentColor = Color.White,
),
) {
Text(
text = "立即使用",
fontSize = 18.sp,
fontWeight = FontWeight.Medium
fontWeight = FontWeight.Medium,
)
}

20
shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/HomeViewModel.kt

@ -2,8 +2,13 @@ package com.whitefish.ring.ui.home
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.whitefish.ring.data.MockDataProvider
import com.whitefish.ring.data.getHeartRate
import com.whitefish.ring.data.mac
import com.whitefish.ring.device.IDeviceManager
import com.whitefish.ring.obtainDeviceManager
import com.whitefish.ring.utils.nowMilliseconds
import com.whitefish.ring.utils.today
import io.github.aakira.napier.Napier
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
@ -11,6 +16,10 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.datetime.TimeZone
import kotlinx.datetime.atTime
import kotlinx.datetime.toInstant
import kotlin.time.Duration.Companion.milliseconds
data class HomeUiState(
val selectedTab: HomeTab = HomeTab.STATE,
@ -28,6 +37,11 @@ class HomeViewModel : ViewModel() {
viewModelScope.launch {
launch {
manager.blePowerState.collectLatest {
if (!mac().isNullOrEmpty()){
manager.setAutoConnect(true)
}
if (it) {
Napier.i { "start scan" }
manager.startScan()
@ -35,12 +49,6 @@ class HomeViewModel : ViewModel() {
}
}
launch {
manager.deviceList.collectLatest {
Napier.i { "deviceList:${it}" }
}
}
}
}

2
shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/state/StateScreen.kt

@ -105,7 +105,7 @@ private fun StateCard(
)
}
when (val type = card.type) {
is StateCardType.HeartRate -> ComposeMultiplatformBasicLineChart()
is StateCardType.HeartRate -> ComposeMultiplatformBasicLineChart(type.data)
is StateCardType.SleepState -> SleepChart(
type.data,
modifier = Modifier.fillMaxSize()

44
shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/state/StateViewModel.kt

@ -1,12 +1,27 @@
package com.whitefish.ring.ui.home.state
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.whitefish.app.ui.chart.sleep.createSampleSleepData
import com.whitefish.ring.data.MockDataProvider
import com.whitefish.ring.data.getHeartRate
import com.whitefish.ring.data.getSleep
import com.whitefish.ring.obtainDeviceManager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import com.whitefish.ring.ui.home.state.components.ExerciseGoalData
import com.whitefish.ring.ui.home.state.components.RecoveryScoreData
import com.whitefish.ring.utils.getTodayEndMillis
import com.whitefish.ring.utils.getTodayStartMillis
import com.whitefish.ring.utils.nowMilliseconds
import com.whitefish.ring.utils.today
import io.github.aakira.napier.Napier
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.datetime.TimeZone
import kotlinx.datetime.atTime
import kotlinx.datetime.toInstant
data class StateUiState(
val stateCards: List<StateCardData> = emptyList(),
@ -22,10 +37,30 @@ class StateViewModel : ViewModel() {
init {
loadStateData()
viewModelScope.launch {
obtainDeviceManager().bleReadyStateFlow.collectLatest {
if (it){
val heartRates = getHeartRate(
today().atTime(0, 0).toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds(),
nowMilliseconds()
)
Napier.i { "heart rates:${heartRates}" }
_uiState.value.copy(stateCards = _uiState.value.stateCards.map { card ->
if (card.type is StateCardType.HeartRate) {
card.copy(type = StateCardType.HeartRate(heartRates.map { it.value }))
} else {
card
}
}).also { newState ->
_uiState.value = newState
}
getSleep(getTodayStartMillis(), getTodayEndMillis())
}
}
}
}
private fun loadStateData() {
// 模拟加载状态数据,对应Android StateFragment的数据
val mockData = listOf(
StateCardData(
title = "心率",
@ -45,11 +80,4 @@ class StateViewModel : ViewModel() {
_uiState.value = _uiState.value.copy(stateCards = mockData)
}
fun refresh() {
_uiState.value = _uiState.value.copy(isLoading = true)
// 模拟刷新数据
loadStateData()
_uiState.value = _uiState.value.copy(isLoading = false)
}
}

26
shared/src/commonMain/kotlin/com/whitefish/ring/utils/Utils.kt

@ -0,0 +1,26 @@
package com.whitefish.ring.utils
import kotlinx.datetime.Clock
import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.UtcOffset
import kotlinx.datetime.atStartOfDayIn
import kotlinx.datetime.atTime
import kotlinx.datetime.minus
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
fun nowMilliseconds(): Long = Clock.System.now().toEpochMilliseconds()
fun today() = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date
fun getTodayStartMillis(): Long {
return today().atTime(0,0).toInstant(UtcOffset.ZERO).toEpochMilliseconds()
}
fun getTodayEndMillis(): Long {
return today().atTime(23, 59, 59, 999999999).toInstant(UtcOffset.ZERO).toEpochMilliseconds()
}

87
shared/src/iosMain/kotlin/com/whitefish/ring/DeviceManager.kt

@ -1,14 +1,16 @@
package com.whitefish.ring
import androidx.compose.ui.util.fastFirstOrNull
import androidx.datastore.preferences.core.edit
import com.whitefish.ring.bean.ui.Device
import com.whitefish.ring.data.address
import com.whitefish.ring.data.mac
import com.whitefish.ring.data.obtainDataStore
import com.whitefish.ring.device.IDeviceManager
import com.whitefish.ring.objc.CMD_EXECTE_ERROR_REASON
import com.whitefish.ring.objc.DeviceCenter
import com.whitefish.ring.objc.EXCUTED_CMD
import com.whitefish.ring.objc.FUNCTION_ERROR
import com.whitefish.ring.objc.LTSRingSDK
import com.whitefish.ring.objc.OusideBleDiscovery
import com.whitefish.ring.objc.SRBLeService
import com.whitefish.ring.objc.SRBleDataProtocalProtocol
import com.whitefish.ring.objc.SRBleScanProtocalProtocol
@ -27,10 +29,16 @@ import platform.darwin.NSUInteger
@OptIn(ExperimentalForeignApi::class)
class DeviceManager : IDeviceManager() {
private val manager = DeviceCenter.instance()
companion object {
val INSTANCE = DeviceCenter.instance()
}
private val sdk = INSTANCE.sdk
private var iosBleList = arrayListOf<SRBLeService>()
private val scope = CoroutineScope(Dispatchers.IO)
private var isBinded = false
private var autoConnecting = false
// 将delegate对象存储为强引用的成员变量,避免被垃圾回收
private val scanDelegate = object : NSObject(), SRBleScanProtocalProtocol {
@ -60,6 +68,16 @@ class DeviceManager: IDeviceManager() {
Device(device.advDataLocalName.toString(), device.macAddress.toString())
}
_deviceList.value = deviceList
scope.launch {
val address = mac()
if (autoConnect && !address.isNullOrEmpty() && !autoConnecting) {
autoConnecting = true
connect(address)
Napier.i { "Auto connect:${address}" }
}
}
}
}
@ -133,11 +151,35 @@ class DeviceManager: IDeviceManager() {
}
override fun srBleIsbinded(isBinded: Boolean) {
scope.launch {
if (isBinded && mac().isNullOrEmpty()) {
showNativeAlert(
title = "设备已绑定",
message = "当前设备已绑定,是否恢复出厂设置?",
onConfirm = {
sdk?.functionDeviceReset()
onResetDeviceWhenBind.invoke()
scope.launch {
obtainDataStore().edit { settings ->
settings[address] = ""
}
}
this@DeviceManager.isBinded = false
},
onCancel = {}
)
}
this@DeviceManager.isBinded = isBinded
}
Napier.i { "srBleIsbinded:${isBinded}" }
}
override fun srBleOEMAuthResult(authSucceddful: Boolean) {
bleReadyStateFlow.value = true
autoConnecting = false
Napier.i { "srBleOEMAuthResult" }
}
override fun srBleFunctionErrorCallBack(
@ -153,37 +195,42 @@ class DeviceManager: IDeviceManager() {
private fun initializeManager() {
Napier.i { "DeviceManager initializing..." }
manager.registWithisCustomBleManage(true)
INSTANCE.registWithisCustomBleManage(true)
// 使用成员变量而不是匿名对象
manager.appScanDelegate = scanDelegate
manager.appDataDelegate = dataDelegate
INSTANCE.appScanDelegate = scanDelegate
INSTANCE.appDataDelegate = dataDelegate
Napier.i { "DeviceManager delegates set: scan=${scanDelegate}, data=${dataDelegate}" }
}
// 添加重新初始化方法,在需要时可以调用
fun reinitialize() {
Napier.i { "DeviceManager reinitializing..." }
initializeManager()
}
override fun startScan() {
Napier.i { "Starting scan, delegate: ${manager.appScanDelegate}" }
manager.startBleScan()
Napier.i { "Starting scan, delegate: ${INSTANCE.appScanDelegate}" }
INSTANCE.startBleScan()
}
override fun stopScan() {
manager.stopBleScan()
INSTANCE.stopBleScan()
}
override fun connect(mac: String) {
iosBleList.fastFirstOrNull { it.macAddress == mac }?.let {
manager.connectDevice(it)
Napier.i { "connect device:${it}" }
INSTANCE.connectDevice(it)
}
}
override fun bind() {
Napier.i { "bind device:${manager.currentDevice()}" }
manager.bindCurrentDevice()
override fun bind(onBound: () -> Unit) {
if (!isBinded) {
Napier.i { "bind device:${INSTANCE.currentDevice()}" }
INSTANCE.bindCurrentDevice()
onBound.invoke()
}
}
override fun getCurrentMac(): String? {
return INSTANCE.currentDevice().macAddress
}
override fun setAutoConnect(enable: Boolean) {
autoConnect = enable
}
}

38
shared/src/iosMain/kotlin/com/whitefish/ring/Platform.ios.kt

@ -6,6 +6,12 @@ import androidx.compose.ui.unit.dp
import com.whitefish.ring.device.IDeviceManager
import io.github.aakira.napier.DebugAntilog
import io.github.aakira.napier.Napier
import platform.UIKit.UIAlertAction
import platform.UIKit.UIAlertActionStyleCancel
import platform.UIKit.UIAlertActionStyleDefault
import platform.UIKit.UIAlertController
import platform.UIKit.UIAlertControllerStyleAlert
import platform.UIKit.UIApplication
import platform.UIKit.UIDevice
class IOSPlatform: Platform {
@ -65,3 +71,35 @@ fun initLogger(){
Napier.base(DebugAntilog())
Napier.i { "Logger init success on ios" }
}
fun showNativeAlert(
title: String,
message: String,
onConfirm: () -> Unit,
onCancel: () -> Unit
) {
val alert = UIAlertController.alertControllerWithTitle(
title = title,
message = message,
preferredStyle = UIAlertControllerStyleAlert
)
// 确认按钮
val confirmAction = UIAlertAction.actionWithTitle(
title = "确认",
style = UIAlertActionStyleDefault
) { _ -> onConfirm() }
// 取消按钮
val cancelAction = UIAlertAction.actionWithTitle(
title = "取消",
style = UIAlertActionStyleCancel
) { _ -> onCancel() }
alert.addAction(confirmAction)
alert.addAction(cancelAction)
// 获取当前视图控制器并展示
val rootViewController = UIApplication.sharedApplication.keyWindow?.rootViewController
rootViewController?.presentViewController(alert, animated = true, completion = null)
}

33
shared/src/iosMain/kotlin/com/whitefish/ring/data/DataStore.ios.kt

@ -0,0 +1,33 @@
package com.whitefish.ring.data
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import co.touchlab.stately.concurrency.Synchronizable
import com.whitefish.ring.data.createDataStore
import kotlinx.atomicfu.locks.SynchronizedObject
import kotlinx.atomicfu.locks.synchronized
import kotlinx.cinterop.ExperimentalForeignApi
import org.koin.mp.Lockable
import platform.Foundation.NSDocumentDirectory
import platform.Foundation.NSFileManager
import platform.Foundation.NSURL
import platform.Foundation.NSUserDomainMask
private var dataStoreInstance: DataStore<Preferences>? = null
private val lock = SynchronizedObject()
@OptIn(ExperimentalForeignApi::class)
actual fun obtainDataStore(): DataStore<Preferences> = synchronized(lock) {
dataStoreInstance ?: createDataStore(
producePath = {
val documentDirectory: NSURL? = NSFileManager.defaultManager.URLForDirectory(
directory = NSDocumentDirectory,
inDomain = NSUserDomainMask,
appropriateForURL = null,
create = false,
error = null,
)
requireNotNull(documentDirectory).path + "/$dataStoreFileName"
}
).also { dataStoreInstance = it }
}

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

@ -0,0 +1,123 @@
package com.whitefish.ring.data
import com.whitefish.app.ui.chart.sleep.SleepSegment
import com.whitefish.ring.bean.ui.HeartRate
import com.whitefish.ring.bean.ui.SleepState
import com.whitefish.ring.objc.DBHeartRate
import com.whitefish.ring.objc.DBSleepData
import com.whitefish.ring.objc.StagingListObj
import com.whitefish.ring.objc.StagingSubObj
import com.whitefish.ring.utils.getCurrentTimestamp
import com.whitefish.ring.utils.timestampToDate
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.withContext
import platform.Foundation.timeIntervalSince1970
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@OptIn(ExperimentalForeignApi::class)
actual suspend fun getHeartRate(start: Long, end: Long): List<HeartRate> =
withContext(Dispatchers.IO) {
val macAddress = mac()
return@withContext suspendCoroutine { coroutine ->
macAddress?.let {
DBHeartRate.queryBy(
it,
start.timestampToDate(),
end.timestampToDate(),
false
) { result, max, min, avg ->
val heartList = result?.map { it as DBHeartRate }
?.map { HeartRate(it.time.getCurrentTimestamp(), it.value.intValue) }
if (heartList.isNullOrEmpty()) {
coroutine.resume(emptyList())
} else {
coroutine.resume(heartList)
}
}
}
}
}
@OptIn(ExperimentalForeignApi::class)
actual suspend fun getSleep(
start: Long,
end: Long
): SleepState = withContext(Dispatchers.IO) {
val macAddress = mac()
return@withContext suspendCoroutine { coroutine ->
macAddress?.let {
val segments = mutableListOf<SleepSegment>()
DBSleepData.queryDbSleepBy(
it, start.timestampToDate().timeIntervalSince1970,
end.timestampToDate().timeIntervalSince1970
) { result ->
val sleepDataList = result?.map { it as DBSleepData }
var totalSeconds = 0
sleepDataList?.let { dataList ->
dataList.forEach { sleepData ->
if (!sleepData.isNap) {
totalSeconds += sleepData.duration.intValue
// 处理睡眠分期数据
sleepData.stagingData?.ousideStagingList?.let { stagingList ->
for (i in 0 until stagingList.size) {
val stagingObj = stagingList[i] as StagingSubObj
// 计算该阶段的持续时间
val duration = if (i == 0) {
// 第一个阶段:从开始到结束
stagingObj.list.let { list ->
val firstTime = (list.first() as? StagingListObj)?.time?.doubleValue ?: 0.0
val lastTime = (list.last() as? StagingListObj)?.time?.doubleValue ?: 0.0
lastTime - firstTime
}
} else {
// 后续阶段:从前一个阶段结束到当前阶段结束
val prevStagingObj = stagingList[i - 1] as StagingSubObj
val currentEndTime = (stagingObj.list.first() as? StagingListObj)?.time?.doubleValue ?: 0.0
val prevEndTime = (prevStagingObj.list.last() as? StagingListObj)?.time?.doubleValue ?: 0.0
currentEndTime - prevEndTime
}
// 转换睡眠状态类型到用户定义的状态
val mappedState = mapSleepStageToState(stagingObj.type.value.toInt())
// 只添加有效的睡眠状态(跳过 NONE 状态)
if (mappedState >= 0) {
val durationMinutes = (duration / 60.0).toFloat()
if (durationMinutes > 0) {
segments.add(SleepSegment(mappedState, durationMinutes))
}
}
}
}
}
}
}
coroutine.resume(SleepState(totalSeconds =totalSeconds, segments = segments ))
}
}
}
}
/**
*
* @param originalType
* @return (0=, 1=, 2=, 3=REM, -1=)
*/
private fun mapSleepStageToState(originalType: Int): Int {
return when (originalType) {
0 -> -1 // NONE - 忽略
1 -> 0 // WAKE - 清醒
2, 3 -> 1 // NREM1, NREM2 - 浅睡
4 -> 2 // NREM3 - 深睡
5 -> 3 // REM - REM睡眠
6 -> -1 // NAP - 忽略(因为已经过滤了 isNap)
else -> -1
}
}

15
shared/src/iosMain/kotlin/com/whitefish/ring/utils/Utils.kt

@ -0,0 +1,15 @@
package com.whitefish.ring.utils
import platform.Foundation.NSDate
import platform.Foundation.NSTimeInterval
import platform.Foundation.dateWithTimeIntervalSince1970
import platform.Foundation.timeIntervalSince1970
fun Long.timestampToDate(): NSDate {
val timeInterval: NSTimeInterval = this / 1000.0
return NSDate.dateWithTimeIntervalSince1970(timeInterval)
}
fun NSDate.getCurrentTimestamp(): Long {
return (this.timeIntervalSince1970 * 1000).toLong()
}
Loading…
Cancel
Save