diff --git a/.kotlin/metadata/kotlinCInteropLibraries/shared-iosArm64Cinterop-cinMain-c9Gnmg.klib b/.kotlin/metadata/kotlinCInteropLibraries/shared-iosArm64Cinterop-cinMain-c9Gnmg.klib new file mode 100644 index 0000000..c1c4549 Binary files /dev/null and b/.kotlin/metadata/kotlinCInteropLibraries/shared-iosArm64Cinterop-cinMain-c9Gnmg.klib differ diff --git a/.kotlin/metadata/kotlinTransformedMetadataLibraries/androidx.datastore-datastore-1.1.7-commonMain-X_H3Ng.klib b/.kotlin/metadata/kotlinTransformedMetadataLibraries/androidx.datastore-datastore-1.1.7-commonMain-X_H3Ng.klib new file mode 100644 index 0000000..0f06696 Binary files /dev/null and b/.kotlin/metadata/kotlinTransformedMetadataLibraries/androidx.datastore-datastore-1.1.7-commonMain-X_H3Ng.klib differ diff --git a/.kotlin/metadata/kotlinTransformedMetadataLibraries/androidx.datastore-datastore-core-1.1.7-commonMain-3X5hEg.klib b/.kotlin/metadata/kotlinTransformedMetadataLibraries/androidx.datastore-datastore-core-1.1.7-commonMain-3X5hEg.klib new file mode 100644 index 0000000..ef815db Binary files /dev/null and b/.kotlin/metadata/kotlinTransformedMetadataLibraries/androidx.datastore-datastore-core-1.1.7-commonMain-3X5hEg.klib differ diff --git a/.kotlin/metadata/kotlinTransformedMetadataLibraries/androidx.datastore-datastore-core-okio-1.1.7-commonMain-o1WRng.klib b/.kotlin/metadata/kotlinTransformedMetadataLibraries/androidx.datastore-datastore-core-okio-1.1.7-commonMain-o1WRng.klib new file mode 100644 index 0000000..c5f8e14 Binary files /dev/null and b/.kotlin/metadata/kotlinTransformedMetadataLibraries/androidx.datastore-datastore-core-okio-1.1.7-commonMain-o1WRng.klib differ diff --git a/.kotlin/metadata/kotlinTransformedMetadataLibraries/androidx.datastore-datastore-preferences-1.1.7-commonMain-f3oI8w.klib b/.kotlin/metadata/kotlinTransformedMetadataLibraries/androidx.datastore-datastore-preferences-1.1.7-commonMain-f3oI8w.klib new file mode 100644 index 0000000..65225f4 Binary files /dev/null and b/.kotlin/metadata/kotlinTransformedMetadataLibraries/androidx.datastore-datastore-preferences-1.1.7-commonMain-f3oI8w.klib differ diff --git a/.kotlin/metadata/kotlinTransformedMetadataLibraries/androidx.datastore-datastore-preferences-core-1.1.7-commonMain-hildbA.klib b/.kotlin/metadata/kotlinTransformedMetadataLibraries/androidx.datastore-datastore-preferences-core-1.1.7-commonMain-hildbA.klib new file mode 100644 index 0000000..6a9de9a Binary files /dev/null and b/.kotlin/metadata/kotlinTransformedMetadataLibraries/androidx.datastore-datastore-preferences-core-1.1.7-commonMain-hildbA.klib differ diff --git a/.kotlin/metadata/kotlinTransformedMetadataLibraries/com.squareup.okio-okio-3.4.0-commonMain-FFE3Tg.klib b/.kotlin/metadata/kotlinTransformedMetadataLibraries/com.squareup.okio-okio-3.4.0-commonMain-FFE3Tg.klib new file mode 100644 index 0000000..cec8d70 Binary files /dev/null and b/.kotlin/metadata/kotlinTransformedMetadataLibraries/com.squareup.okio-okio-3.4.0-commonMain-FFE3Tg.klib differ diff --git a/.kotlin/metadata/kotlinTransformedMetadataLibraries/io.insert-koin-koin-core-4.0.3-commonMain-LKCCMw.klib b/.kotlin/metadata/kotlinTransformedMetadataLibraries/io.insert-koin-koin-core-4.0.3-commonMain-LKCCMw.klib new file mode 100644 index 0000000..83d9408 Binary files /dev/null and b/.kotlin/metadata/kotlinTransformedMetadataLibraries/io.insert-koin-koin-core-4.0.3-commonMain-LKCCMw.klib differ diff --git a/.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.kotlinx-kotlinx-datetime-0.6.2-commonMain-fgfANw.klib b/.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.kotlinx-kotlinx-datetime-0.6.2-commonMain-fgfANw.klib new file mode 100644 index 0000000..015cc1c Binary files /dev/null and b/.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.kotlinx-kotlinx-datetime-0.6.2-commonMain-fgfANw.klib differ diff --git a/.kotlin/sessions/kotlin-compiler-7621307791481117302.salive b/.kotlin/sessions/kotlin-compiler-7621307791481117302.salive deleted file mode 100644 index e69de29..0000000 diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index bb6ab19..ae5f8ee 100644 --- a/androidApp/build.gradle.kts +++ b/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) diff --git a/androidApp/src/main/java/com/whitefish/ring/android/MainActivity.kt b/androidApp/src/main/java/com/whitefish/ring/android/MainActivity.kt index d62964b..d1d2f00 100644 --- a/androidApp/src/main/java/com/whitefish/ring/android/MainActivity.kt +++ b/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() } diff --git a/iosApp/iosApp/Libs/Modules/DeviceCenter.h b/iosApp/iosApp/Libs/Modules/DeviceCenter.h index 8833d5a..152a95d 100644 --- a/iosApp/iosApp/Libs/Modules/DeviceCenter.h +++ b/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; diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 12440f9..da47a1f 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -20,8 +20,8 @@ kotlin { } } iosArm64().apply { - compilations.getByName("main"){ - val cin by cinterops.creating{ + compilations.getByName("main") { + val cin by cinterops.creating { definitionFile.set(project.file("Ring.def")) packageName("com.whitefish.ring.objc") val files = project.fileTree("../iosApp/iosApp/Libs").files.filter { it.extension == "h" } @@ -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 { @@ -78,7 +86,7 @@ android { defaultConfig { minSdk = 29 } - lint{ + lint { disable.add("NullSafeMutableLiveData") } compileOptions { @@ -90,7 +98,7 @@ android { ksp(libs.androidx.room.compiler) implementation(libs.android.database.sqlcipher) implementation(fileTree("libs")) - implementation ("com.google.accompanist:accompanist-permissions:0.37.3") + implementation("com.google.accompanist:accompanist-permissions:0.37.3") } } dependencies { diff --git a/shared/src/androidMain/kotlin/com/whitefish/ring/DeviceManager.kt b/shared/src/androidMain/kotlin/com/whitefish/ring/DeviceManager.kt index 15d2bc4..78901e6 100644 --- a/shared/src/androidMain/kotlin/com/whitefish/ring/DeviceManager.kt +++ b/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) { diff --git a/shared/src/androidMain/kotlin/com/whitefish/ring/Platform.android.kt b/shared/src/androidMain/kotlin/com/whitefish/ring/Platform.android.kt index eacc34c..ec456c2 100644 --- a/shared/src/androidMain/kotlin/com/whitefish/ring/Platform.android.kt +++ b/shared/src/androidMain/kotlin/com/whitefish/ring/Platform.android.kt @@ -65,3 +65,4 @@ private val DeviceInstance = DeviceManager() actual fun obtainDeviceManager(): IDeviceManager { return DeviceInstance } + diff --git a/shared/src/androidMain/kotlin/com/whitefish/ring/bt/BleManager.kt b/shared/src/androidMain/kotlin/com/whitefish/ring/bt/BleManager.kt index 536f62d..85d9a83 100644 --- a/shared/src/androidMain/kotlin/com/whitefish/ring/bt/BleManager.kt +++ b/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") } } diff --git a/shared/src/androidMain/kotlin/com/whitefish/ring/data/DataStore.android.kt b/shared/src/androidMain/kotlin/com/whitefish/ring/data/DataStore.android.kt new file mode 100644 index 0000000..0fcf586 --- /dev/null +++ b/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? = null +private val lock = Any() + +actual fun obtainDataStore(): DataStore = synchronized(lock) { + dataStoreInstance ?: createDataStore { + Application.INSTANTS!!.filesDir.resolve(dataStoreFileName).absolutePath + }.also { dataStoreInstance = it } +} + + + + diff --git a/shared/src/androidMain/kotlin/com/whitefish/ring/data/DeviceDataProvider.android.kt b/shared/src/androidMain/kotlin/com/whitefish/ring/data/DeviceDataProvider.android.kt new file mode 100644 index 0000000..c013587 --- /dev/null +++ b/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 = 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()) + } + } + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/whitefish/ring/App.kt b/shared/src/commonMain/kotlin/com/whitefish/ring/App.kt index 1cb5414..8b2470f 100644 --- a/shared/src/commonMain/kotlin/com/whitefish/ring/App.kt +++ b/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 + } + } } -} \ No newline at end of file +} diff --git a/shared/src/commonMain/kotlin/com/whitefish/ring/bean/ui/HeartRate.kt b/shared/src/commonMain/kotlin/com/whitefish/ring/bean/ui/HeartRate.kt new file mode 100644 index 0000000..d3c2801 --- /dev/null +++ b/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) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/whitefish/ring/bean/ui/SleepState.kt b/shared/src/commonMain/kotlin/com/whitefish/ring/bean/ui/SleepState.kt new file mode 100644 index 0000000..bd7dd5f --- /dev/null +++ b/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 +) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/whitefish/ring/data/DataStore.kt b/shared/src/commonMain/kotlin/com/whitefish/ring/data/DataStore.kt new file mode 100644 index 0000000..9ff35cf --- /dev/null +++ b/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 +internal const val dataStoreFileName = "dice.preferences_pb" + + +val address = stringPreferencesKey("mac") + + +fun createDataStore(producePath: () -> String): DataStore = + PreferenceDataStoreFactory.createWithPath( + produceFile = { producePath().toPath() } + ) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/whitefish/ring/data/DeviceDataProvider.kt b/shared/src/commonMain/kotlin/com/whitefish/ring/data/DeviceDataProvider.kt new file mode 100644 index 0000000..e747a79 --- /dev/null +++ b/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 +expect suspend fun getSleep(start: Long, end: Long): SleepState \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/whitefish/ring/data/MockDataProvider.kt b/shared/src/commonMain/kotlin/com/whitefish/ring/data/MockDataProvider.kt new file mode 100644 index 0000000..f3c676a --- /dev/null +++ b/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 { + 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) + ) + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/whitefish/ring/device/IDeviceManager.kt b/shared/src/commonMain/kotlin/com/whitefish/ring/device/IDeviceManager.kt index 7fd2eec..b5e8a01 100644 --- a/shared/src/commonMain/kotlin/com/whitefish/ring/device/IDeviceManager.kt +++ b/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(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) } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/chart/ComposeMultiplatformBasicLineChart.kt b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/chart/ComposeMultiplatformBasicLineChart.kt index 85c8f2e..6e76671 100644 --- a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/chart/ComposeMultiplatformBasicLineChart.kt +++ b/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, 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) ) } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/DeviceScreen.kt b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/DeviceScreen.kt index e3ad73a..cc90335 100644 --- a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/DeviceScreen.kt +++ b/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){ + if (bindState) { onBind.invoke() + return } Box( @@ -52,20 +58,20 @@ fun DeviceScreen(onBind:() -> Unit){ .padding(top = 60.dp, bottom = 40.dp), textAlign = TextAlign.Center ) - + // 设备列表 LazyColumn( verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.weight(1f) ) { items(uiState.deviceList) { device -> - DeviceItem(device = device){ + DeviceItem(device = device) { viewModel.connect(it.mac) } } } } - + // 底部提示 Text( text = "连接失败?", @@ -79,7 +85,7 @@ fun DeviceScreen(onBind:() -> Unit){ } @Composable -private fun DeviceItem(device: Device,onClick:(Device)-> Unit) { +private fun DeviceItem(device: Device, onClick: (Device) -> Unit) { Card( modifier = Modifier .fillMaxWidth() @@ -115,9 +121,9 @@ private fun DeviceItem(device: Device,onClick:(Device)-> Unit) { fontSize = 24.sp ) } - + Spacer(modifier = Modifier.width(16.dp)) - + // 设备信息 Column( modifier = Modifier.weight(1f) @@ -128,9 +134,9 @@ private fun DeviceItem(device: Device,onClick:(Device)-> Unit) { fontWeight = FontWeight.Medium, color = Color(0xFF333333) ) - + Spacer(modifier = Modifier.height(4.dp)) - + Text( text = "设备号:${device.mac}", fontSize = 14.sp, @@ -143,8 +149,8 @@ private fun DeviceItem(device: Device,onClick:(Device)-> Unit) { @Composable @Preview -fun Device(){ - DeviceScreen{ - - } +fun Device() { +// DeviceScreen { +// +// } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/DeviceViewModel.kt b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/DeviceViewModel.kt index 90c16dc..75e36e0 100644 --- a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/DeviceViewModel.kt +++ b/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 @@ -15,8 +14,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -class DeviceViewModel: ViewModel() { -// var currentStep by remember { mutableStateOf(GuideStep.WELCOME) } +class DeviceViewModel : ViewModel() { class UiState( val deviceList: List = emptyList() @@ -25,12 +23,12 @@ class DeviceViewModel: ViewModel() { val manager = obtainDeviceManager() private val _uiState = MutableStateFlow(UiState()) val uiState = _uiState.asStateFlow() - - init { - Napier.i { "DeviceViewModel initializing..." } - - viewModelScope.launch { + val bindSuccess = MutableStateFlow(false) + + fun init() { + viewModelScope.launch { + _uiState.value = UiState(emptyList()) launch { manager.deviceList.collectLatest { Napier.i { "new device:${it}" } @@ -40,7 +38,7 @@ class DeviceViewModel: ViewModel() { launch { manager.blePowerState.collectLatest { - if (it){ + if (it) { manager.startScan() } } @@ -49,16 +47,29 @@ 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 + } + } + } + } } } } } } - fun connect(mac: String){ + fun connect(mac: String) { manager.connect(mac) } + + fun setOnDeviceReset(event: () -> Unit) { + manager.setOnResetDeviceOnBind(event) + } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/GuideNavigationScreen.kt b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/GuideNavigationScreen.kt index 160a656..4a24d71 100644 --- a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/GuideNavigationScreen.kt +++ b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/GuideNavigationScreen.kt @@ -2,35 +2,48 @@ 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 - } + }, ) } - + GuideStep.REGISTER -> { RegisterScreen( onLoginClick = { phoneNumber, verificationCode -> @@ -38,25 +51,27 @@ fun GuideNavigationScreen( if (phoneNumber.isNotEmpty() && verificationCode.isNotEmpty()) { currentStep = GuideStep.CONNECTION_GUIDE } - } + }, ) } - + GuideStep.CONNECTION_GUIDE -> { 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 -> { PersonalInfoScreen( onNextClick = { @@ -70,10 +85,10 @@ fun GuideNavigationScreen( }, onHeightClick = { // 这里可以打开身高选择器 - } + }, ) } - + GuideStep.WEARING_FINGER -> { WearingFingerScreen( onNextClick = { @@ -81,10 +96,10 @@ fun GuideNavigationScreen( }, onFingerSelected = { position -> // 保存选择的佩戴位置 - } + }, ) } - + GuideStep.DOMINANT_HAND -> { DominantHandScreen( onNextClick = { @@ -93,8 +108,8 @@ fun GuideNavigationScreen( }, onHandSelected = { hand -> // 保存选择的惯用手 - } + }, ) } } -} \ No newline at end of file +} diff --git a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/GuideScreensPreviews.kt b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/GuideScreensPreviews.kt index 9fb1042..44140ed 100644 --- a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/GuideScreensPreviews.kt +++ b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/GuideScreensPreviews.kt @@ -62,9 +62,9 @@ fun AllGuideScreensPreview() { .fillMaxWidth() .height(600.dp) ) { - DeviceScreen{ - - } +// DeviceScreen{ +// +// } } } diff --git a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/NavigationViewModel.kt b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/NavigationViewModel.kt new file mode 100644 index 0000000..dadc876 --- /dev/null +++ b/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] = "" + } + } + + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/RegisterScreen.kt b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/RegisterScreen.kt index e3b7404..6bbcd0f 100644 --- a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/RegisterScreen.kt +++ b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/RegisterScreen.kt @@ -3,131 +3,131 @@ 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 - .fillMaxSize() - .background(Color(0xFFF5F5F5)) + modifier = + Modifier + .fillMaxSize() + .background(Color(0xFFF5F5F5)), ) { Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 24.dp), - horizontalAlignment = Alignment.CenterHorizontally + modifier = + Modifier + .fillMaxSize() + .padding(horizontal = 24.dp), + horizontalAlignment = Alignment.CenterHorizontally, ) { Spacer(modifier = Modifier.height(120.dp)) - + // 戒指图片占位符 Box( - modifier = Modifier - .size(160.dp) - .background( - Color(0xFFE5E5E5), - RoundedCornerShape(80.dp) - ), - contentAlignment = Alignment.Center + modifier = + Modifier + .size(160.dp) + .background( + Color(0xFFE5E5E5), + RoundedCornerShape(80.dp), + ), + contentAlignment = Alignment.Center, ) { Text( text = "💍", - fontSize = 64.sp + fontSize = 64.sp, ) } - + Spacer(modifier = Modifier.height(40.dp)) - + // 欢迎标题 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( text = "欢迎来到Acti", fontSize = 28.sp, fontWeight = FontWeight.Medium, color = Color(0xFF333333), - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) } - + Spacer(modifier = Modifier.height(60.dp)) - + // 手机号输入框 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( value = phoneNumber, onValueChange = { phoneNumber = it }, placeholder = { Text( text = "请输入您的手机号", - color = Color(0xFF999999) + color = Color(0xFF999999), ) }, modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(12.dp), - colors = OutlinedTextFieldDefaults.colors( - focusedBorderColor = Color(0xFF007AFF), - unfocusedBorderColor = Color(0xFFE5E5E5), - focusedContainerColor = Color.White, - unfocusedContainerColor = Color.White - ), - singleLine = true + colors = + OutlinedTextFieldDefaults.colors( + focusedBorderColor = Color(0xFF007AFF), + unfocusedBorderColor = Color(0xFFE5E5E5), + focusedContainerColor = Color.White, + unfocusedContainerColor = Color.White, + ), + singleLine = true, ) } - + Spacer(modifier = Modifier.height(24.dp)) - + // 验证码输入框 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,73 +135,80 @@ fun RegisterScreen( placeholder = { Text( text = "请输入验证码", - color = Color(0xFF999999) + color = Color(0xFF999999), ) }, modifier = Modifier.weight(1f), shape = RoundedCornerShape(12.dp), - colors = OutlinedTextFieldDefaults.colors( - focusedBorderColor = Color(0xFF007AFF), - unfocusedBorderColor = Color(0xFFE5E5E5), - focusedContainerColor = Color.White, - unfocusedContainerColor = Color.White - ), - singleLine = true + colors = + OutlinedTextFieldDefaults.colors( + focusedBorderColor = Color(0xFF007AFF), + unfocusedBorderColor = Color(0xFFE5E5E5), + focusedContainerColor = Color.White, + unfocusedContainerColor = Color.White, + ), + singleLine = true, ) - + Spacer(modifier = Modifier.width(12.dp)) - + TextButton( - onClick = { + onClick = { if (phoneNumber.isNotEmpty()) { 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), ) } } } - + Spacer(modifier = Modifier.height(16.dp)) - + // 提示文字 Text( text = "未注册的手机号码会自动创建新账号", fontSize = 12.sp, color = Color(0xFF999999), textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) - + Spacer(modifier = Modifier.height(40.dp)) - + // 登录按钮 Button( onClick = { onLoginClick(phoneNumber, verificationCode) }, enabled = phoneNumber.isNotEmpty() && verificationCode.isNotEmpty(), - modifier = Modifier - .fillMaxWidth() - .height(56.dp), + 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, ) } - + Spacer(modifier = Modifier.weight(1f)) } } @@ -211,4 +218,4 @@ fun RegisterScreen( @Preview fun RegisterScreenPreview() { RegisterScreen() -} \ No newline at end of file +} diff --git a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/SearchingScreen.kt b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/SearchingScreen.kt index 43b4c7e..60113fc 100644 --- a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/SearchingScreen.kt +++ b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/SearchingScreen.kt @@ -20,38 +20,41 @@ 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( - animation = tween(2000, easing = LinearEasing), - repeatMode = RepeatMode.Restart - ) + animationSpec = + infiniteRepeatable( + animation = tween(2000, easing = LinearEasing), + repeatMode = RepeatMode.Restart, + ), ) - + // 模拟搜索过程,5秒后跳转到设备列表 LaunchedEffect(Unit) { kotlinx.coroutines.delay(5000) onDeviceFound() } - + Box( - modifier = Modifier - .fillMaxSize() - .background(Color(0xFFF5F5F5)) + modifier = + Modifier + .fillMaxSize() + .background(Color(0xFFF5F5F5)), ) { Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 24.dp), - horizontalAlignment = Alignment.CenterHorizontally + modifier = + Modifier + .fillMaxSize() + .padding(horizontal = 24.dp), + horizontalAlignment = Alignment.CenterHorizontally, ) { Spacer(modifier = Modifier.height(120.dp)) - + // 标题 Text( text = "正在搜索设备...", @@ -59,100 +62,104 @@ 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 - .size(200.dp) - .background( - Color.White, - RoundedCornerShape(100.dp) - ), - contentAlignment = Alignment.Center + modifier = + Modifier + .size(200.dp) + .background( + Color.White, + RoundedCornerShape(100.dp), + ), + contentAlignment = Alignment.Center, ) { // 外圆环 - 旋转动画 Box( - modifier = Modifier - .size(160.dp) - .rotate(rotation) - .background( - Color.Transparent - ), - contentAlignment = Alignment.Center + modifier = + Modifier + .size(160.dp) + .rotate(rotation) + .background( + Color.Transparent, + ), + contentAlignment = Alignment.Center, ) { // 虚线圆环效果 repeat(8) { index -> Box( - modifier = Modifier - .size(8.dp) - .background( - Color(0xFF007AFF), - 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 - ) + modifier = + Modifier + .size(8.dp) + .background( + Color(0xFF007AFF), + 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, + ), ) } } - + // 中心搜索图标 Box( - modifier = Modifier - .size(80.dp) - .background( - Color(0xFF007AFF), - RoundedCornerShape(40.dp) - ), - contentAlignment = Alignment.Center + modifier = + Modifier + .size(80.dp) + .background( + Color(0xFF007AFF), + RoundedCornerShape(40.dp), + ), + contentAlignment = Alignment.Center, ) { Text( text = "🔍", fontSize = 32.sp, - color = Color.White + color = Color.White, ) } } - + Spacer(modifier = Modifier.height(60.dp)) - + // 进度指示器 LinearProgressIndicator( - modifier = Modifier - .fillMaxWidth() - .height(4.dp), + modifier = + Modifier + .fillMaxWidth() + .height(4.dp), color = Color(0xFF007AFF), - trackColor = Color(0xFFE5E5E5) + trackColor = Color(0xFFE5E5E5), ) - + Spacer(modifier = Modifier.height(24.dp)) - + // 提示文字 Text( text = "请确保戒指在充电状态并且靠近手机", fontSize = 16.sp, color = Color(0xFF666666), textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) - + Spacer(modifier = Modifier.weight(1f)) - + // 找不到设备链接 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, ) } } @@ -163,4 +170,4 @@ fun SearchTip( @Preview fun SearchingScreenPreview() { SearchTip() -} \ No newline at end of file +} diff --git a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/WelcomeScreen.kt b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/WelcomeScreen.kt index 1ed9d5d..5ccc7b8 100644 --- a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/WelcomeScreen.kt +++ b/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,139 +15,144 @@ 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 - .fillMaxSize() - .background(Color(0xFFF5F5F5)) + modifier = + Modifier + .fillMaxSize() + .background(Color(0xFFF5F5F5)), ) { Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 24.dp), - horizontalAlignment = Alignment.CenterHorizontally + modifier = + Modifier + .fillMaxSize() + .padding(horizontal = 24.dp), + horizontalAlignment = Alignment.CenterHorizontally, ) { Spacer(modifier = Modifier.height(120.dp)) - + // 主标题 Text( text = "Acti", fontSize = 48.sp, fontWeight = FontWeight.Bold, color = Color(0xFF333333), - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) - + Spacer(modifier = Modifier.height(24.dp)) - + // 副标题 Text( text = "赋能每一个动作", fontSize = 18.sp, fontWeight = FontWeight.Normal, color = Color(0xFF666666), - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) - + Spacer(modifier = Modifier.height(16.dp)) - + // 英文副标题 Text( text = "Empower Every Move", fontSize = 16.sp, fontWeight = FontWeight.Normal, color = Color(0xFF999999), - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) - + Spacer(modifier = Modifier.weight(1f)) - + // 戒指图片占位符 Box( - modifier = Modifier - .size(200.dp) - .background( - Color(0xFFE5E5E5), - RoundedCornerShape(16.dp) - ), - contentAlignment = Alignment.Center + modifier = + Modifier + .size(200.dp) + .background( + Color(0xFFE5E5E5), + RoundedCornerShape(16.dp), + ), + contentAlignment = Alignment.Center, ) { Text( text = "💍", - fontSize = 80.sp + fontSize = 80.sp, ) } - + Spacer(modifier = Modifier.weight(1f)) - + // 协议同意checkbox Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + 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)) - + 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), ) } - + Spacer(modifier = Modifier.height(24.dp)) - + // 立即使用按钮 Button( onClick = onStartClick, enabled = isChecked, - modifier = Modifier - .fillMaxWidth() - .height(56.dp), + modifier = + Modifier + .fillMaxWidth() + .height(56.dp), shape = RoundedCornerShape(28.dp), - colors = ButtonDefaults.buttonColors( - containerColor = if (isChecked) Color(0xFF007AFF) else Color(0xFFCCCCCC), - contentColor = Color.White - ) + colors = + ButtonDefaults.buttonColors( + containerColor = if (isChecked) Color(0xFF007AFF) else Color(0xFFCCCCCC), + contentColor = Color.White, + ), ) { Text( text = "立即使用", fontSize = 18.sp, - fontWeight = FontWeight.Medium + fontWeight = FontWeight.Medium, ) } - + Spacer(modifier = Modifier.height(40.dp)) } } @@ -158,4 +162,4 @@ fun WelcomeScreen( @Preview fun WelcomeScreenPreview() { WelcomeScreen() -} \ No newline at end of file +} diff --git a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/HomeViewModel.kt b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/HomeViewModel.kt index 5880e8b..4921d7e 100644 --- a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/HomeViewModel.kt +++ b/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,19 +37,18 @@ class HomeViewModel : ViewModel() { viewModelScope.launch { launch { manager.blePowerState.collectLatest { - if (it){ + + if (!mac().isNullOrEmpty()){ + manager.setAutoConnect(true) + } + + if (it) { Napier.i { "start scan" } manager.startScan() } } } - launch { - manager.deviceList.collectLatest { - Napier.i { "deviceList:${it}" } - } - } - } } diff --git a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/state/StateScreen.kt b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/state/StateScreen.kt index 6157da1..fa9bcf3 100644 --- a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/state/StateScreen.kt +++ b/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() diff --git a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/state/StateViewModel.kt b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/state/StateViewModel.kt index 030286b..c8166c8 100644 --- a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/state/StateViewModel.kt +++ b/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 = 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 = "心率", @@ -42,14 +77,7 @@ class StateViewModel : ViewModel() { type = StateCardType.SleepState(createSampleSleepData()) ) ) - + _uiState.value = _uiState.value.copy(stateCards = mockData) } - - fun refresh() { - _uiState.value = _uiState.value.copy(isLoading = true) - // 模拟刷新数据 - loadStateData() - _uiState.value = _uiState.value.copy(isLoading = false) - } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/whitefish/ring/utils/Utils.kt b/shared/src/commonMain/kotlin/com/whitefish/ring/utils/Utils.kt new file mode 100644 index 0000000..979258f --- /dev/null +++ b/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() +} diff --git a/shared/src/iosMain/kotlin/com/whitefish/ring/DeviceManager.kt b/shared/src/iosMain/kotlin/com/whitefish/ring/DeviceManager.kt index 9a96e0b..2536808 100644 --- a/shared/src/iosMain/kotlin/com/whitefish/ring/DeviceManager.kt +++ b/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 @@ -26,12 +28,18 @@ import platform.darwin.NSObject import platform.darwin.NSUInteger @OptIn(ExperimentalForeignApi::class) -class DeviceManager: IDeviceManager() { - private val manager = DeviceCenter.instance() +class DeviceManager : IDeviceManager() { + companion object { + val INSTANCE = DeviceCenter.instance() + } + + private val sdk = INSTANCE.sdk private var iosBleList = arrayListOf() private val scope = CoroutineScope(Dispatchers.IO) - + private var isBinded = false + private var autoConnecting = false + // 将delegate对象存储为强引用的成员变量,避免被垃圾回收 private val scanDelegate = object : NSObject(), SRBleScanProtocalProtocol { override fun srBleDidConnectPeripheral(service: SRBLeService) { @@ -44,7 +52,7 @@ class DeviceManager: IDeviceManager() { override fun srBlePowerStateChange(state: CBManagerState) { Napier.i { "srBlePowerStateChange:${state}" } - if (state.toInt() == 5){ + if (state.toInt() == 5) { scope.launch { blePowerState.emit(true) } @@ -57,12 +65,22 @@ class DeviceManager: IDeviceManager() { val deviceList = perphelArray.map { val device = it as SRBLeService iosBleList.add(device) - Device(device.advDataLocalName.toString(),device.macAddress.toString()) + 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}" } + } + } + } } - + private val dataDelegate = object : NSObject(), SRBleDataProtocalProtocol { override fun srBleDeviceDidReadyForReadAndWrite(service: SRBLeService) { Napier.i { "srBleDeviceDidReadyForReadAndWrite" } @@ -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( @@ -146,44 +188,49 @@ class DeviceManager: IDeviceManager() { ) { } } - + init { initializeManager() } - + 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 } } \ No newline at end of file diff --git a/shared/src/iosMain/kotlin/com/whitefish/ring/Platform.ios.kt b/shared/src/iosMain/kotlin/com/whitefish/ring/Platform.ios.kt index 56ed02b..c22004b 100644 --- a/shared/src/iosMain/kotlin/com/whitefish/ring/Platform.ios.kt +++ b/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 { @@ -64,4 +70,36 @@ actual fun obtainDeviceManager(): IDeviceManager { 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) } \ No newline at end of file diff --git a/shared/src/iosMain/kotlin/com/whitefish/ring/data/DataStore.ios.kt b/shared/src/iosMain/kotlin/com/whitefish/ring/data/DataStore.ios.kt new file mode 100644 index 0000000..2335117 --- /dev/null +++ b/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? = null +private val lock = SynchronizedObject() +@OptIn(ExperimentalForeignApi::class) +actual fun obtainDataStore(): DataStore = 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 } +} diff --git a/shared/src/iosMain/kotlin/com/whitefish/ring/data/DeviceDataProvider.ios.kt b/shared/src/iosMain/kotlin/com/whitefish/ring/data/DeviceDataProvider.ios.kt new file mode 100644 index 0000000..0b8f8fb --- /dev/null +++ b/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 = + 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() + + 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 + } +} diff --git a/shared/src/iosMain/kotlin/com/whitefish/ring/utils/Utils.kt b/shared/src/iosMain/kotlin/com/whitefish/ring/utils/Utils.kt new file mode 100644 index 0000000..b519e77 --- /dev/null +++ b/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() +} \ No newline at end of file