Showing
18 changed files
with
496 additions
and
106 deletions
| @@ -308,7 +308,10 @@ interface HealthKitHostApi { | @@ -308,7 +308,10 @@ interface HealthKitHostApi { | ||
| 308 | fun cancelHealthAppAuthorization(): Boolean | 308 | fun cancelHealthAppAuthorization(): Boolean |
| 309 | /** Runs native health read and server upload pipeline. */ | 309 | /** Runs native health read and server upload pipeline. */ |
| 310 | fun performHealthUpload(callback: (Result<HealthUploadResult>) -> Unit) | 310 | fun performHealthUpload(callback: (Result<HealthUploadResult>) -> Unit) |
| 311 | - /** Opens Huawei Health client authorization UI. Returns whether user granted. */ | 311 | + /** |
| 312 | + * 获取当前已上传的健康数据点,主要用于调试 | ||
| 313 | + * Opens Huawei Health client authorization UI. Returns whether user granted. | ||
| 314 | + */ | ||
| 312 | fun checkHealthAppAuthorization(callback: (Result<HealthAuthorization>) -> Unit) | 315 | fun checkHealthAppAuthorization(callback: (Result<HealthAuthorization>) -> Unit) |
| 313 | fun requestHealthClientAuthorization(callback: (Result<Boolean>) -> Unit) | 316 | fun requestHealthClientAuthorization(callback: (Result<Boolean>) -> Unit) |
| 314 | fun fetchHrvData(startTime: Long, endTime: Long, callback: (Result<List<HealthUploadDataPoint>>) -> Unit) | 317 | fun fetchHrvData(startTime: Long, endTime: Long, callback: (Result<List<HealthUploadDataPoint>>) -> Unit) |
| @@ -118,6 +118,61 @@ data class WearDeviceInfo ( | @@ -118,6 +118,61 @@ data class WearDeviceInfo ( | ||
| 118 | 118 | ||
| 119 | override fun hashCode(): Int = toList().hashCode() | 119 | override fun hashCode(): Int = toList().hashCode() |
| 120 | } | 120 | } |
| 121 | + | ||
| 122 | +/** Generated class from Pigeon that represents data sent in messages. */ | ||
| 123 | +data class WatchTransformTheme ( | ||
| 124 | + val themeId: Long, | ||
| 125 | + val themeName: String, | ||
| 126 | + val energeticDescription: String, | ||
| 127 | + val energeticImage: ByteArray? = null, | ||
| 128 | + val normalDescription: String, | ||
| 129 | + val normalImage: ByteArray? = null, | ||
| 130 | + val slightStressfulDescription: String, | ||
| 131 | + val slightStressfulImage: ByteArray? = null, | ||
| 132 | + val stressfulDescription: String, | ||
| 133 | + val stressfulImage: ByteArray? = null | ||
| 134 | +) | ||
| 135 | + { | ||
| 136 | + companion object { | ||
| 137 | + fun fromList(pigeonVar_list: List<Any?>): WatchTransformTheme { | ||
| 138 | + val themeId = pigeonVar_list[0] as Long | ||
| 139 | + val themeName = pigeonVar_list[1] as String | ||
| 140 | + val energeticDescription = pigeonVar_list[2] as String | ||
| 141 | + val energeticImage = pigeonVar_list[3] as ByteArray? | ||
| 142 | + val normalDescription = pigeonVar_list[4] as String | ||
| 143 | + val normalImage = pigeonVar_list[5] as ByteArray? | ||
| 144 | + val slightStressfulDescription = pigeonVar_list[6] as String | ||
| 145 | + val slightStressfulImage = pigeonVar_list[7] as ByteArray? | ||
| 146 | + val stressfulDescription = pigeonVar_list[8] as String | ||
| 147 | + val stressfulImage = pigeonVar_list[9] as ByteArray? | ||
| 148 | + return WatchTransformTheme(themeId, themeName, energeticDescription, energeticImage, normalDescription, normalImage, slightStressfulDescription, slightStressfulImage, stressfulDescription, stressfulImage) | ||
| 149 | + } | ||
| 150 | + } | ||
| 151 | + fun toList(): List<Any?> { | ||
| 152 | + return listOf( | ||
| 153 | + themeId, | ||
| 154 | + themeName, | ||
| 155 | + energeticDescription, | ||
| 156 | + energeticImage, | ||
| 157 | + normalDescription, | ||
| 158 | + normalImage, | ||
| 159 | + slightStressfulDescription, | ||
| 160 | + slightStressfulImage, | ||
| 161 | + stressfulDescription, | ||
| 162 | + stressfulImage, | ||
| 163 | + ) | ||
| 164 | + } | ||
| 165 | + override fun equals(other: Any?): Boolean { | ||
| 166 | + if (other !is WatchTransformTheme) { | ||
| 167 | + return false | ||
| 168 | + } | ||
| 169 | + if (this === other) { | ||
| 170 | + return true | ||
| 171 | + } | ||
| 172 | + return WearEngineApiPigeonUtils.deepEquals(toList(), other.toList()) } | ||
| 173 | + | ||
| 174 | + override fun hashCode(): Int = toList().hashCode() | ||
| 175 | +} | ||
| 121 | private open class WearEngineApiPigeonCodec : StandardMessageCodec() { | 176 | private open class WearEngineApiPigeonCodec : StandardMessageCodec() { |
| 122 | override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { | 177 | override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { |
| 123 | return when (type) { | 178 | return when (type) { |
| @@ -126,6 +181,11 @@ private open class WearEngineApiPigeonCodec : StandardMessageCodec() { | @@ -126,6 +181,11 @@ private open class WearEngineApiPigeonCodec : StandardMessageCodec() { | ||
| 126 | WearDeviceInfo.fromList(it) | 181 | WearDeviceInfo.fromList(it) |
| 127 | } | 182 | } |
| 128 | } | 183 | } |
| 184 | + 130.toByte() -> { | ||
| 185 | + return (readValue(buffer) as? List<Any?>)?.let { | ||
| 186 | + WatchTransformTheme.fromList(it) | ||
| 187 | + } | ||
| 188 | + } | ||
| 129 | else -> super.readValueOfType(type, buffer) | 189 | else -> super.readValueOfType(type, buffer) |
| 130 | } | 190 | } |
| 131 | } | 191 | } |
| @@ -135,6 +195,10 @@ private open class WearEngineApiPigeonCodec : StandardMessageCodec() { | @@ -135,6 +195,10 @@ private open class WearEngineApiPigeonCodec : StandardMessageCodec() { | ||
| 135 | stream.write(129) | 195 | stream.write(129) |
| 136 | writeValue(stream, value.toList()) | 196 | writeValue(stream, value.toList()) |
| 137 | } | 197 | } |
| 198 | + is WatchTransformTheme -> { | ||
| 199 | + stream.write(130) | ||
| 200 | + writeValue(stream, value.toList()) | ||
| 201 | + } | ||
| 138 | else -> super.writeValue(stream, value) | 202 | else -> super.writeValue(stream, value) |
| 139 | } | 203 | } |
| 140 | } | 204 | } |
| @@ -149,7 +213,8 @@ interface WearEngineHostApi { | @@ -149,7 +213,8 @@ interface WearEngineHostApi { | ||
| 149 | fun sendTextMessage(message: String): Boolean | 213 | fun sendTextMessage(message: String): Boolean |
| 150 | /** 添加表盘 */ | 214 | /** 添加表盘 */ |
| 151 | fun addWatchSurface(callback: (Result<Boolean>) -> Unit) | 215 | fun addWatchSurface(callback: (Result<Boolean>) -> Unit) |
| 152 | - fun sendWatchSyncPayload(jsonPayload: String, callback: (Result<Boolean>) -> Unit) | 216 | + /** 添加或者删除主题 */ |
| 217 | + fun syncWatchTheme(theme: WatchTransformTheme?, callback: (Result<Boolean>) -> Unit) | ||
| 153 | /** | 218 | /** |
| 154 | * Opens the system photo picker and returns a local PNG file path after | 219 | * Opens the system photo picker and returns a local PNG file path after |
| 155 | * removing the image background on the host platform. | 220 | * removing the image background on the host platform. |
| @@ -251,12 +316,12 @@ interface WearEngineHostApi { | @@ -251,12 +316,12 @@ interface WearEngineHostApi { | ||
| 251 | } | 316 | } |
| 252 | } | 317 | } |
| 253 | run { | 318 | run { |
| 254 | - val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.sendWatchSyncPayload$separatedMessageChannelSuffix", codec) | 319 | + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.syncWatchTheme$separatedMessageChannelSuffix", codec) |
| 255 | if (api != null) { | 320 | if (api != null) { |
| 256 | channel.setMessageHandler { message, reply -> | 321 | channel.setMessageHandler { message, reply -> |
| 257 | val args = message as List<Any?> | 322 | val args = message as List<Any?> |
| 258 | - val jsonPayloadArg = args[0] as String | ||
| 259 | - api.sendWatchSyncPayload(jsonPayloadArg) { result: Result<Boolean> -> | 323 | + val themeArg = args[0] as WatchTransformTheme? |
| 324 | + api.syncWatchTheme(themeArg) { result: Result<Boolean> -> | ||
| 260 | val error = result.exceptionOrNull() | 325 | val error = result.exceptionOrNull() |
| 261 | if (error != null) { | 326 | if (error != null) { |
| 262 | reply.reply(WearEngineApiPigeonUtils.wrapError(error)) | 327 | reply.reply(WearEngineApiPigeonUtils.wrapError(error)) |
| @@ -333,6 +333,7 @@ protocol HealthKitHostApi { | @@ -333,6 +333,7 @@ protocol HealthKitHostApi { | ||
| 333 | func cancelHealthAppAuthorization() throws -> Bool | 333 | func cancelHealthAppAuthorization() throws -> Bool |
| 334 | /// Runs native health read and server upload pipeline. | 334 | /// Runs native health read and server upload pipeline. |
| 335 | func performHealthUpload(completion: @escaping (Result<HealthUploadResult, Error>) -> Void) | 335 | func performHealthUpload(completion: @escaping (Result<HealthUploadResult, Error>) -> Void) |
| 336 | + /// 获取当前已上传的健康数据点,主要用于调试 | ||
| 336 | /// Opens Huawei Health client authorization UI. Returns whether user granted. | 337 | /// Opens Huawei Health client authorization UI. Returns whether user granted. |
| 337 | func checkHealthAppAuthorization(completion: @escaping (Result<HealthAuthorization, Error>) -> Void) | 338 | func checkHealthAppAuthorization(completion: @escaping (Result<HealthAuthorization, Error>) -> Void) |
| 338 | func requestHealthClientAuthorization(completion: @escaping (Result<Bool, Error>) -> Void) | 339 | func requestHealthClientAuthorization(completion: @escaping (Result<Bool, Error>) -> Void) |
| @@ -403,6 +404,7 @@ class HealthKitHostApiSetup { | @@ -403,6 +404,7 @@ class HealthKitHostApiSetup { | ||
| 403 | } else { | 404 | } else { |
| 404 | performHealthUploadChannel.setMessageHandler(nil) | 405 | performHealthUploadChannel.setMessageHandler(nil) |
| 405 | } | 406 | } |
| 407 | + /// 获取当前已上传的健康数据点,主要用于调试 | ||
| 406 | /// Opens Huawei Health client authorization UI. Returns whether user granted. | 408 | /// Opens Huawei Health client authorization UI. Returns whether user granted. |
| 407 | let checkHealthAppAuthorizationChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.checkHealthAppAuthorization\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) | 409 | let checkHealthAppAuthorizationChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.checkHealthAppAuthorization\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) |
| 408 | if let api = api { | 410 | if let api = api { |
| @@ -153,11 +153,74 @@ struct WearDeviceInfo: Hashable { | @@ -153,11 +153,74 @@ struct WearDeviceInfo: Hashable { | ||
| 153 | } | 153 | } |
| 154 | } | 154 | } |
| 155 | 155 | ||
| 156 | +/// Generated class from Pigeon that represents data sent in messages. | ||
| 157 | +struct WatchTransformTheme: Hashable { | ||
| 158 | + var themeId: Int64 | ||
| 159 | + var themeName: String | ||
| 160 | + var energeticDescription: String | ||
| 161 | + var energeticImage: FlutterStandardTypedData? = nil | ||
| 162 | + var normalDescription: String | ||
| 163 | + var normalImage: FlutterStandardTypedData? = nil | ||
| 164 | + var slightStressfulDescription: String | ||
| 165 | + var slightStressfulImage: FlutterStandardTypedData? = nil | ||
| 166 | + var stressfulDescription: String | ||
| 167 | + var stressfulImage: FlutterStandardTypedData? = nil | ||
| 168 | + | ||
| 169 | + | ||
| 170 | + // swift-format-ignore: AlwaysUseLowerCamelCase | ||
| 171 | + static func fromList(_ pigeonVar_list: [Any?]) -> WatchTransformTheme? { | ||
| 172 | + let themeId = pigeonVar_list[0] as! Int64 | ||
| 173 | + let themeName = pigeonVar_list[1] as! String | ||
| 174 | + let energeticDescription = pigeonVar_list[2] as! String | ||
| 175 | + let energeticImage: FlutterStandardTypedData? = nilOrValue(pigeonVar_list[3]) | ||
| 176 | + let normalDescription = pigeonVar_list[4] as! String | ||
| 177 | + let normalImage: FlutterStandardTypedData? = nilOrValue(pigeonVar_list[5]) | ||
| 178 | + let slightStressfulDescription = pigeonVar_list[6] as! String | ||
| 179 | + let slightStressfulImage: FlutterStandardTypedData? = nilOrValue(pigeonVar_list[7]) | ||
| 180 | + let stressfulDescription = pigeonVar_list[8] as! String | ||
| 181 | + let stressfulImage: FlutterStandardTypedData? = nilOrValue(pigeonVar_list[9]) | ||
| 182 | + | ||
| 183 | + return WatchTransformTheme( | ||
| 184 | + themeId: themeId, | ||
| 185 | + themeName: themeName, | ||
| 186 | + energeticDescription: energeticDescription, | ||
| 187 | + energeticImage: energeticImage, | ||
| 188 | + normalDescription: normalDescription, | ||
| 189 | + normalImage: normalImage, | ||
| 190 | + slightStressfulDescription: slightStressfulDescription, | ||
| 191 | + slightStressfulImage: slightStressfulImage, | ||
| 192 | + stressfulDescription: stressfulDescription, | ||
| 193 | + stressfulImage: stressfulImage | ||
| 194 | + ) | ||
| 195 | + } | ||
| 196 | + func toList() -> [Any?] { | ||
| 197 | + return [ | ||
| 198 | + themeId, | ||
| 199 | + themeName, | ||
| 200 | + energeticDescription, | ||
| 201 | + energeticImage, | ||
| 202 | + normalDescription, | ||
| 203 | + normalImage, | ||
| 204 | + slightStressfulDescription, | ||
| 205 | + slightStressfulImage, | ||
| 206 | + stressfulDescription, | ||
| 207 | + stressfulImage, | ||
| 208 | + ] | ||
| 209 | + } | ||
| 210 | + static func == (lhs: WatchTransformTheme, rhs: WatchTransformTheme) -> Bool { | ||
| 211 | + return deepEqualsWearEngineApi(lhs.toList(), rhs.toList()) } | ||
| 212 | + func hash(into hasher: inout Hasher) { | ||
| 213 | + deepHashWearEngineApi(value: toList(), hasher: &hasher) | ||
| 214 | + } | ||
| 215 | +} | ||
| 216 | + | ||
| 156 | private class WearEngineApiPigeonCodecReader: FlutterStandardReader { | 217 | private class WearEngineApiPigeonCodecReader: FlutterStandardReader { |
| 157 | override func readValue(ofType type: UInt8) -> Any? { | 218 | override func readValue(ofType type: UInt8) -> Any? { |
| 158 | switch type { | 219 | switch type { |
| 159 | case 129: | 220 | case 129: |
| 160 | return WearDeviceInfo.fromList(self.readValue() as! [Any?]) | 221 | return WearDeviceInfo.fromList(self.readValue() as! [Any?]) |
| 222 | + case 130: | ||
| 223 | + return WatchTransformTheme.fromList(self.readValue() as! [Any?]) | ||
| 161 | default: | 224 | default: |
| 162 | return super.readValue(ofType: type) | 225 | return super.readValue(ofType: type) |
| 163 | } | 226 | } |
| @@ -169,6 +232,9 @@ private class WearEngineApiPigeonCodecWriter: FlutterStandardWriter { | @@ -169,6 +232,9 @@ private class WearEngineApiPigeonCodecWriter: FlutterStandardWriter { | ||
| 169 | if let value = value as? WearDeviceInfo { | 232 | if let value = value as? WearDeviceInfo { |
| 170 | super.writeByte(129) | 233 | super.writeByte(129) |
| 171 | super.writeValue(value.toList()) | 234 | super.writeValue(value.toList()) |
| 235 | + } else if let value = value as? WatchTransformTheme { | ||
| 236 | + super.writeByte(130) | ||
| 237 | + super.writeValue(value.toList()) | ||
| 172 | } else { | 238 | } else { |
| 173 | super.writeValue(value) | 239 | super.writeValue(value) |
| 174 | } | 240 | } |
| @@ -198,7 +264,8 @@ protocol WearEngineHostApi { | @@ -198,7 +264,8 @@ protocol WearEngineHostApi { | ||
| 198 | func sendTextMessage(message: String) throws -> Bool | 264 | func sendTextMessage(message: String) throws -> Bool |
| 199 | /// 添加表盘 | 265 | /// 添加表盘 |
| 200 | func addWatchSurface(completion: @escaping (Result<Bool, Error>) -> Void) | 266 | func addWatchSurface(completion: @escaping (Result<Bool, Error>) -> Void) |
| 201 | - func sendWatchSyncPayload(jsonPayload: String, completion: @escaping (Result<Bool, Error>) -> Void) | 267 | + /// 添加或者删除主题 |
| 268 | + func syncWatchTheme(theme: WatchTransformTheme?, completion: @escaping (Result<Bool, Error>) -> Void) | ||
| 202 | /// Opens the system photo picker and returns a local PNG file path after | 269 | /// Opens the system photo picker and returns a local PNG file path after |
| 203 | /// removing the image background on the host platform. | 270 | /// removing the image background on the host platform. |
| 204 | func removeBackground(originImagePath: String, completion: @escaping (Result<String?, Error>) -> Void) | 271 | func removeBackground(originImagePath: String, completion: @escaping (Result<String?, Error>) -> Void) |
| @@ -284,12 +351,13 @@ class WearEngineHostApiSetup { | @@ -284,12 +351,13 @@ class WearEngineHostApiSetup { | ||
| 284 | } else { | 351 | } else { |
| 285 | addWatchSurfaceChannel.setMessageHandler(nil) | 352 | addWatchSurfaceChannel.setMessageHandler(nil) |
| 286 | } | 353 | } |
| 287 | - let sendWatchSyncPayloadChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.sendWatchSyncPayload\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) | 354 | + /// 添加或者删除主题 |
| 355 | + let syncWatchThemeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.syncWatchTheme\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) | ||
| 288 | if let api = api { | 356 | if let api = api { |
| 289 | - sendWatchSyncPayloadChannel.setMessageHandler { message, reply in | 357 | + syncWatchThemeChannel.setMessageHandler { message, reply in |
| 290 | let args = message as! [Any?] | 358 | let args = message as! [Any?] |
| 291 | - let jsonPayloadArg = args[0] as! String | ||
| 292 | - api.sendWatchSyncPayload(jsonPayload: jsonPayloadArg) { result in | 359 | + let themeArg: WatchTransformTheme? = nilOrValue(args[0]) |
| 360 | + api.syncWatchTheme(theme: themeArg) { result in | ||
| 293 | switch result { | 361 | switch result { |
| 294 | case .success(let res): | 362 | case .success(let res): |
| 295 | reply(wrapResult(res)) | 363 | reply(wrapResult(res)) |
| @@ -299,7 +367,7 @@ class WearEngineHostApiSetup { | @@ -299,7 +367,7 @@ class WearEngineHostApiSetup { | ||
| 299 | } | 367 | } |
| 300 | } | 368 | } |
| 301 | } else { | 369 | } else { |
| 302 | - sendWatchSyncPayloadChannel.setMessageHandler(nil) | 370 | + syncWatchThemeChannel.setMessageHandler(nil) |
| 303 | } | 371 | } |
| 304 | /// Opens the system photo picker and returns a local PNG file path after | 372 | /// Opens the system photo picker and returns a local PNG file path after |
| 305 | /// removing the image background on the host platform. | 373 | /// removing the image background on the host platform. |
| @@ -41,10 +41,20 @@ final class WearEngineHostApiImpl: WearEngineHostApi { | @@ -41,10 +41,20 @@ final class WearEngineHostApiImpl: WearEngineHostApi { | ||
| 41 | watchService.sendTextMessage(message) | 41 | watchService.sendTextMessage(message) |
| 42 | } | 42 | } |
| 43 | 43 | ||
| 44 | - func sendWatchSyncPayload(jsonPayload: String, completion: @escaping (Result<Bool, any Error>) -> Void) { | ||
| 45 | - let success = WatchConnectivityService.shared.sendWatchThemeChangedMessage(json: jsonPayload) | ||
| 46 | - completion(.success(success)) | ||
| 47 | - } | 44 | + func syncWatchTheme(theme: WatchTransformTheme?, completion: @escaping (Result<Bool, any Error>) -> Void) { |
| 45 | + guard let theme else{ | ||
| 46 | + //TODO: - 删除主题 | ||
| 47 | + completion(.success(true)) | ||
| 48 | + return | ||
| 49 | + } | ||
| 50 | + let success = WatchThemeStore.shared.saveTheme(theme) | ||
| 51 | + guard success else { | ||
| 52 | + completion(.success(false)) | ||
| 53 | + return | ||
| 54 | + } | ||
| 55 | + let notified = watchService.sendWatchThemeChangedMessage() | ||
| 56 | + completion(.success(notified)) | ||
| 57 | + } | ||
| 48 | 58 | ||
| 49 | func removeBackground(originImagePath: String, completion: @escaping (Result<String?, any Error>) -> Void) { | 59 | func removeBackground(originImagePath: String, completion: @escaping (Result<String?, any Error>) -> Void) { |
| 50 | // 抠图 | 60 | // 抠图 |
| @@ -53,6 +53,7 @@ final class WatchConnectivityService: NSObject { | @@ -53,6 +53,7 @@ final class WatchConnectivityService: NSObject { | ||
| 53 | return WearDeviceInfo( | 53 | return WearDeviceInfo( |
| 54 | deviceId: nil, | 54 | deviceId: nil, |
| 55 | deviceName: "Apple Watch", | 55 | deviceName: "Apple Watch", |
| 56 | + isPaired: isPaired, | ||
| 56 | isReachable: isReachable, | 57 | isReachable: isReachable, |
| 57 | isWatchAppInstalled: isWatchAppInstalled | 58 | isWatchAppInstalled: isWatchAppInstalled |
| 58 | ) | 59 | ) |
| @@ -111,7 +112,7 @@ final class WatchConnectivityService: NSObject { | @@ -111,7 +112,7 @@ final class WatchConnectivityService: NSObject { | ||
| 111 | return true | 112 | return true |
| 112 | } | 113 | } |
| 113 | 114 | ||
| 114 | - func sendWatchThemeChangedMessage(json: String) -> Bool{ | 115 | + func sendWatchThemeChangedMessage(json: String? = nil) -> Bool{ |
| 115 | sendCommandMessage(AppGroupMessageKey.reloadTheme, json: json) | 116 | sendCommandMessage(AppGroupMessageKey.reloadTheme, json: json) |
| 116 | } | 117 | } |
| 117 | } | 118 | } |
| 1 | import Foundation | 1 | import Foundation |
| 2 | 2 | ||
| 3 | /// Shared Watch theme persistence. | 3 | /// Shared Watch theme persistence. |
| 4 | -/// The Watch app and Widget extension read this exact JSON string from App Group. | 4 | +/// The Watch app and Widget extension read this exact JSON data from App Group. |
| 5 | final class WatchThemeStore { | 5 | final class WatchThemeStore { |
| 6 | static let shared = WatchThemeStore() | 6 | static let shared = WatchThemeStore() |
| 7 | 7 | ||
| @@ -9,13 +9,95 @@ final class WatchThemeStore { | @@ -9,13 +9,95 @@ final class WatchThemeStore { | ||
| 9 | 9 | ||
| 10 | @discardableResult | 10 | @discardableResult |
| 11 | func saveThemeJSONString(_ json: String) -> Bool { | 11 | func saveThemeJSONString(_ json: String) -> Bool { |
| 12 | - guard json.data(using: .utf8) != nil else { return false } | ||
| 13 | - AppGroupConstants.defaults?.set(json, forKey: AppGroupConstants.Key.myWatchTheme) | 12 | + guard let data = json.data(using: .utf8) else { return false } |
| 13 | + AppGroupConstants.defaults?.set(data, forKey: AppGroupConstants.Key.myWatchTheme) | ||
| 14 | return true | 14 | return true |
| 15 | } | 15 | } |
| 16 | 16 | ||
| 17 | + @discardableResult | ||
| 18 | + func saveTheme(_ theme: WatchTransformTheme) -> Bool { | ||
| 19 | + guard let defaults = AppGroupConstants.defaults else { return false } | ||
| 20 | + let imageStore = SyncedWatchThemeImageStore(themeId: theme.themeId) | ||
| 21 | + let model = SyncedWatchThemeModel( | ||
| 22 | + id: Int(theme.themeId), | ||
| 23 | + themeName: theme.themeName, | ||
| 24 | + isOfficial: 0, | ||
| 25 | + energeticTitle: theme.energeticDescription, | ||
| 26 | + energeticImageURL: imageStore.save( | ||
| 27 | + theme.energeticImage?.data, | ||
| 28 | + status: "energetic", | ||
| 29 | + defaults: defaults | ||
| 30 | + ), | ||
| 31 | + normalTitle: theme.normalDescription, | ||
| 32 | + normalImageURL: imageStore.save( | ||
| 33 | + theme.normalImage?.data, | ||
| 34 | + status: "normal", | ||
| 35 | + defaults: defaults | ||
| 36 | + ), | ||
| 37 | + slightStressTitle: theme.slightStressfulDescription, | ||
| 38 | + slightStressImageURL: imageStore.save( | ||
| 39 | + theme.slightStressfulImage?.data, | ||
| 40 | + status: "slight_stressful", | ||
| 41 | + defaults: defaults | ||
| 42 | + ), | ||
| 43 | + stressfulTitle: theme.stressfulDescription, | ||
| 44 | + stressfulImageURL: imageStore.save( | ||
| 45 | + theme.stressfulImage?.data, | ||
| 46 | + status: "stressful", | ||
| 47 | + defaults: defaults | ||
| 48 | + ) | ||
| 49 | + ) | ||
| 50 | + | ||
| 51 | + do { | ||
| 52 | + let data = try JSONEncoder().encode(model) | ||
| 53 | + defaults.set(data, forKey: AppGroupConstants.Key.myWatchTheme) | ||
| 54 | + defaults.synchronize() | ||
| 55 | + return true | ||
| 56 | + } catch { | ||
| 57 | + return false | ||
| 58 | + } | ||
| 59 | + } | ||
| 60 | + | ||
| 17 | func clearTheme() { | 61 | func clearTheme() { |
| 18 | AppGroupConstants.defaults?.removeObject(forKey: AppGroupConstants.Key.myWatchTheme) | 62 | AppGroupConstants.defaults?.removeObject(forKey: AppGroupConstants.Key.myWatchTheme) |
| 19 | } | 63 | } |
| 20 | } | 64 | } |
| 21 | 65 | ||
| 66 | +private struct SyncedWatchThemeImageStore { | ||
| 67 | + let themeId: Int64 | ||
| 68 | + | ||
| 69 | + func save(_ data: Data?, status: String, defaults: UserDefaults) -> String? { | ||
| 70 | + guard let data else { return nil } | ||
| 71 | + let key = "watch_theme_\(themeId)_\(status)" | ||
| 72 | + defaults.set(data, forKey: key) | ||
| 73 | + return key | ||
| 74 | + } | ||
| 75 | +} | ||
| 76 | + | ||
| 77 | +private struct SyncedWatchThemeModel: Codable { | ||
| 78 | + let id: Int? | ||
| 79 | + let themeName: String? | ||
| 80 | + let isOfficial: Int? | ||
| 81 | + let energeticTitle: String? | ||
| 82 | + let energeticImageURL: String? | ||
| 83 | + let normalTitle: String? | ||
| 84 | + let normalImageURL: String? | ||
| 85 | + let slightStressTitle: String? | ||
| 86 | + let slightStressImageURL: String? | ||
| 87 | + let stressfulTitle: String? | ||
| 88 | + let stressfulImageURL: String? | ||
| 89 | + | ||
| 90 | + enum CodingKeys: String, CodingKey { | ||
| 91 | + case id | ||
| 92 | + case themeName = "theme_name" | ||
| 93 | + case isOfficial = "is_official" | ||
| 94 | + case energeticTitle = "energetic_description" | ||
| 95 | + case energeticImageURL = "energetic_image" | ||
| 96 | + case normalTitle = "normal_description" | ||
| 97 | + case normalImageURL = "normal_image" | ||
| 98 | + case slightStressTitle = "slight_stressful_description" | ||
| 99 | + case slightStressImageURL = "slight_stressful_image" | ||
| 100 | + case stressfulTitle = "stressful_description" | ||
| 101 | + case stressfulImageURL = "stressful_image" | ||
| 102 | + } | ||
| 103 | +} |
| 1 | import 'dart:async'; | 1 | import 'dart:async'; |
| 2 | -import 'dart:convert'; | ||
| 3 | 2 | ||
| 4 | import 'package:doublefeel_flutter/app/modules/friends/data/friends_repository.dart'; | 3 | import 'package:doublefeel_flutter/app/modules/friends/data/friends_repository.dart'; |
| 5 | import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_device_extension.dart'; | 4 | import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_device_extension.dart'; |
| 5 | +import 'package:doublefeel_flutter/app/modules/watch_theme/services/watch_theme_sync_payload_builder.dart'; | ||
| 6 | import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; | 6 | import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; |
| 7 | import 'package:doublefeel_flutter/core/network/api/theme_api.dart'; | 7 | import 'package:doublefeel_flutter/core/network/api/theme_api.dart'; |
| 8 | import 'package:doublefeel_flutter/core/result/app_result.dart'; | 8 | import 'package:doublefeel_flutter/core/result/app_result.dart'; |
| @@ -12,6 +12,7 @@ import 'package:doublefeel_flutter/pigeon/wear_engine_api.g.dart'; | @@ -12,6 +12,7 @@ import 'package:doublefeel_flutter/pigeon/wear_engine_api.g.dart'; | ||
| 12 | import 'package:flutter/material.dart'; | 12 | import 'package:flutter/material.dart'; |
| 13 | import 'package:get/get.dart'; | 13 | import 'package:get/get.dart'; |
| 14 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | 14 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; |
| 15 | +import 'package:logger/logger.dart'; | ||
| 15 | 16 | ||
| 16 | import '../../friends/models/friend_health_data.dart'; | 17 | import '../../friends/models/friend_health_data.dart'; |
| 17 | import '../../friends/views/select_friend_view.dart'; | 18 | import '../../friends/views/select_friend_view.dart'; |
| @@ -24,6 +25,8 @@ class CustomWatchThemePreviewController extends GetxController { | @@ -24,6 +25,8 @@ class CustomWatchThemePreviewController extends GetxController { | ||
| 24 | CustomWatchThemePreviewController(this._themeApi); | 25 | CustomWatchThemePreviewController(this._themeApi); |
| 25 | 26 | ||
| 26 | final ThemeApi _themeApi; | 27 | final ThemeApi _themeApi; |
| 28 | + final WatchThemeSyncPayloadBuilder _syncPayloadBuilder = | ||
| 29 | + const WatchThemeSyncPayloadBuilder(); | ||
| 27 | final FriendsRepository _repository = | 30 | final FriendsRepository _repository = |
| 28 | FriendsRepositoryImpl(Get.find<FriendApi>()); | 31 | FriendsRepositoryImpl(Get.find<FriendApi>()); |
| 29 | 32 | ||
| @@ -120,6 +123,7 @@ class CustomWatchThemePreviewController extends GetxController { | @@ -120,6 +123,7 @@ class CustomWatchThemePreviewController extends GetxController { | ||
| 120 | AppToast.show(l10n.watchThemeDeleteFailed); | 123 | AppToast.show(l10n.watchThemeDeleteFailed); |
| 121 | return; | 124 | return; |
| 122 | } | 125 | } |
| 126 | + await WearEngineHostApi().syncWatchTheme(null); | ||
| 123 | AppToast.show(l10n.watchThemeDeleted); | 127 | AppToast.show(l10n.watchThemeDeleted); |
| 124 | Get.back(); | 128 | Get.back(); |
| 125 | } | 129 | } |
| @@ -160,10 +164,8 @@ class CustomWatchThemePreviewController extends GetxController { | @@ -160,10 +164,8 @@ class CustomWatchThemePreviewController extends GetxController { | ||
| 160 | } | 164 | } |
| 161 | try { | 165 | try { |
| 162 | await WearEngineHostApi().addWatchSurface(); | 166 | await WearEngineHostApi().addWatchSurface(); |
| 163 | - await PlatformHostApi().nativeHandleUrl("itms-watchs://"); | ||
| 164 | - } catch (e) { | ||
| 165 | - AppToast.show(e.toString()); | ||
| 166 | - } | 167 | + } catch (e) {} |
| 168 | + await PlatformHostApi().nativeHandleUrl("itms-watchs://"); | ||
| 167 | } | 169 | } |
| 168 | 170 | ||
| 169 | Future<void> tryApplyAndSyncWatchFace() async { | 171 | Future<void> tryApplyAndSyncWatchFace() async { |
| @@ -201,20 +203,23 @@ class CustomWatchThemePreviewController extends GetxController { | @@ -201,20 +203,23 @@ class CustomWatchThemePreviewController extends GetxController { | ||
| 201 | } | 203 | } |
| 202 | 204 | ||
| 203 | isApplying.value = true; | 205 | isApplying.value = true; |
| 204 | - final result = await _themeApi.applyTheme(themeId); | ||
| 205 | - if (result is! AppSuccess<void>) { | ||
| 206 | - isApplying.value = false; | ||
| 207 | - AppToast.show(l10n.watchThemeApplyFailed); | ||
| 208 | - return false; | ||
| 209 | - } | ||
| 210 | - | ||
| 211 | - selectedThemeItem.value = themeItem; | ||
| 212 | 206 | ||
| 213 | var syncSuccess = true; | 207 | var syncSuccess = true; |
| 214 | try { | 208 | try { |
| 215 | - syncSuccess = await WearEngineHostApi().sendWatchSyncPayload( | ||
| 216 | - jsonEncode(themeItem.toJson()), | ||
| 217 | - ); | 209 | + final payload = await _syncPayloadBuilder.build(themeItem); |
| 210 | + final success = await WearEngineHostApi().syncWatchTheme(payload); | ||
| 211 | + if (success) { | ||
| 212 | + final result = await _themeApi.applyTheme(themeId); | ||
| 213 | + if (result is! AppSuccess<void>) { | ||
| 214 | + isApplying.value = false; | ||
| 215 | + AppToast.show(l10n.watchThemeApplyFailed); | ||
| 216 | + return false; | ||
| 217 | + } | ||
| 218 | + syncSuccess = success; | ||
| 219 | + selectedThemeItem.value = themeItem; | ||
| 220 | + return false; | ||
| 221 | + } | ||
| 222 | + syncSuccess = success; | ||
| 218 | } catch (_) { | 223 | } catch (_) { |
| 219 | // The active theme is already saved on the server. | 224 | // The active theme is already saved on the server. |
| 220 | syncSuccess = false; | 225 | syncSuccess = false; |
| 1 | import 'dart:async'; | 1 | import 'dart:async'; |
| 2 | -import 'dart:convert'; | ||
| 3 | 2 | ||
| 4 | import 'package:doublefeel_flutter/app/modules/friends/data/friends_repository.dart'; | 3 | import 'package:doublefeel_flutter/app/modules/friends/data/friends_repository.dart'; |
| 5 | import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_device_extension.dart'; | 4 | import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_device_extension.dart'; |
| 5 | +import 'package:doublefeel_flutter/app/modules/watch_theme/services/watch_theme_sync_payload_builder.dart'; | ||
| 6 | import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; | 6 | import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; |
| 7 | import 'package:doublefeel_flutter/core/network/api/theme_api.dart'; | 7 | import 'package:doublefeel_flutter/core/network/api/theme_api.dart'; |
| 8 | import 'package:doublefeel_flutter/core/result/app_result.dart'; | 8 | import 'package:doublefeel_flutter/core/result/app_result.dart'; |
| @@ -23,6 +23,8 @@ class WatchThemePreviewController extends GetxController { | @@ -23,6 +23,8 @@ class WatchThemePreviewController extends GetxController { | ||
| 23 | WatchThemePreviewController(this._themeApi); | 23 | WatchThemePreviewController(this._themeApi); |
| 24 | 24 | ||
| 25 | final ThemeApi _themeApi; | 25 | final ThemeApi _themeApi; |
| 26 | + final WatchThemeSyncPayloadBuilder _syncPayloadBuilder = | ||
| 27 | + const WatchThemeSyncPayloadBuilder(); | ||
| 26 | final FriendsRepository _repository = | 28 | final FriendsRepository _repository = |
| 27 | FriendsRepositoryImpl(Get.find<FriendApi>()); | 29 | FriendsRepositoryImpl(Get.find<FriendApi>()); |
| 28 | 30 | ||
| @@ -132,10 +134,10 @@ class WatchThemePreviewController extends GetxController { | @@ -132,10 +134,10 @@ class WatchThemePreviewController extends GetxController { | ||
| 132 | 134 | ||
| 133 | try { | 135 | try { |
| 134 | await WearEngineHostApi().addWatchSurface(); | 136 | await WearEngineHostApi().addWatchSurface(); |
| 135 | - await PlatformHostApi().nativeHandleUrl("itms-watchs://"); | ||
| 136 | } catch (e) { | 137 | } catch (e) { |
| 137 | - AppToast.show(e.toString()); | 138 | + |
| 138 | } | 139 | } |
| 140 | + await PlatformHostApi().nativeHandleUrl("itms-watchs://"); | ||
| 139 | } | 141 | } |
| 140 | 142 | ||
| 141 | Future<void> tryApplyAndSyncWatchFace() async { | 143 | Future<void> tryApplyAndSyncWatchFace() async { |
| @@ -173,20 +175,23 @@ class WatchThemePreviewController extends GetxController { | @@ -173,20 +175,23 @@ class WatchThemePreviewController extends GetxController { | ||
| 173 | } | 175 | } |
| 174 | 176 | ||
| 175 | isApplying.value = true; | 177 | isApplying.value = true; |
| 176 | - final result = await _themeApi.applyTheme(themeId); | ||
| 177 | - if (result is! AppSuccess<void>) { | ||
| 178 | - isApplying.value = false; | ||
| 179 | - AppToast.show(l10n.watchThemeApplyFailed); | ||
| 180 | - return false; | ||
| 181 | - } | ||
| 182 | - | ||
| 183 | - selectedThemeItem.value = themeItem; | ||
| 184 | 178 | ||
| 185 | var syncSuccess = true; | 179 | var syncSuccess = true; |
| 186 | try { | 180 | try { |
| 187 | - syncSuccess = await WearEngineHostApi().sendWatchSyncPayload( | ||
| 188 | - jsonEncode(themeItem.toJson()), | ||
| 189 | - ); | 181 | + final payload = await _syncPayloadBuilder.build(themeItem); |
| 182 | + final success = await WearEngineHostApi().syncWatchTheme(payload); | ||
| 183 | + if (success) { | ||
| 184 | + final result = await _themeApi.applyTheme(themeId); | ||
| 185 | + if (result is! AppSuccess<void>) { | ||
| 186 | + isApplying.value = false; | ||
| 187 | + AppToast.show(l10n.watchThemeApplyFailed); | ||
| 188 | + return false; | ||
| 189 | + } | ||
| 190 | + syncSuccess = success; | ||
| 191 | + selectedThemeItem.value = themeItem; | ||
| 192 | + return false; | ||
| 193 | + } | ||
| 194 | + syncSuccess = success; | ||
| 190 | } catch (_) { | 195 | } catch (_) { |
| 191 | // The active theme is already saved on the server. | 196 | // The active theme is already saved on the server. |
| 192 | syncSuccess = false; | 197 | syncSuccess = false; |
| 1 | +import 'dart:io'; | ||
| 2 | + | ||
| 3 | +import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_theme_models.dart'; | ||
| 4 | +import 'package:doublefeel_flutter/pigeon/wear_engine_api.g.dart'; | ||
| 5 | +import 'package:flutter/foundation.dart'; | ||
| 6 | +import 'package:flutter/services.dart'; | ||
| 7 | + | ||
| 8 | +class WatchThemeSyncPayloadBuilder { | ||
| 9 | + const WatchThemeSyncPayloadBuilder(); | ||
| 10 | + | ||
| 11 | + Future<WatchTransformTheme> build(WatchThemeItem item) async { | ||
| 12 | + return WatchTransformTheme( | ||
| 13 | + themeId: item.id ?? 0, | ||
| 14 | + themeName: item.themeName, | ||
| 15 | + energeticDescription: item.energeticDescription, | ||
| 16 | + energeticImage: await _loadImageBytes(item.energeticImage), | ||
| 17 | + normalDescription: item.normalDescription, | ||
| 18 | + normalImage: await _loadImageBytes(item.normalImage), | ||
| 19 | + slightStressfulDescription: item.slightStressfulDescription, | ||
| 20 | + slightStressfulImage: await _loadImageBytes(item.slightStressfulImage), | ||
| 21 | + stressfulDescription: item.stressfulDescription, | ||
| 22 | + stressfulImage: await _loadImageBytes(item.stressfulImage), | ||
| 23 | + ); | ||
| 24 | + } | ||
| 25 | + | ||
| 26 | + Future<Uint8List?> _loadImageBytes(String? imagePath) async { | ||
| 27 | + final value = imagePath?.trim(); | ||
| 28 | + if (value == null || value.isEmpty) return null; | ||
| 29 | + | ||
| 30 | + final uri = Uri.tryParse(value); | ||
| 31 | + if (uri != null && (uri.scheme == 'http' || uri.scheme == 'https')) { | ||
| 32 | + return _downloadImage(uri); | ||
| 33 | + } | ||
| 34 | + | ||
| 35 | + final file = File(value); | ||
| 36 | + if (await file.exists()) { | ||
| 37 | + return file.readAsBytes(); | ||
| 38 | + } | ||
| 39 | + | ||
| 40 | + try { | ||
| 41 | + final data = await rootBundle.load(value); | ||
| 42 | + return data.buffer.asUint8List(); | ||
| 43 | + } catch (_) { | ||
| 44 | + return null; | ||
| 45 | + } | ||
| 46 | + } | ||
| 47 | + | ||
| 48 | + Future<Uint8List?> _downloadImage(Uri uri) async { | ||
| 49 | + final client = HttpClient(); | ||
| 50 | + try { | ||
| 51 | + final request = await client.getUrl(uri); | ||
| 52 | + final response = await request.close(); | ||
| 53 | + if (response.statusCode < 200 || response.statusCode >= 300) { | ||
| 54 | + return null; | ||
| 55 | + } | ||
| 56 | + return consolidateHttpClientResponseBytes(response); | ||
| 57 | + } catch (_) { | ||
| 58 | + return null; | ||
| 59 | + } finally { | ||
| 60 | + client.close(force: true); | ||
| 61 | + } | ||
| 62 | + } | ||
| 63 | +} |
| @@ -55,6 +55,7 @@ class OfficialThemeGrid extends StatelessWidget { | @@ -55,6 +55,7 @@ class OfficialThemeGrid extends StatelessWidget { | ||
| 55 | size: 88, | 55 | size: 88, |
| 56 | child: WatchThemeCharacterAvatar( | 56 | child: WatchThemeCharacterAvatar( |
| 57 | size: 88, | 57 | size: 88, |
| 58 | + padding: 8, | ||
| 58 | assetPath: theme.infoList.firstOrNull?.assetPath, | 59 | assetPath: theme.infoList.firstOrNull?.assetPath, |
| 59 | imageUrl: theme.infoList.firstOrNull?.imgUrl, | 60 | imageUrl: theme.infoList.firstOrNull?.imgUrl, |
| 60 | ), | 61 | ), |
| @@ -4,12 +4,14 @@ class WatchThemeCharacterAvatar extends StatelessWidget { | @@ -4,12 +4,14 @@ class WatchThemeCharacterAvatar extends StatelessWidget { | ||
| 4 | const WatchThemeCharacterAvatar({ | 4 | const WatchThemeCharacterAvatar({ |
| 5 | super.key, | 5 | super.key, |
| 6 | required this.size, | 6 | required this.size, |
| 7 | + this.padding = 0, | ||
| 7 | this.assetPath, | 8 | this.assetPath, |
| 8 | this.imageUrl, | 9 | this.imageUrl, |
| 9 | this.empty = false, | 10 | this.empty = false, |
| 10 | }); | 11 | }); |
| 11 | 12 | ||
| 12 | final double size; | 13 | final double size; |
| 14 | + final double padding; | ||
| 13 | final String? assetPath; | 15 | final String? assetPath; |
| 14 | final String? imageUrl; | 16 | final String? imageUrl; |
| 15 | final bool empty; | 17 | final bool empty; |
| @@ -18,24 +20,23 @@ class WatchThemeCharacterAvatar extends StatelessWidget { | @@ -18,24 +20,23 @@ class WatchThemeCharacterAvatar extends StatelessWidget { | ||
| 18 | Widget build(BuildContext context) { | 20 | Widget build(BuildContext context) { |
| 19 | final avatarSize = size; | 21 | final avatarSize = size; |
| 20 | return SizedBox( | 22 | return SizedBox( |
| 21 | - width: avatarSize, | ||
| 22 | - height: avatarSize, | 23 | + width: avatarSize, |
| 24 | + height: avatarSize, | ||
| 23 | child: ClipRRect( | 25 | child: ClipRRect( |
| 24 | borderRadius: BorderRadius.circular(16), | 26 | borderRadius: BorderRadius.circular(16), |
| 25 | child: Container( | 27 | child: Container( |
| 26 | - width: avatarSize, | 28 | + width: avatarSize, |
| 27 | height: avatarSize, | 29 | height: avatarSize, |
| 28 | - alignment: Alignment.center, | ||
| 29 | - child: empty || (assetPath == null && imageUrl == null) | ||
| 30 | - ? const SizedBox.shrink() | ||
| 31 | - : _buildImage(avatarSize), | ||
| 32 | - ), | ||
| 33 | - ) | ||
| 34 | - ); | 30 | + alignment: Alignment.center, |
| 31 | + child: empty || (assetPath == null && imageUrl == null) | ||
| 32 | + ? const SizedBox.shrink() | ||
| 33 | + : _buildImage(avatarSize), | ||
| 34 | + ), | ||
| 35 | + )); | ||
| 35 | } | 36 | } |
| 36 | 37 | ||
| 37 | Widget _buildImage(double avatarSize) { | 38 | Widget _buildImage(double avatarSize) { |
| 38 | - final width = avatarSize; | 39 | + final width = avatarSize - padding * 2; |
| 39 | if (imageUrl != null && imageUrl!.isNotEmpty) { | 40 | if (imageUrl != null && imageUrl!.isNotEmpty) { |
| 40 | return Image.network( | 41 | return Image.network( |
| 41 | imageUrl!, | 42 | imageUrl!, |
| @@ -28,6 +28,9 @@ class _WatchThemeSyncDialogState extends State<WatchThemeSyncDialog> { | @@ -28,6 +28,9 @@ class _WatchThemeSyncDialogState extends State<WatchThemeSyncDialog> { | ||
| 28 | if (_state == _WatchThemeSyncState.syncing) return; | 28 | if (_state == _WatchThemeSyncState.syncing) return; |
| 29 | setState(() => _state = _WatchThemeSyncState.syncing); | 29 | setState(() => _state = _WatchThemeSyncState.syncing); |
| 30 | final success = await widget.onSync(); | 30 | final success = await widget.onSync(); |
| 31 | + if (success) { | ||
| 32 | + await Future<void>.delayed(const Duration(seconds: 1)); | ||
| 33 | + } | ||
| 31 | if (!mounted) return; | 34 | if (!mounted) return; |
| 32 | setState(() { | 35 | setState(() { |
| 33 | _success = success; | 36 | _success = success; |
| @@ -48,46 +48,4 @@ class WearEngineService { | @@ -48,46 +48,4 @@ class WearEngineService { | ||
| 48 | return result is AppSuccess; | 48 | return result is AppSuccess; |
| 49 | } | 49 | } |
| 50 | 50 | ||
| 51 | - Future<bool> syncWatchUserPayload({required bool isOther}) async { | ||
| 52 | - if (!isAndroid) { | ||
| 53 | - return false; | ||
| 54 | - } | ||
| 55 | - final device = await _wearEngineHost.checkConnectedDevice(); | ||
| 56 | - if (device == null) { | ||
| 57 | - return false; | ||
| 58 | - } | ||
| 59 | - | ||
| 60 | - final today = await _healthApi.getTodayData(isOther: isOther); | ||
| 61 | - final prefs = _userPrefs.preferences.value; | ||
| 62 | - final me = prefs.meUserInfo; | ||
| 63 | - final partner = prefs.partnerUserInfo; | ||
| 64 | - | ||
| 65 | - int hrvValue = 0; | ||
| 66 | - int heartRate = 0; | ||
| 67 | - int steps = 0; | ||
| 68 | - | ||
| 69 | - if (today case AppSuccess(data: final todayData)) { | ||
| 70 | - if (todayData.hrvDataList?.isNotEmpty == true) { | ||
| 71 | - hrvValue = todayData.hrvDataList!.first.value?.toInt() ?? 0; | ||
| 72 | - } | ||
| 73 | - heartRate = todayData.recentData?.heartRate ?? 0; | ||
| 74 | - steps = todayData.recentData?.steps ?? 0; | ||
| 75 | - } | ||
| 76 | - | ||
| 77 | - final payload = jsonEncode({ | ||
| 78 | - 'dataType': 1, | ||
| 79 | - 'isLogin': _userPrefs.accessToken.isNotEmpty, | ||
| 80 | - 'isBound': (partner?.pairId ?? 0) > 0, | ||
| 81 | - 'isVip': prefs.vipInfo?.isVip ?? false, | ||
| 82 | - 'meCharacter': me?.persona ?? UserCharacter.cat.id, | ||
| 83 | - 'meHrv': hrvValue, | ||
| 84 | - 'meHeartRate': heartRate, | ||
| 85 | - 'meSteps': steps, | ||
| 86 | - 'taCharacter': partner?.persona ?? UserCharacter.cat.id, | ||
| 87 | - }); | ||
| 88 | - | ||
| 89 | - final ok = await _wearEngineHost.sendWatchSyncPayload(payload); | ||
| 90 | - AppLogger.i('WearEngineService.syncWatchUserPayload ok=$ok'); | ||
| 91 | - return ok; | ||
| 92 | - } | ||
| 93 | } | 51 | } |
| @@ -421,6 +421,7 @@ class HealthKitHostApi { | @@ -421,6 +421,7 @@ class HealthKitHostApi { | ||
| 421 | } | 421 | } |
| 422 | } | 422 | } |
| 423 | 423 | ||
| 424 | + /// 获取当前已上传的健康数据点,主要用于调试 | ||
| 424 | /// Opens Huawei Health client authorization UI. Returns whether user granted. | 425 | /// Opens Huawei Health client authorization UI. Returns whether user granted. |
| 425 | Future<HealthAuthorization> checkHealthAppAuthorization() async { | 426 | Future<HealthAuthorization> checkHealthAppAuthorization() async { |
| 426 | final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.checkHealthAppAuthorization$pigeonVar_messageChannelSuffix'; | 427 | final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.checkHealthAppAuthorization$pigeonVar_messageChannelSuffix'; |
| @@ -91,6 +91,92 @@ class WearDeviceInfo { | @@ -91,6 +91,92 @@ class WearDeviceInfo { | ||
| 91 | ; | 91 | ; |
| 92 | } | 92 | } |
| 93 | 93 | ||
| 94 | +class WatchTransformTheme { | ||
| 95 | + WatchTransformTheme({ | ||
| 96 | + required this.themeId, | ||
| 97 | + required this.themeName, | ||
| 98 | + required this.energeticDescription, | ||
| 99 | + this.energeticImage, | ||
| 100 | + required this.normalDescription, | ||
| 101 | + this.normalImage, | ||
| 102 | + required this.slightStressfulDescription, | ||
| 103 | + this.slightStressfulImage, | ||
| 104 | + required this.stressfulDescription, | ||
| 105 | + this.stressfulImage, | ||
| 106 | + }); | ||
| 107 | + | ||
| 108 | + int themeId; | ||
| 109 | + | ||
| 110 | + String themeName; | ||
| 111 | + | ||
| 112 | + String energeticDescription; | ||
| 113 | + | ||
| 114 | + Uint8List? energeticImage; | ||
| 115 | + | ||
| 116 | + String normalDescription; | ||
| 117 | + | ||
| 118 | + Uint8List? normalImage; | ||
| 119 | + | ||
| 120 | + String slightStressfulDescription; | ||
| 121 | + | ||
| 122 | + Uint8List? slightStressfulImage; | ||
| 123 | + | ||
| 124 | + String stressfulDescription; | ||
| 125 | + | ||
| 126 | + Uint8List? stressfulImage; | ||
| 127 | + | ||
| 128 | + List<Object?> _toList() { | ||
| 129 | + return <Object?>[ | ||
| 130 | + themeId, | ||
| 131 | + themeName, | ||
| 132 | + energeticDescription, | ||
| 133 | + energeticImage, | ||
| 134 | + normalDescription, | ||
| 135 | + normalImage, | ||
| 136 | + slightStressfulDescription, | ||
| 137 | + slightStressfulImage, | ||
| 138 | + stressfulDescription, | ||
| 139 | + stressfulImage, | ||
| 140 | + ]; | ||
| 141 | + } | ||
| 142 | + | ||
| 143 | + Object encode() { | ||
| 144 | + return _toList(); } | ||
| 145 | + | ||
| 146 | + static WatchTransformTheme decode(Object result) { | ||
| 147 | + result as List<Object?>; | ||
| 148 | + return WatchTransformTheme( | ||
| 149 | + themeId: result[0]! as int, | ||
| 150 | + themeName: result[1]! as String, | ||
| 151 | + energeticDescription: result[2]! as String, | ||
| 152 | + energeticImage: result[3] as Uint8List?, | ||
| 153 | + normalDescription: result[4]! as String, | ||
| 154 | + normalImage: result[5] as Uint8List?, | ||
| 155 | + slightStressfulDescription: result[6]! as String, | ||
| 156 | + slightStressfulImage: result[7] as Uint8List?, | ||
| 157 | + stressfulDescription: result[8]! as String, | ||
| 158 | + stressfulImage: result[9] as Uint8List?, | ||
| 159 | + ); | ||
| 160 | + } | ||
| 161 | + | ||
| 162 | + @override | ||
| 163 | + // ignore: avoid_equals_and_hash_code_on_mutable_classes | ||
| 164 | + bool operator ==(Object other) { | ||
| 165 | + if (other is! WatchTransformTheme || other.runtimeType != runtimeType) { | ||
| 166 | + return false; | ||
| 167 | + } | ||
| 168 | + if (identical(this, other)) { | ||
| 169 | + return true; | ||
| 170 | + } | ||
| 171 | + return _deepEquals(encode(), other.encode()); | ||
| 172 | + } | ||
| 173 | + | ||
| 174 | + @override | ||
| 175 | + // ignore: avoid_equals_and_hash_code_on_mutable_classes | ||
| 176 | + int get hashCode => Object.hashAll(_toList()) | ||
| 177 | +; | ||
| 178 | +} | ||
| 179 | + | ||
| 94 | 180 | ||
| 95 | class _PigeonCodec extends StandardMessageCodec { | 181 | class _PigeonCodec extends StandardMessageCodec { |
| 96 | const _PigeonCodec(); | 182 | const _PigeonCodec(); |
| @@ -102,6 +188,9 @@ class _PigeonCodec extends StandardMessageCodec { | @@ -102,6 +188,9 @@ class _PigeonCodec extends StandardMessageCodec { | ||
| 102 | } else if (value is WearDeviceInfo) { | 188 | } else if (value is WearDeviceInfo) { |
| 103 | buffer.putUint8(129); | 189 | buffer.putUint8(129); |
| 104 | writeValue(buffer, value.encode()); | 190 | writeValue(buffer, value.encode()); |
| 191 | + } else if (value is WatchTransformTheme) { | ||
| 192 | + buffer.putUint8(130); | ||
| 193 | + writeValue(buffer, value.encode()); | ||
| 105 | } else { | 194 | } else { |
| 106 | super.writeValue(buffer, value); | 195 | super.writeValue(buffer, value); |
| 107 | } | 196 | } |
| @@ -112,6 +201,8 @@ class _PigeonCodec extends StandardMessageCodec { | @@ -112,6 +201,8 @@ class _PigeonCodec extends StandardMessageCodec { | ||
| 112 | switch (type) { | 201 | switch (type) { |
| 113 | case 129: | 202 | case 129: |
| 114 | return WearDeviceInfo.decode(readValue(buffer)!); | 203 | return WearDeviceInfo.decode(readValue(buffer)!); |
| 204 | + case 130: | ||
| 205 | + return WatchTransformTheme.decode(readValue(buffer)!); | ||
| 115 | default: | 206 | default: |
| 116 | return super.readValueOfType(type, buffer); | 207 | return super.readValueOfType(type, buffer); |
| 117 | } | 208 | } |
| @@ -267,14 +358,15 @@ class WearEngineHostApi { | @@ -267,14 +358,15 @@ class WearEngineHostApi { | ||
| 267 | } | 358 | } |
| 268 | } | 359 | } |
| 269 | 360 | ||
| 270 | - Future<bool> sendWatchSyncPayload(String jsonPayload) async { | ||
| 271 | - final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.sendWatchSyncPayload$pigeonVar_messageChannelSuffix'; | 361 | + /// 添加或者删除主题 |
| 362 | + Future<bool> syncWatchTheme(WatchTransformTheme? theme) async { | ||
| 363 | + final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.syncWatchTheme$pigeonVar_messageChannelSuffix'; | ||
| 272 | final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>( | 364 | final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>( |
| 273 | pigeonVar_channelName, | 365 | pigeonVar_channelName, |
| 274 | pigeonChannelCodec, | 366 | pigeonChannelCodec, |
| 275 | binaryMessenger: pigeonVar_binaryMessenger, | 367 | binaryMessenger: pigeonVar_binaryMessenger, |
| 276 | ); | 368 | ); |
| 277 | - final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[jsonPayload]); | 369 | + final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[theme]); |
| 278 | final List<Object?>? pigeonVar_replyList = | 370 | final List<Object?>? pigeonVar_replyList = |
| 279 | await pigeonVar_sendFuture as List<Object?>?; | 371 | await pigeonVar_sendFuture as List<Object?>?; |
| 280 | if (pigeonVar_replyList == null) { | 372 | if (pigeonVar_replyList == null) { |
| @@ -83,6 +83,9 @@ abstract class HealthKitHostApi { | @@ -83,6 +83,9 @@ abstract class HealthKitHostApi { | ||
| 83 | @async | 83 | @async |
| 84 | HealthUploadResult performHealthUpload(); | 84 | HealthUploadResult performHealthUpload(); |
| 85 | 85 | ||
| 86 | + /// 获取当前已上传的健康数据点,主要用于调试 | ||
| 87 | + // List<HealthUploadDataPoint> getDebugCurrentUploadedData(); | ||
| 88 | + | ||
| 86 | // @required | 89 | // @required |
| 87 | 90 | ||
| 88 | /// Opens Huawei Health client authorization UI. Returns whether user granted. | 91 | /// Opens Huawei Health client authorization UI. Returns whether user granted. |
| @@ -16,6 +16,32 @@ class WearDeviceInfo { | @@ -16,6 +16,32 @@ class WearDeviceInfo { | ||
| 16 | bool? isWatchAppInstalled; | 16 | bool? isWatchAppInstalled; |
| 17 | } | 17 | } |
| 18 | 18 | ||
| 19 | +class WatchTransformTheme { | ||
| 20 | + WatchTransformTheme({ | ||
| 21 | + required this.themeId, | ||
| 22 | + required this.themeName, | ||
| 23 | + required this.energeticDescription, | ||
| 24 | + this.energeticImage, | ||
| 25 | + required this.normalDescription, | ||
| 26 | + this.normalImage, | ||
| 27 | + required this.slightStressfulDescription, | ||
| 28 | + this.slightStressfulImage, | ||
| 29 | + required this.stressfulDescription, | ||
| 30 | + this.stressfulImage, | ||
| 31 | + }); | ||
| 32 | + | ||
| 33 | + int themeId; | ||
| 34 | + final String themeName; | ||
| 35 | + final String energeticDescription; | ||
| 36 | + final Uint8List? energeticImage; | ||
| 37 | + final String normalDescription; | ||
| 38 | + final Uint8List? normalImage; | ||
| 39 | + final String slightStressfulDescription; | ||
| 40 | + final Uint8List? slightStressfulImage; | ||
| 41 | + final String stressfulDescription; | ||
| 42 | + final Uint8List? stressfulImage; | ||
| 43 | +} | ||
| 44 | + | ||
| 19 | @ConfigurePigeon( | 45 | @ConfigurePigeon( |
| 20 | PigeonOptions( | 46 | PigeonOptions( |
| 21 | dartOut: 'lib/pigeon/wear_engine_api.g.dart', | 47 | dartOut: 'lib/pigeon/wear_engine_api.g.dart', |
| @@ -47,8 +73,9 @@ abstract class WearEngineHostApi { | @@ -47,8 +73,9 @@ abstract class WearEngineHostApi { | ||
| 47 | @async | 73 | @async |
| 48 | bool addWatchSurface(); | 74 | bool addWatchSurface(); |
| 49 | 75 | ||
| 76 | +/// 添加或者删除主题 | ||
| 50 | @async | 77 | @async |
| 51 | - bool sendWatchSyncPayload(String jsonPayload); | 78 | + bool syncWatchTheme(WatchTransformTheme? theme); |
| 52 | 79 | ||
| 53 | /// Opens the system photo picker and returns a local PNG file path after | 80 | /// Opens the system photo picker and returns a local PNG file path after |
| 54 | /// removing the image background on the host platform. | 81 | /// removing the image background on the host platform. |
-
Please register or login to post a comment