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. 24
      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. 25
      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. 38
      shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/DeviceScreen.kt
  29. 43
      shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/DeviceViewModel.kt
  30. 49
      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. 159
      shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/RegisterScreen.kt
  34. 137
      shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/SearchingScreen.kt
  35. 124
      shared/src/commonMain/kotlin/com/whitefish/ring/ui/guide/WelcomeScreen.kt
  36. 22
      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. 48
      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. 101
      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 { dependencies {
implementation(fileTree("../shared/libs"))
implementation(projects.shared) implementation(projects.shared)
implementation(libs.compose.ui) implementation(libs.compose.ui)
implementation(libs.compose.ui.tooling.preview) 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) super.onCreate(savedInstanceState)
PermissionManager.permissionChecker = permissionChecker PermissionManager.permissionChecker = permissionChecker
PermissionManager.checkPermission(this) PermissionManager.checkPermission(this)
(obtainDeviceManager() as DeviceManager).init(this)
setContent { setContent {
App() 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); //@property(copy, nonatomic)void (^ _Nullable bindFinishCbk)(BOOL isBindLimit);
-(void)registWithisCustomBleManage:(BOOL)isCustomBleManage -(void)registWithisCustomBleManage:(BOOL)isCustomBleManage;
;

24
shared/build.gradle.kts

@ -20,8 +20,8 @@ kotlin {
} }
} }
iosArm64().apply { iosArm64().apply {
compilations.getByName("main"){ compilations.getByName("main") {
val cin by cinterops.creating{ val cin by cinterops.creating {
definitionFile.set(project.file("Ring.def")) definitionFile.set(project.file("Ring.def"))
packageName("com.whitefish.ring.objc") packageName("com.whitefish.ring.objc")
val files = project.fileTree("../iosApp/iosApp/Libs").files.filter { it.extension == "h" } val files = project.fileTree("../iosApp/iosApp/Libs").files.filter { it.extension == "h" }
@ -44,15 +44,19 @@ kotlin {
framework { framework {
baseName = "shared" baseName = "shared"
isStatic = true isStatic = true
freeCompilerArgs += listOf( freeCompilerArgs +=
"-linker-option", "-L${project.projectDir.parent}/iosApp/iosApp/Libs", listOf(
"-linker-option", "-lRingSDK_2.0.2" "-linker-option",
) "-L${project.projectDir.parent}/iosApp/iosApp/Libs",
"-linker-option",
"-lRingSDK_2.0.2",
)
} }
} }
sourceSets { sourceSets {
commonMain.dependencies { commonMain.dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.2")
implementation("io.github.aakira:napier:2.7.1") implementation("io.github.aakira:napier:2.7.1")
implementation(compose.runtime) implementation(compose.runtime)
implementation(compose.foundation) implementation(compose.foundation)
@ -64,6 +68,10 @@ kotlin {
implementation(libs.androidx.lifecycle.runtimeCompose) implementation(libs.androidx.lifecycle.runtimeCompose)
implementation(libs.lifecycle.viewmodel.compose) implementation(libs.lifecycle.viewmodel.compose)
implementation(libs.vico.multiplatform) 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 { commonTest.dependencies {
@ -78,7 +86,7 @@ android {
defaultConfig { defaultConfig {
minSdk = 29 minSdk = 29
} }
lint{ lint {
disable.add("NullSafeMutableLiveData") disable.add("NullSafeMutableLiveData")
} }
compileOptions { compileOptions {
@ -90,7 +98,7 @@ android {
ksp(libs.androidx.room.compiler) ksp(libs.androidx.room.compiler)
implementation(libs.android.database.sqlcipher) implementation(libs.android.database.sqlcipher)
implementation(fileTree("libs")) implementation(fileTree("libs"))
implementation ("com.google.accompanist:accompanist-permissions:0.37.3") implementation("com.google.accompanist:accompanist-permissions:0.37.3")
} }
} }
dependencies { 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_CONNECTING = -2
const val STATE_DEVICE_CONNECTED = -1 const val STATE_DEVICE_CONNECTED = -1
} }
private val context = Application.INSTANTS!! private val app = Application.INSTANTS!!
private var isRegisterBattery = false private var isRegisterBattery = false
val batteryLevel = MutableLiveData(STATE_DEVICE_DISCONNECTED to 0) val batteryLevel = MutableLiveData(STATE_DEVICE_DISCONNECTED to 0)
private val sycProgress = MutableLiveData(0) private val sycProgress = MutableLiveData(0)
var isSyncingData: Boolean = false var isSyncingData: Boolean = false
private lateinit var context: Context
private val scope = CoroutineScope(Dispatchers.IO)
// var homeViewModel: demo.linktop.nexring.ui.HomeViewModel? = null // var homeViewModel: demo.linktop.nexring.ui.HomeViewModel? = null
// var workoutDetailViewModel: demo.linktop.nexring.ui.workout.WorkoutDetailViewModel? = null // var workoutDetailViewModel: demo.linktop.nexring.ui.workout.WorkoutDetailViewModel? = null
init { fun init(context: Context){
registerCb() registerCb()
this.context = context
} }
override fun onBleState(state: Int) { override fun onBleState(state: Int) {
bleStateListeners().forEach { bleStateListeners().forEach {
@ -131,18 +135,18 @@ class DeviceManager() : IDeviceManager(), OnBleConnectionListener, OnSleepDataLo
} }
fun registerCb() { fun registerCb() {
context.bleManager.addOnBleConnectionListener(this) app.bleManager.addOnBleConnectionListener(this)
NexRingManager.get().sleepApi().setOnSleepDataLoadListener(this) NexRingManager.get().sleepApi().setOnSleepDataLoadListener(this)
} }
fun unregisterCb() { fun unregisterCb() {
NexRingManager.get().sleepApi().setOnSleepDataLoadListener(null) NexRingManager.get().sleepApi().setOnSleepDataLoadListener(null)
context.bleManager.removeOnBleConnectionListener(this) app.bleManager.removeOnBleConnectionListener(this)
} }
override fun connect(address: String) { override fun connect(address: String) {
with(context.bleManager) { with(app.bleManager) {
when (bleState.value) { when (bleState.value) {
BluetoothProfile.STATE_DISCONNECTED -> { BluetoothProfile.STATE_DISCONNECTED -> {
batteryLevel.postValue(STATE_DEVICE_CONNECTING to 0) 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() NexRingManager.get()
.deviceApi() .deviceApi()
.getBindState { .getBindState {
if (it) { if (it) {
//todo bind dialog //todo bind dialog
// AlertDialog.Builder(this@DeviceActivity) AlertDialog.Builder(context)
// .setCancelable(false) .setCancelable(false)
// .setTitle(R.string.dialog_title_restricted_mode) .setTitle(R.string.dialog_title_restricted_mode)
// .setMessage(R.string.dialog_msg_restricted_mode) .setMessage(R.string.dialog_msg_restricted_mode)
// .setNegativeButton(android.R.string.cancel) { _, _ -> .setNegativeButton(android.R.string.cancel) { _, _ ->
//
// }.setPositiveButton(android.R.string.ok) { _, _ -> }.setPositiveButton(android.R.string.ok) { _, _ ->
// Logger.i("reset device") onResetDeviceWhenBind.invoke()
// deviceAdapter.clear() NexRingManager.get()
// NexRingManager.get() .deviceApi()
// .deviceApi() .factoryReset()
// .factoryReset() }.create().show()
// switchUI(true)
// postDelay {
// Thread.sleep(200)
// DeviceManager.INSTANCE.scan(this@DeviceActivity)
// }
// isConnecting = false
// }.create().show()
} else { } else {
NexRingManager.get() NexRingManager.get()
.deviceApi() .deviceApi()
.bind { .bind {
onBound.invoke()
//todo bind result //todo bind result
} }
} }
} }
} }
override fun getCurrentMac(): String? {
return app.bleManager.connectedDevice?.address
}
override fun startScan() { override fun startScan() {
val bluetoothAdapter = val bluetoothAdapter =
(context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter (context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter
if (bluetoothAdapter.isEnabled) { if (bluetoothAdapter.isEnabled) {
if (!context.bleManager.isScanning) { if (!app.bleManager.isScanning) {
context.bleManager.startScan(20 * 1000L, app.bleManager.startScan(20 * 1000L,
object : OnBleScanCallback { object : OnBleScanCallback {
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
override fun onScanning(result: BleDevice) { 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 { actual fun obtainDeviceManager(): IDeviceManager {
return DeviceInstance 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 bleState = MutableStateFlow(0)
var connectedDevice: BluetoothDevice? = null var connectedDevice: BluetoothDevice? = null
private set
private val _oemStepComplete = MutableStateFlow(false) private val _oemStepComplete = MutableStateFlow(false)
val oemStepComplete = _oemStepComplete.asStateFlow() val oemStepComplete = _oemStepComplete.asStateFlow()
@ -386,6 +387,7 @@ class BleManager(val app: Application) {
} }
} }
_oemStepComplete.value = true _oemStepComplete.value = true
Napier.i("OEM_STEP_PROCESS_COMPLETED") 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())
}
}
}
}

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

@ -1,23 +1,30 @@
@file:Suppress("ktlint:standard:no-wildcard-imports")
package com.whitefish.ring package com.whitefish.ring
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier 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.guide.GuideNavigationScreen
import com.whitefish.ring.ui.home.HomeScreen import com.whitefish.ring.ui.home.HomeScreen
import org.jetbrains.compose.ui.tooling.preview.Preview import org.jetbrains.compose.ui.tooling.preview.Preview
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
@Composable @Composable
@Preview @Preview
fun App() { fun App() {
MaterialTheme { MaterialTheme {
// HomeScreen(
// modifier = Modifier var guideComplete by remember { mutableStateOf(false) }
// )
// DeviceScreen{ if (guideComplete){
// HomeScreen()
// } }else{
GuideNavigationScreen() 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 package com.whitefish.ring.device
import com.whitefish.ring.bean.ui.Device import com.whitefish.ring.bean.ui.Device
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
abstract class IDeviceManager { abstract class IDeviceManager {
@ -15,6 +13,9 @@ abstract class IDeviceManager {
val blePowerState = MutableStateFlow<Boolean>(false) // ios的蓝牙是懒加载的,安卓则无此特性 val blePowerState = MutableStateFlow<Boolean>(false) // ios的蓝牙是懒加载的,安卓则无此特性
private val bleStateListeners = arrayListOf<(Int) -> Unit>() private val bleStateListeners = arrayListOf<(Int) -> Unit>()
protected var onResetDeviceWhenBind:()-> Unit = {}
protected var autoConnect = false
fun bleStateListeners() = bleStateListeners fun bleStateListeners() = bleStateListeners
@ -22,9 +23,15 @@ abstract class IDeviceManager {
bleStateListeners.add(event) bleStateListeners.add(event)
} }
fun setOnResetDeviceOnBind(event:() -> Unit) {
onResetDeviceWhenBind = event
}
abstract fun startScan() abstract fun startScan()
abstract fun stopScan() abstract fun stopScan()
abstract fun connect(mac: String) 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.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import com.patrykandpatrick.vico.multiplatform.cartesian.CartesianChartHost 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.CartesianChartModelProducer
import com.patrykandpatrick.vico.multiplatform.cartesian.data.lineSeries import com.patrykandpatrick.vico.multiplatform.cartesian.data.lineSeries
import com.patrykandpatrick.vico.multiplatform.cartesian.layer.LineCartesianLayer 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 import com.patrykandpatrick.vico.multiplatform.common.fill
@Composable @Composable
fun ComposeMultiplatformBasicLineChart(modifier: Modifier = Modifier) { fun ComposeMultiplatformBasicLineChart(data: List<Int>, modifier: Modifier = Modifier) {
val modelProducer = remember { CartesianChartModelProducer() } val modelProducer = remember { CartesianChartModelProducer() }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
modelProducer.runTransaction { modelProducer.runTransaction {
// Learn more: https://patrykandpatrick.com/z5ah6v. // 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( CartesianChartHost(
@ -48,6 +52,6 @@ fun ComposeMultiplatformBasicLineChart(modifier: Modifier = Modifier) {
), ),
modelProducer = modelProducer, modelProducer = modelProducer,
modifier = modifier, modifier = modifier,
zoomState = VicoZoomState(false, Zoom.Content,Zoom.Content,Zoom.Content)
) )
} }

38
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 import org.jetbrains.compose.ui.tooling.preview.Preview
@Composable @Composable
fun DeviceScreen(onBind:() -> Unit){ fun DeviceScreen(
val viewModel: DeviceViewModel = viewModel { DeviceViewModel() } viewModel: DeviceViewModel = viewModel { DeviceViewModel() }, onDeviceReset: () -> Unit,
onBind: () -> Unit
) {
viewModel.init()
viewModel.setOnDeviceReset(onDeviceReset)
val uiState by viewModel.uiState.collectAsState() 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() onBind.invoke()
return
} }
Box( Box(
@ -52,20 +58,20 @@ fun DeviceScreen(onBind:() -> Unit){
.padding(top = 60.dp, bottom = 40.dp), .padding(top = 60.dp, bottom = 40.dp),
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
// 设备列表 // 设备列表
LazyColumn( LazyColumn(
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) { ) {
items(uiState.deviceList) { device -> items(uiState.deviceList) { device ->
DeviceItem(device = device){ DeviceItem(device = device) {
viewModel.connect(it.mac) viewModel.connect(it.mac)
} }
} }
} }
} }
// 底部提示 // 底部提示
Text( Text(
text = "连接失败?", text = "连接失败?",
@ -79,7 +85,7 @@ fun DeviceScreen(onBind:() -> Unit){
} }
@Composable @Composable
private fun DeviceItem(device: Device,onClick:(Device)-> Unit) { private fun DeviceItem(device: Device, onClick: (Device) -> Unit) {
Card( Card(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -115,9 +121,9 @@ private fun DeviceItem(device: Device,onClick:(Device)-> Unit) {
fontSize = 24.sp fontSize = 24.sp
) )
} }
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
// 设备信息 // 设备信息
Column( Column(
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
@ -128,9 +134,9 @@ private fun DeviceItem(device: Device,onClick:(Device)-> Unit) {
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
color = Color(0xFF333333) color = Color(0xFF333333)
) )
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
Text( Text(
text = "设备号:${device.mac}", text = "设备号:${device.mac}",
fontSize = 14.sp, fontSize = 14.sp,
@ -143,8 +149,8 @@ private fun DeviceItem(device: Device,onClick:(Device)-> Unit) {
@Composable @Composable
@Preview @Preview
fun Device(){ fun Device() {
DeviceScreen{ // DeviceScreen {
//
} // }
} }

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

@ -1,13 +1,12 @@
package com.whitefish.ring.ui.guide package com.whitefish.ring.ui.guide
import androidx.compose.runtime.getValue import androidx.datastore.preferences.core.edit
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.whitefish.ring.bean.ui.Device 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 com.whitefish.ring.obtainDeviceManager
import io.github.aakira.napier.Napier import io.github.aakira.napier.Napier
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -15,8 +14,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class DeviceViewModel: ViewModel() { class DeviceViewModel : ViewModel() {
// var currentStep by remember { mutableStateOf(GuideStep.WELCOME) }
class UiState( class UiState(
val deviceList: List<Device> = emptyList() val deviceList: List<Device> = emptyList()
@ -25,12 +23,12 @@ class DeviceViewModel: ViewModel() {
val manager = obtainDeviceManager() val manager = obtainDeviceManager()
private val _uiState = MutableStateFlow(UiState()) private val _uiState = MutableStateFlow(UiState())
val uiState = _uiState.asStateFlow() val uiState = _uiState.asStateFlow()
init {
Napier.i { "DeviceViewModel initializing..." }
viewModelScope.launch {
val bindSuccess = MutableStateFlow(false)
fun init() {
viewModelScope.launch {
_uiState.value = UiState(emptyList())
launch { launch {
manager.deviceList.collectLatest { manager.deviceList.collectLatest {
Napier.i { "new device:${it}" } Napier.i { "new device:${it}" }
@ -40,7 +38,7 @@ class DeviceViewModel: ViewModel() {
launch { launch {
manager.blePowerState.collectLatest { manager.blePowerState.collectLatest {
if (it){ if (it) {
manager.startScan() manager.startScan()
} }
} }
@ -49,16 +47,29 @@ class DeviceViewModel: ViewModel() {
launch { launch {
manager.bleReadyStateFlow.collectLatest { manager.bleReadyStateFlow.collectLatest {
Napier.i { "ble ready:${it}" } Napier.i { "ble ready:${it}" }
if (it){ if (it && mac().isNullOrEmpty()) {
manager.bind() 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) manager.connect(mac)
} }
fun setOnDeviceReset(event: () -> Unit) {
manager.setOnResetDeviceOnBind(event)
}
} }

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

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

@ -62,9 +62,9 @@ fun AllGuideScreensPreview() {
.fillMaxWidth() .fillMaxWidth()
.height(600.dp) .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] = ""
}
}
}
}

159
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.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight 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.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import org.jetbrains.compose.ui.tooling.preview.Preview import org.jetbrains.compose.ui.tooling.preview.Preview
@Composable @Composable
fun RegisterScreen( fun RegisterScreen(onLoginClick: (phoneNumber: String, verificationCode: String) -> Unit = { _, _ -> }) {
onLoginClick: (phoneNumber: String, verificationCode: String) -> Unit = { _, _ -> }
) {
var phoneNumber by remember { mutableStateOf("") } var phoneNumber by remember { mutableStateOf("") }
var verificationCode by remember { mutableStateOf("") } var verificationCode by remember { mutableStateOf("") }
var isCodeSent by remember { mutableStateOf(false) } var isCodeSent by remember { mutableStateOf(false) }
Box( Box(
modifier = Modifier modifier =
.fillMaxSize() Modifier
.background(Color(0xFFF5F5F5)) .fillMaxSize()
.background(Color(0xFFF5F5F5)),
) { ) {
Column( Column(
modifier = Modifier modifier =
.fillMaxSize() Modifier
.padding(horizontal = 24.dp), .fillMaxSize()
horizontalAlignment = Alignment.CenterHorizontally .padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Spacer(modifier = Modifier.height(120.dp)) Spacer(modifier = Modifier.height(120.dp))
// 戒指图片占位符 // 戒指图片占位符
Box( Box(
modifier = Modifier modifier =
.size(160.dp) Modifier
.background( .size(160.dp)
Color(0xFFE5E5E5), .background(
RoundedCornerShape(80.dp) Color(0xFFE5E5E5),
), RoundedCornerShape(80.dp),
contentAlignment = Alignment.Center ),
contentAlignment = Alignment.Center,
) { ) {
Text( Text(
text = "💍", text = "💍",
fontSize = 64.sp fontSize = 64.sp,
) )
} }
Spacer(modifier = Modifier.height(40.dp)) Spacer(modifier = Modifier.height(40.dp))
// 欢迎标题 // 欢迎标题
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Text( Text(
text = "Hi,", text = "Hi,",
fontSize = 28.sp, fontSize = 28.sp,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
color = Color(0xFF333333), color = Color(0xFF333333),
textAlign = TextAlign.Center textAlign = TextAlign.Center,
) )
Text( Text(
text = "欢迎来到Acti", text = "欢迎来到Acti",
fontSize = 28.sp, fontSize = 28.sp,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
color = Color(0xFF333333), color = Color(0xFF333333),
textAlign = TextAlign.Center textAlign = TextAlign.Center,
) )
} }
Spacer(modifier = Modifier.height(60.dp)) Spacer(modifier = Modifier.height(60.dp))
// 手机号输入框 // 手机号输入框
Column( Column(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
) { ) {
Text( Text(
text = "手机号", text = "手机号",
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
color = Color(0xFF333333), color = Color(0xFF333333),
modifier = Modifier.padding(bottom = 8.dp) modifier = Modifier.padding(bottom = 8.dp),
) )
OutlinedTextField( OutlinedTextField(
value = phoneNumber, value = phoneNumber,
onValueChange = { phoneNumber = it }, onValueChange = { phoneNumber = it },
placeholder = { placeholder = {
Text( Text(
text = "请输入您的手机号", text = "请输入您的手机号",
color = Color(0xFF999999) color = Color(0xFF999999),
) )
}, },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
colors = OutlinedTextFieldDefaults.colors( colors =
focusedBorderColor = Color(0xFF007AFF), OutlinedTextFieldDefaults.colors(
unfocusedBorderColor = Color(0xFFE5E5E5), focusedBorderColor = Color(0xFF007AFF),
focusedContainerColor = Color.White, unfocusedBorderColor = Color(0xFFE5E5E5),
unfocusedContainerColor = Color.White focusedContainerColor = Color.White,
), unfocusedContainerColor = Color.White,
singleLine = true ),
singleLine = true,
) )
} }
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
// 验证码输入框 // 验证码输入框
Column( Column(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
) { ) {
Text( Text(
text = "验证码", text = "验证码",
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
color = Color(0xFF333333), color = Color(0xFF333333),
modifier = Modifier.padding(bottom = 8.dp) modifier = Modifier.padding(bottom = 8.dp),
) )
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
) { ) {
OutlinedTextField( OutlinedTextField(
value = verificationCode, value = verificationCode,
@ -135,73 +135,80 @@ fun RegisterScreen(
placeholder = { placeholder = {
Text( Text(
text = "请输入验证码", text = "请输入验证码",
color = Color(0xFF999999) color = Color(0xFF999999),
) )
}, },
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
colors = OutlinedTextFieldDefaults.colors( colors =
focusedBorderColor = Color(0xFF007AFF), OutlinedTextFieldDefaults.colors(
unfocusedBorderColor = Color(0xFFE5E5E5), focusedBorderColor = Color(0xFF007AFF),
focusedContainerColor = Color.White, unfocusedBorderColor = Color(0xFFE5E5E5),
unfocusedContainerColor = Color.White focusedContainerColor = Color.White,
), unfocusedContainerColor = Color.White,
singleLine = true ),
singleLine = true,
) )
Spacer(modifier = Modifier.width(12.dp)) Spacer(modifier = Modifier.width(12.dp))
TextButton( TextButton(
onClick = { onClick = {
if (phoneNumber.isNotEmpty()) { if (phoneNumber.isNotEmpty()) {
isCodeSent = true isCodeSent = true
} }
}, },
enabled = phoneNumber.isNotEmpty() enabled = phoneNumber.isNotEmpty(),
) { ) {
Text( Text(
text = if (isCodeSent) "重新发送验证码" else "获取验证码", text = if (isCodeSent) "重新发送验证码" else "获取验证码",
fontSize = 14.sp, 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)) Spacer(modifier = Modifier.height(16.dp))
// 提示文字 // 提示文字
Text( Text(
text = "未注册的手机号码会自动创建新账号", text = "未注册的手机号码会自动创建新账号",
fontSize = 12.sp, fontSize = 12.sp,
color = Color(0xFF999999), color = Color(0xFF999999),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
) )
Spacer(modifier = Modifier.height(40.dp)) Spacer(modifier = Modifier.height(40.dp))
// 登录按钮 // 登录按钮
Button( Button(
onClick = { onLoginClick(phoneNumber, verificationCode) }, onClick = { onLoginClick(phoneNumber, verificationCode) },
enabled = phoneNumber.isNotEmpty() && verificationCode.isNotEmpty(), enabled = phoneNumber.isNotEmpty() && verificationCode.isNotEmpty(),
modifier = Modifier modifier =
.fillMaxWidth() Modifier
.height(56.dp), .fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(28.dp), shape = RoundedCornerShape(28.dp),
colors = ButtonDefaults.buttonColors( colors =
containerColor = if (phoneNumber.isNotEmpty() && verificationCode.isNotEmpty()) ButtonDefaults.buttonColors(
Color(0xFF007AFF) else Color(0xFFCCCCCC), containerColor =
contentColor = Color.White if (phoneNumber.isNotEmpty() && verificationCode.isNotEmpty()) {
) Color(0xFF007AFF)
} else {
Color(0xFFCCCCCC)
},
contentColor = Color.White,
),
) { ) {
Text( Text(
text = "登录", text = "登录",
fontSize = 18.sp, fontSize = 18.sp,
fontWeight = FontWeight.Medium fontWeight = FontWeight.Medium,
) )
} }
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
} }
} }
@ -211,4 +218,4 @@ fun RegisterScreen(
@Preview @Preview
fun RegisterScreenPreview() { fun RegisterScreenPreview() {
RegisterScreen() RegisterScreen()
} }

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

@ -20,38 +20,41 @@ import org.jetbrains.compose.ui.tooling.preview.Preview
@Composable @Composable
fun SearchTip( fun SearchTip(
onDeviceNotFoundClick: () -> Unit = {}, onDeviceNotFoundClick: () -> Unit = {},
onDeviceFound: () -> Unit = {} onDeviceFound: () -> Unit = {},
) { ) {
// 旋转动画 // 旋转动画
val infiniteTransition = rememberInfiniteTransition() val infiniteTransition = rememberInfiniteTransition()
val rotation by infiniteTransition.animateFloat( val rotation by infiniteTransition.animateFloat(
initialValue = 0f, initialValue = 0f,
targetValue = 360f, targetValue = 360f,
animationSpec = infiniteRepeatable( animationSpec =
animation = tween(2000, easing = LinearEasing), infiniteRepeatable(
repeatMode = RepeatMode.Restart animation = tween(2000, easing = LinearEasing),
) repeatMode = RepeatMode.Restart,
),
) )
// 模拟搜索过程,5秒后跳转到设备列表 // 模拟搜索过程,5秒后跳转到设备列表
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
kotlinx.coroutines.delay(5000) kotlinx.coroutines.delay(5000)
onDeviceFound() onDeviceFound()
} }
Box( Box(
modifier = Modifier modifier =
.fillMaxSize() Modifier
.background(Color(0xFFF5F5F5)) .fillMaxSize()
.background(Color(0xFFF5F5F5)),
) { ) {
Column( Column(
modifier = Modifier modifier =
.fillMaxSize() Modifier
.padding(horizontal = 24.dp), .fillMaxSize()
horizontalAlignment = Alignment.CenterHorizontally .padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Spacer(modifier = Modifier.height(120.dp)) Spacer(modifier = Modifier.height(120.dp))
// 标题 // 标题
Text( Text(
text = "正在搜索设备...", text = "正在搜索设备...",
@ -59,100 +62,104 @@ fun SearchTip(
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
color = Color(0xFF333333), color = Color(0xFF333333),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
) )
Spacer(modifier = Modifier.height(80.dp)) Spacer(modifier = Modifier.height(80.dp))
// 搜索动画 // 搜索动画
Box( Box(
modifier = Modifier modifier =
.size(200.dp) Modifier
.background( .size(200.dp)
Color.White, .background(
RoundedCornerShape(100.dp) Color.White,
), RoundedCornerShape(100.dp),
contentAlignment = Alignment.Center ),
contentAlignment = Alignment.Center,
) { ) {
// 外圆环 - 旋转动画 // 外圆环 - 旋转动画
Box( Box(
modifier = Modifier modifier =
.size(160.dp) Modifier
.rotate(rotation) .size(160.dp)
.background( .rotate(rotation)
Color.Transparent .background(
), Color.Transparent,
contentAlignment = Alignment.Center ),
contentAlignment = Alignment.Center,
) { ) {
// 虚线圆环效果 // 虚线圆环效果
repeat(8) { index -> repeat(8) { index ->
Box( Box(
modifier = Modifier modifier =
.size(8.dp) Modifier
.background( .size(8.dp)
Color(0xFF007AFF), .background(
RoundedCornerShape(4.dp) Color(0xFF007AFF),
) RoundedCornerShape(4.dp),
.offset( ).offset(
x = (70 * kotlin.math.cos(index * 45.0 * kotlin.math.PI / 180)).dp, 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( Box(
modifier = Modifier modifier =
.size(80.dp) Modifier
.background( .size(80.dp)
Color(0xFF007AFF), .background(
RoundedCornerShape(40.dp) Color(0xFF007AFF),
), RoundedCornerShape(40.dp),
contentAlignment = Alignment.Center ),
contentAlignment = Alignment.Center,
) { ) {
Text( Text(
text = "🔍", text = "🔍",
fontSize = 32.sp, fontSize = 32.sp,
color = Color.White color = Color.White,
) )
} }
} }
Spacer(modifier = Modifier.height(60.dp)) Spacer(modifier = Modifier.height(60.dp))
// 进度指示器 // 进度指示器
LinearProgressIndicator( LinearProgressIndicator(
modifier = Modifier modifier =
.fillMaxWidth() Modifier
.height(4.dp), .fillMaxWidth()
.height(4.dp),
color = Color(0xFF007AFF), color = Color(0xFF007AFF),
trackColor = Color(0xFFE5E5E5) trackColor = Color(0xFFE5E5E5),
) )
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
// 提示文字 // 提示文字
Text( Text(
text = "请确保戒指在充电状态并且靠近手机", text = "请确保戒指在充电状态并且靠近手机",
fontSize = 16.sp, fontSize = 16.sp,
color = Color(0xFF666666), color = Color(0xFF666666),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
) )
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
// 找不到设备链接 // 找不到设备链接
TextButton( TextButton(
onClick = onDeviceNotFoundClick, onClick = onDeviceNotFoundClick,
modifier = Modifier.padding(bottom = 40.dp) modifier = Modifier.padding(bottom = 40.dp),
) { ) {
Text( Text(
text = "找不到设备?", text = "找不到设备?",
fontSize = 16.sp, fontSize = 16.sp,
color = Color(0xFF007AFF), color = Color(0xFF007AFF),
textDecoration = TextDecoration.Underline textDecoration = TextDecoration.Underline,
) )
} }
} }
@ -163,4 +170,4 @@ fun SearchTip(
@Preview @Preview
fun SearchingScreenPreview() { fun SearchingScreenPreview() {
SearchTip() SearchTip()
} }

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

@ -1,6 +1,5 @@
package com.whitefish.ring.ui.guide package com.whitefish.ring.ui.guide
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
@ -16,139 +15,144 @@ import androidx.compose.ui.unit.sp
import org.jetbrains.compose.ui.tooling.preview.Preview import org.jetbrains.compose.ui.tooling.preview.Preview
@Composable @Composable
fun WelcomeScreen( fun WelcomeScreen(onStartClick: () -> Unit = {}) {
onStartClick: () -> Unit = {}
) {
var isChecked by remember { mutableStateOf(false) } var isChecked by remember { mutableStateOf(false) }
Box( Box(
modifier = Modifier modifier =
.fillMaxSize() Modifier
.background(Color(0xFFF5F5F5)) .fillMaxSize()
.background(Color(0xFFF5F5F5)),
) { ) {
Column( Column(
modifier = Modifier modifier =
.fillMaxSize() Modifier
.padding(horizontal = 24.dp), .fillMaxSize()
horizontalAlignment = Alignment.CenterHorizontally .padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Spacer(modifier = Modifier.height(120.dp)) Spacer(modifier = Modifier.height(120.dp))
// 主标题 // 主标题
Text( Text(
text = "Acti", text = "Acti",
fontSize = 48.sp, fontSize = 48.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = Color(0xFF333333), color = Color(0xFF333333),
textAlign = TextAlign.Center textAlign = TextAlign.Center,
) )
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
// 副标题 // 副标题
Text( Text(
text = "赋能每一个动作", text = "赋能每一个动作",
fontSize = 18.sp, fontSize = 18.sp,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
color = Color(0xFF666666), color = Color(0xFF666666),
textAlign = TextAlign.Center textAlign = TextAlign.Center,
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
// 英文副标题 // 英文副标题
Text( Text(
text = "Empower Every Move", text = "Empower Every Move",
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
color = Color(0xFF999999), color = Color(0xFF999999),
textAlign = TextAlign.Center textAlign = TextAlign.Center,
) )
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
// 戒指图片占位符 // 戒指图片占位符
Box( Box(
modifier = Modifier modifier =
.size(200.dp) Modifier
.background( .size(200.dp)
Color(0xFFE5E5E5), .background(
RoundedCornerShape(16.dp) Color(0xFFE5E5E5),
), RoundedCornerShape(16.dp),
contentAlignment = Alignment.Center ),
contentAlignment = Alignment.Center,
) { ) {
Text( Text(
text = "💍", text = "💍",
fontSize = 80.sp fontSize = 80.sp,
) )
} }
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
// 协议同意checkbox // 协议同意checkbox
Row( Row(
modifier = Modifier modifier =
.fillMaxWidth() Modifier
.padding(horizontal = 16.dp), .fillMaxWidth()
verticalAlignment = Alignment.CenterVertically .padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) { ) {
Checkbox( Checkbox(
checked = isChecked, checked = isChecked,
onCheckedChange = { isChecked = it }, onCheckedChange = { isChecked = it },
colors = CheckboxDefaults.colors( colors =
checkedColor = Color(0xFF007AFF) CheckboxDefaults.colors(
) checkedColor = Color(0xFF007AFF),
),
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text( Text(
text = "我已阅读并同意", text = "我已阅读并同意",
fontSize = 14.sp, fontSize = 14.sp,
color = Color(0xFF666666) color = Color(0xFF666666),
) )
Text( Text(
text = "《用户协议》", text = "《用户协议》",
fontSize = 14.sp, fontSize = 14.sp,
color = Color(0xFF007AFF) color = Color(0xFF007AFF),
) )
Text( Text(
text = "", text = "",
fontSize = 14.sp, fontSize = 14.sp,
color = Color(0xFF666666) color = Color(0xFF666666),
) )
Text( Text(
text = "《隐私政策》", text = "《隐私政策》",
fontSize = 14.sp, fontSize = 14.sp,
color = Color(0xFF007AFF) color = Color(0xFF007AFF),
) )
} }
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
// 立即使用按钮 // 立即使用按钮
Button( Button(
onClick = onStartClick, onClick = onStartClick,
enabled = isChecked, enabled = isChecked,
modifier = Modifier modifier =
.fillMaxWidth() Modifier
.height(56.dp), .fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(28.dp), shape = RoundedCornerShape(28.dp),
colors = ButtonDefaults.buttonColors( colors =
containerColor = if (isChecked) Color(0xFF007AFF) else Color(0xFFCCCCCC), ButtonDefaults.buttonColors(
contentColor = Color.White containerColor = if (isChecked) Color(0xFF007AFF) else Color(0xFFCCCCCC),
) contentColor = Color.White,
),
) { ) {
Text( Text(
text = "立即使用", text = "立即使用",
fontSize = 18.sp, fontSize = 18.sp,
fontWeight = FontWeight.Medium fontWeight = FontWeight.Medium,
) )
} }
Spacer(modifier = Modifier.height(40.dp)) Spacer(modifier = Modifier.height(40.dp))
} }
} }
@ -158,4 +162,4 @@ fun WelcomeScreen(
@Preview @Preview
fun WelcomeScreenPreview() { fun WelcomeScreenPreview() {
WelcomeScreen() WelcomeScreen()
} }

22
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.ViewModel
import androidx.lifecycle.viewModelScope 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.device.IDeviceManager
import com.whitefish.ring.obtainDeviceManager import com.whitefish.ring.obtainDeviceManager
import com.whitefish.ring.utils.nowMilliseconds
import com.whitefish.ring.utils.today
import io.github.aakira.napier.Napier import io.github.aakira.napier.Napier
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -11,6 +16,10 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch 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( data class HomeUiState(
val selectedTab: HomeTab = HomeTab.STATE, val selectedTab: HomeTab = HomeTab.STATE,
@ -28,19 +37,18 @@ class HomeViewModel : ViewModel() {
viewModelScope.launch { viewModelScope.launch {
launch { launch {
manager.blePowerState.collectLatest { manager.blePowerState.collectLatest {
if (it){
if (!mac().isNullOrEmpty()){
manager.setAutoConnect(true)
}
if (it) {
Napier.i { "start scan" } Napier.i { "start scan" }
manager.startScan() manager.startScan()
} }
} }
} }
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) { when (val type = card.type) {
is StateCardType.HeartRate -> ComposeMultiplatformBasicLineChart() is StateCardType.HeartRate -> ComposeMultiplatformBasicLineChart(type.data)
is StateCardType.SleepState -> SleepChart( is StateCardType.SleepState -> SleepChart(
type.data, type.data,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()

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

@ -1,12 +1,27 @@
package com.whitefish.ring.ui.home.state package com.whitefish.ring.ui.home.state
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.whitefish.app.ui.chart.sleep.createSampleSleepData 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.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import com.whitefish.ring.ui.home.state.components.ExerciseGoalData import com.whitefish.ring.ui.home.state.components.ExerciseGoalData
import com.whitefish.ring.ui.home.state.components.RecoveryScoreData 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( data class StateUiState(
val stateCards: List<StateCardData> = emptyList(), val stateCards: List<StateCardData> = emptyList(),
@ -22,10 +37,30 @@ class StateViewModel : ViewModel() {
init { init {
loadStateData() 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() { private fun loadStateData() {
// 模拟加载状态数据,对应Android StateFragment的数据
val mockData = listOf( val mockData = listOf(
StateCardData( StateCardData(
title = "心率", title = "心率",
@ -42,14 +77,7 @@ class StateViewModel : ViewModel() {
type = StateCardType.SleepState(createSampleSleepData()) type = StateCardType.SleepState(createSampleSleepData())
) )
) )
_uiState.value = _uiState.value.copy(stateCards = mockData) _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()
}

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

@ -1,14 +1,16 @@
package com.whitefish.ring package com.whitefish.ring
import androidx.compose.ui.util.fastFirstOrNull import androidx.compose.ui.util.fastFirstOrNull
import androidx.datastore.preferences.core.edit
import com.whitefish.ring.bean.ui.Device 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.device.IDeviceManager
import com.whitefish.ring.objc.CMD_EXECTE_ERROR_REASON import com.whitefish.ring.objc.CMD_EXECTE_ERROR_REASON
import com.whitefish.ring.objc.DeviceCenter import com.whitefish.ring.objc.DeviceCenter
import com.whitefish.ring.objc.EXCUTED_CMD import com.whitefish.ring.objc.EXCUTED_CMD
import com.whitefish.ring.objc.FUNCTION_ERROR 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.SRBLeService
import com.whitefish.ring.objc.SRBleDataProtocalProtocol import com.whitefish.ring.objc.SRBleDataProtocalProtocol
import com.whitefish.ring.objc.SRBleScanProtocalProtocol import com.whitefish.ring.objc.SRBleScanProtocalProtocol
@ -26,12 +28,18 @@ import platform.darwin.NSObject
import platform.darwin.NSUInteger import platform.darwin.NSUInteger
@OptIn(ExperimentalForeignApi::class) @OptIn(ExperimentalForeignApi::class)
class DeviceManager: IDeviceManager() { 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 var iosBleList = arrayListOf<SRBLeService>()
private val scope = CoroutineScope(Dispatchers.IO) private val scope = CoroutineScope(Dispatchers.IO)
private var isBinded = false
private var autoConnecting = false
// 将delegate对象存储为强引用的成员变量,避免被垃圾回收 // 将delegate对象存储为强引用的成员变量,避免被垃圾回收
private val scanDelegate = object : NSObject(), SRBleScanProtocalProtocol { private val scanDelegate = object : NSObject(), SRBleScanProtocalProtocol {
override fun srBleDidConnectPeripheral(service: SRBLeService) { override fun srBleDidConnectPeripheral(service: SRBLeService) {
@ -44,7 +52,7 @@ class DeviceManager: IDeviceManager() {
override fun srBlePowerStateChange(state: CBManagerState) { override fun srBlePowerStateChange(state: CBManagerState) {
Napier.i { "srBlePowerStateChange:${state}" } Napier.i { "srBlePowerStateChange:${state}" }
if (state.toInt() == 5){ if (state.toInt() == 5) {
scope.launch { scope.launch {
blePowerState.emit(true) blePowerState.emit(true)
} }
@ -57,12 +65,22 @@ class DeviceManager: IDeviceManager() {
val deviceList = perphelArray.map { val deviceList = perphelArray.map {
val device = it as SRBLeService val device = it as SRBLeService
iosBleList.add(device) iosBleList.add(device)
Device(device.advDataLocalName.toString(),device.macAddress.toString()) Device(device.advDataLocalName.toString(), device.macAddress.toString())
} }
_deviceList.value = deviceList _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 { private val dataDelegate = object : NSObject(), SRBleDataProtocalProtocol {
override fun srBleDeviceDidReadyForReadAndWrite(service: SRBLeService) { override fun srBleDeviceDidReadyForReadAndWrite(service: SRBLeService) {
Napier.i { "srBleDeviceDidReadyForReadAndWrite" } Napier.i { "srBleDeviceDidReadyForReadAndWrite" }
@ -133,11 +151,35 @@ class DeviceManager: IDeviceManager() {
} }
override fun srBleIsbinded(isBinded: Boolean) { 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) { override fun srBleOEMAuthResult(authSucceddful: Boolean) {
bleReadyStateFlow.value = true bleReadyStateFlow.value = true
autoConnecting = false
Napier.i { "srBleOEMAuthResult" } Napier.i { "srBleOEMAuthResult" }
} }
override fun srBleFunctionErrorCallBack( override fun srBleFunctionErrorCallBack(
@ -146,44 +188,49 @@ class DeviceManager: IDeviceManager() {
) { ) {
} }
} }
init { init {
initializeManager() initializeManager()
} }
private fun initializeManager() { private fun initializeManager() {
Napier.i { "DeviceManager initializing..." } Napier.i { "DeviceManager initializing..." }
manager.registWithisCustomBleManage(true) INSTANCE.registWithisCustomBleManage(true)
// 使用成员变量而不是匿名对象 // 使用成员变量而不是匿名对象
manager.appScanDelegate = scanDelegate INSTANCE.appScanDelegate = scanDelegate
manager.appDataDelegate = dataDelegate INSTANCE.appDataDelegate = dataDelegate
Napier.i { "DeviceManager delegates set: scan=${scanDelegate}, data=${dataDelegate}" } Napier.i { "DeviceManager delegates set: scan=${scanDelegate}, data=${dataDelegate}" }
} }
// 添加重新初始化方法,在需要时可以调用
fun reinitialize() {
Napier.i { "DeviceManager reinitializing..." }
initializeManager()
}
override fun startScan() { override fun startScan() {
Napier.i { "Starting scan, delegate: ${manager.appScanDelegate}" } Napier.i { "Starting scan, delegate: ${INSTANCE.appScanDelegate}" }
manager.startBleScan() INSTANCE.startBleScan()
} }
override fun stopScan() { override fun stopScan() {
manager.stopBleScan() INSTANCE.stopBleScan()
} }
override fun connect(mac: String) { override fun connect(mac: String) {
iosBleList.fastFirstOrNull { it.macAddress == mac }?.let { iosBleList.fastFirstOrNull { it.macAddress == mac }?.let {
manager.connectDevice(it) INSTANCE.connectDevice(it)
Napier.i { "connect device:${it}" }
} }
} }
override fun bind() { override fun bind(onBound: () -> Unit) {
Napier.i { "bind device:${manager.currentDevice()}" } if (!isBinded) {
manager.bindCurrentDevice() 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 com.whitefish.ring.device.IDeviceManager
import io.github.aakira.napier.DebugAntilog import io.github.aakira.napier.DebugAntilog
import io.github.aakira.napier.Napier 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 import platform.UIKit.UIDevice
class IOSPlatform: Platform { class IOSPlatform: Platform {
@ -64,4 +70,36 @@ actual fun obtainDeviceManager(): IDeviceManager {
fun initLogger(){ fun initLogger(){
Napier.base(DebugAntilog()) Napier.base(DebugAntilog())
Napier.i { "Logger init success on ios" } 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