Commit 87e39b965578ac2ab9ee475cf30a79d698bf109c

Authored by 权海
1 parent cb2e56ad

feat(ui):增加bridge一些接口,增加原生的debug环境回传flutter(切环境)

Showing 48 changed files with 768 additions and 344 deletions
@@ -54,6 +54,12 @@ @@ -54,6 +54,12 @@
54 @import sqflite_darwin; 54 @import sqflite_darwin;
55 #endif 55 #endif
56 56
  57 +#if __has_include(<video_thumbnail/VideoThumbnailPlugin.h>)
  58 +#import <video_thumbnail/VideoThumbnailPlugin.h>
  59 +#else
  60 +@import video_thumbnail;
  61 +#endif
  62 +
57 #if __has_include(<webview_flutter_wkwebview/WebViewFlutterPlugin.h>) 63 #if __has_include(<webview_flutter_wkwebview/WebViewFlutterPlugin.h>)
58 #import <webview_flutter_wkwebview/WebViewFlutterPlugin.h> 64 #import <webview_flutter_wkwebview/WebViewFlutterPlugin.h>
59 #else 65 #else
@@ -71,6 +77,7 @@ @@ -71,6 +77,7 @@
71 [FPPSharePlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPSharePlusPlugin"]]; 77 [FPPSharePlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPSharePlusPlugin"]];
72 [SharedPreferencesPlugin registerWithRegistrar:[registry registrarForPlugin:@"SharedPreferencesPlugin"]]; 78 [SharedPreferencesPlugin registerWithRegistrar:[registry registrarForPlugin:@"SharedPreferencesPlugin"]];
73 [SqflitePlugin registerWithRegistrar:[registry registrarForPlugin:@"SqflitePlugin"]]; 79 [SqflitePlugin registerWithRegistrar:[registry registrarForPlugin:@"SqflitePlugin"]];
  80 + [VideoThumbnailPlugin registerWithRegistrar:[registry registrarForPlugin:@"VideoThumbnailPlugin"]];
74 [WebViewFlutterPlugin registerWithRegistrar:[registry registrarForPlugin:@"WebViewFlutterPlugin"]]; 81 [WebViewFlutterPlugin registerWithRegistrar:[registry registrarForPlugin:@"WebViewFlutterPlugin"]];
75 } 82 }
76 83
@@ -79,6 +79,17 @@ class FlutterError ( @@ -79,6 +79,17 @@ class FlutterError (
79 val details: Any? = null 79 val details: Any? = null
80 ) : Throwable() 80 ) : Throwable()
81 81
  82 +enum class HResourceType(val raw: Int) {
  83 + IMAGE(0),
  84 + VIDEO(1);
  85 +
  86 + companion object {
  87 + fun ofRaw(raw: Int): HResourceType? {
  88 + return values().firstOrNull { it.raw == raw }
  89 + }
  90 + }
  91 +}
  92 +
82 /** Generated class from Pigeon that represents data sent in messages. */ 93 /** Generated class from Pigeon that represents data sent in messages. */
83 data class AppleSignInModel ( 94 data class AppleSignInModel (
84 val userId: String, 95 val userId: String,
@@ -265,21 +276,26 @@ private open class PlatformApiPigeonCodec : StandardMessageCodec() { @@ -265,21 +276,26 @@ private open class PlatformApiPigeonCodec : StandardMessageCodec() {
265 override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { 276 override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
266 return when (type) { 277 return when (type) {
267 129.toByte() -> { 278 129.toByte() -> {
  279 + return (readValue(buffer) as Long?)?.let {
  280 + HResourceType.ofRaw(it.toInt())
  281 + }
  282 + }
  283 + 130.toByte() -> {
268 return (readValue(buffer) as? List<Any?>)?.let { 284 return (readValue(buffer) as? List<Any?>)?.let {
269 AppleSignInModel.fromList(it) 285 AppleSignInModel.fromList(it)
270 } 286 }
271 } 287 }
272 - 130.toByte() -> { 288 + 131.toByte() -> {
273 return (readValue(buffer) as? List<Any?>)?.let { 289 return (readValue(buffer) as? List<Any?>)?.let {
274 WatchAppOtherInfo.fromList(it) 290 WatchAppOtherInfo.fromList(it)
275 } 291 }
276 } 292 }
277 - 131.toByte() -> { 293 + 132.toByte() -> {
278 return (readValue(buffer) as? List<Any?>)?.let { 294 return (readValue(buffer) as? List<Any?>)?.let {
279 AppleProductInfo.fromList(it) 295 AppleProductInfo.fromList(it)
280 } 296 }
281 } 297 }
282 - 132.toByte() -> { 298 + 133.toByte() -> {
283 return (readValue(buffer) as? List<Any?>)?.let { 299 return (readValue(buffer) as? List<Any?>)?.let {
284 AppleProductPaymentResult.fromList(it) 300 AppleProductPaymentResult.fromList(it)
285 } 301 }
@@ -289,20 +305,24 @@ private open class PlatformApiPigeonCodec : StandardMessageCodec() { @@ -289,20 +305,24 @@ private open class PlatformApiPigeonCodec : StandardMessageCodec() {
289 } 305 }
290 override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { 306 override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
291 when (value) { 307 when (value) {
292 - is AppleSignInModel -> { 308 + is HResourceType -> {
293 stream.write(129) 309 stream.write(129)
  310 + writeValue(stream, value.raw)
  311 + }
  312 + is AppleSignInModel -> {
  313 + stream.write(130)
294 writeValue(stream, value.toList()) 314 writeValue(stream, value.toList())
295 } 315 }
296 is WatchAppOtherInfo -> { 316 is WatchAppOtherInfo -> {
297 - stream.write(130) 317 + stream.write(131)
298 writeValue(stream, value.toList()) 318 writeValue(stream, value.toList())
299 } 319 }
300 is AppleProductInfo -> { 320 is AppleProductInfo -> {
301 - stream.write(131) 321 + stream.write(132)
302 writeValue(stream, value.toList()) 322 writeValue(stream, value.toList())
303 } 323 }
304 is AppleProductPaymentResult -> { 324 is AppleProductPaymentResult -> {
305 - stream.write(132) 325 + stream.write(133)
306 writeValue(stream, value.toList()) 326 writeValue(stream, value.toList())
307 } 327 }
308 else -> super.writeValue(stream, value) 328 else -> super.writeValue(stream, value)
@@ -318,6 +338,8 @@ interface PlatformHostApi { @@ -318,6 +338,8 @@ interface PlatformHostApi {
318 * `{systemWebViewUA} {appName}/{versionCode}({versionName})({manufacturer}##{brand}##{model}; {OS}{osVersion}; {height}x{width})(huawei)` 338 * `{systemWebViewUA} {appName}/{versionCode}({versionName})({manufacturer}##{brand}##{model}; {OS}{osVersion}; {height}x{width})(huawei)`
319 */ 339 */
320 fun getFullUserAgent(): String 340 fun getFullUserAgent(): String
  341 + /** 是否是测试环境 */
  342 + fun isDebugEnvoriment(): Boolean
321 /** 343 /**
322 * 更新用户信息, 有登录态后调用 344 * 更新用户信息, 有登录态后调用
323 * jsonString: UserPreferences的序列化string 345 * jsonString: UserPreferences的序列化string
@@ -329,10 +351,12 @@ interface PlatformHostApi { @@ -329,10 +351,12 @@ interface PlatformHostApi {
329 /** 刷新会员信息 */ 351 /** 刷新会员信息 */
330 fun refreshVip() 352 fun refreshVip()
331 /** 353 /**
332 - * 刷新watch app 和 表盘的所有数据: 354 + * 刷新watch app 和 表盘的所有数据:
333 * 任何可能导致展示watch内容变化时调用(好友变化、某些设置导致变化等) 355 * 任何可能导致展示watch内容变化时调用(好友变化、某些设置导致变化等)
334 */ 356 */
335 fun refreshWatchAppAndWidgets() 357 fun refreshWatchAppAndWidgets()
  358 + /** 请求评分弹窗 */
  359 + fun requestAppReview(callback: (Result<Boolean>) -> Unit)
336 /** 请求苹果登录 */ 360 /** 请求苹果登录 */
337 fun requestAppleSignIn(callback: (Result<AppleSignInModel?>) -> Unit) 361 fun requestAppleSignIn(callback: (Result<AppleSignInModel?>) -> Unit)
338 /** 362 /**
@@ -345,6 +369,11 @@ interface PlatformHostApi { @@ -345,6 +369,11 @@ interface PlatformHostApi {
345 fun performApplePayment(productId: String, uuid: String, callback: (Result<AppleProductPaymentResult?>) -> Unit) 369 fun performApplePayment(productId: String, uuid: String, callback: (Result<AppleProductPaymentResult?>) -> Unit)
346 /** 恢复购买 */ 370 /** 恢复购买 */
347 fun performRestore(callback: (Result<Boolean>) -> Unit) 371 fun performRestore(callback: (Result<Boolean>) -> Unit)
  372 + /**
  373 + * 上传文件到云端
  374 + * 注意catch flutter error
  375 + */
  376 + fun uploadFile(filePath: String, resourceType: HResourceType, callback: (Result<String?>) -> Unit)
348 377
349 companion object { 378 companion object {
350 /** The codec used by PlatformHostApi. */ 379 /** The codec used by PlatformHostApi. */
@@ -371,6 +400,21 @@ interface PlatformHostApi { @@ -371,6 +400,21 @@ interface PlatformHostApi {
371 } 400 }
372 } 401 }
373 run { 402 run {
  403 + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.isDebugEnvoriment$separatedMessageChannelSuffix", codec)
  404 + if (api != null) {
  405 + channel.setMessageHandler { _, reply ->
  406 + val wrapped: List<Any?> = try {
  407 + listOf(api.isDebugEnvoriment())
  408 + } catch (exception: Throwable) {
  409 + PlatformApiPigeonUtils.wrapError(exception)
  410 + }
  411 + reply.reply(wrapped)
  412 + }
  413 + } else {
  414 + channel.setMessageHandler(null)
  415 + }
  416 + }
  417 + run {
374 val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.updateLoginInfo$separatedMessageChannelSuffix", codec) 418 val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.updateLoginInfo$separatedMessageChannelSuffix", codec)
375 if (api != null) { 419 if (api != null) {
376 channel.setMessageHandler { message, reply -> 420 channel.setMessageHandler { message, reply ->
@@ -438,6 +482,24 @@ interface PlatformHostApi { @@ -438,6 +482,24 @@ interface PlatformHostApi {
438 } 482 }
439 } 483 }
440 run { 484 run {
  485 + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.requestAppReview$separatedMessageChannelSuffix", codec)
  486 + if (api != null) {
  487 + channel.setMessageHandler { _, reply ->
  488 + api.requestAppReview{ result: Result<Boolean> ->
  489 + val error = result.exceptionOrNull()
  490 + if (error != null) {
  491 + reply.reply(PlatformApiPigeonUtils.wrapError(error))
  492 + } else {
  493 + val data = result.getOrNull()
  494 + reply.reply(PlatformApiPigeonUtils.wrapResult(data))
  495 + }
  496 + }
  497 + }
  498 + } else {
  499 + channel.setMessageHandler(null)
  500 + }
  501 + }
  502 + run {
441 val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.requestAppleSignIn$separatedMessageChannelSuffix", codec) 503 val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.requestAppleSignIn$separatedMessageChannelSuffix", codec)
442 if (api != null) { 504 if (api != null) {
443 channel.setMessageHandler { _, reply -> 505 channel.setMessageHandler { _, reply ->
@@ -515,6 +577,27 @@ interface PlatformHostApi { @@ -515,6 +577,27 @@ interface PlatformHostApi {
515 channel.setMessageHandler(null) 577 channel.setMessageHandler(null)
516 } 578 }
517 } 579 }
  580 + run {
  581 + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.uploadFile$separatedMessageChannelSuffix", codec)
  582 + if (api != null) {
  583 + channel.setMessageHandler { message, reply ->
  584 + val args = message as List<Any?>
  585 + val filePathArg = args[0] as String
  586 + val resourceTypeArg = args[1] as HResourceType
  587 + api.uploadFile(filePathArg, resourceTypeArg) { result: Result<String?> ->
  588 + val error = result.exceptionOrNull()
  589 + if (error != null) {
  590 + reply.reply(PlatformApiPigeonUtils.wrapError(error))
  591 + } else {
  592 + val data = result.getOrNull()
  593 + reply.reply(PlatformApiPigeonUtils.wrapResult(data))
  594 + }
  595 + }
  596 + }
  597 + } else {
  598 + channel.setMessageHandler(null)
  599 + }
  600 + }
518 } 601 }
519 } 602 }
520 } 603 }
@@ -147,6 +147,8 @@ interface WearEngineHostApi { @@ -147,6 +147,8 @@ interface WearEngineHostApi {
147 * removing the image background on the host platform. 147 * removing the image background on the host platform.
148 */ 148 */
149 fun removeBackground(originImagePath: String, callback: (Result<String?>) -> Unit) 149 fun removeBackground(originImagePath: String, callback: (Result<String?>) -> Unit)
  150 + /** 是否有已安装的表盘 */
  151 + fun hasInstalledWatchSurface(callback: (Result<Boolean>) -> Unit)
150 152
151 companion object { 153 companion object {
152 /** The codec used by WearEngineHostApi. */ 154 /** The codec used by WearEngineHostApi. */
@@ -256,6 +258,24 @@ interface WearEngineHostApi { @@ -256,6 +258,24 @@ interface WearEngineHostApi {
256 channel.setMessageHandler(null) 258 channel.setMessageHandler(null)
257 } 259 }
258 } 260 }
  261 + run {
  262 + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.hasInstalledWatchSurface$separatedMessageChannelSuffix", codec)
  263 + if (api != null) {
  264 + channel.setMessageHandler { _, reply ->
  265 + api.hasInstalledWatchSurface{ result: Result<Boolean> ->
  266 + val error = result.exceptionOrNull()
  267 + if (error != null) {
  268 + reply.reply(WearEngineApiPigeonUtils.wrapError(error))
  269 + } else {
  270 + val data = result.getOrNull()
  271 + reply.reply(WearEngineApiPigeonUtils.wrapResult(data))
  272 + }
  273 + }
  274 + }
  275 + } else {
  276 + channel.setMessageHandler(null)
  277 + }
  278 + }
259 } 279 }
260 } 280 }
261 } 281 }
@@ -112,6 +112,11 @@ func deepHashPlatformApi(value: Any?, hasher: inout Hasher) { @@ -112,6 +112,11 @@ func deepHashPlatformApi(value: Any?, hasher: inout Hasher) {
112 112
113 113
114 114
  115 +enum HResourceType: Int {
  116 + case image = 0
  117 + case video = 1
  118 +}
  119 +
115 /// Generated class from Pigeon that represents data sent in messages. 120 /// Generated class from Pigeon that represents data sent in messages.
116 struct AppleSignInModel: Hashable { 121 struct AppleSignInModel: Hashable {
117 var userId: String 122 var userId: String
@@ -301,12 +306,18 @@ private class PlatformApiPigeonCodecReader: FlutterStandardReader { @@ -301,12 +306,18 @@ private class PlatformApiPigeonCodecReader: FlutterStandardReader {
301 override func readValue(ofType type: UInt8) -> Any? { 306 override func readValue(ofType type: UInt8) -> Any? {
302 switch type { 307 switch type {
303 case 129: 308 case 129:
304 - return AppleSignInModel.fromList(self.readValue() as! [Any?]) 309 + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?)
  310 + if let enumResultAsInt = enumResultAsInt {
  311 + return HResourceType(rawValue: enumResultAsInt)
  312 + }
  313 + return nil
305 case 130: 314 case 130:
306 - return WatchAppOtherInfo.fromList(self.readValue() as! [Any?]) 315 + return AppleSignInModel.fromList(self.readValue() as! [Any?])
307 case 131: 316 case 131:
308 - return AppleProductInfo.fromList(self.readValue() as! [Any?]) 317 + return WatchAppOtherInfo.fromList(self.readValue() as! [Any?])
309 case 132: 318 case 132:
  319 + return AppleProductInfo.fromList(self.readValue() as! [Any?])
  320 + case 133:
310 return AppleProductPaymentResult.fromList(self.readValue() as! [Any?]) 321 return AppleProductPaymentResult.fromList(self.readValue() as! [Any?])
311 default: 322 default:
312 return super.readValue(ofType: type) 323 return super.readValue(ofType: type)
@@ -316,17 +327,20 @@ private class PlatformApiPigeonCodecReader: FlutterStandardReader { @@ -316,17 +327,20 @@ private class PlatformApiPigeonCodecReader: FlutterStandardReader {
316 327
317 private class PlatformApiPigeonCodecWriter: FlutterStandardWriter { 328 private class PlatformApiPigeonCodecWriter: FlutterStandardWriter {
318 override func writeValue(_ value: Any) { 329 override func writeValue(_ value: Any) {
319 - if let value = value as? AppleSignInModel { 330 + if let value = value as? HResourceType {
320 super.writeByte(129) 331 super.writeByte(129)
  332 + super.writeValue(value.rawValue)
  333 + } else if let value = value as? AppleSignInModel {
  334 + super.writeByte(130)
321 super.writeValue(value.toList()) 335 super.writeValue(value.toList())
322 } else if let value = value as? WatchAppOtherInfo { 336 } else if let value = value as? WatchAppOtherInfo {
323 - super.writeByte(130) 337 + super.writeByte(131)
324 super.writeValue(value.toList()) 338 super.writeValue(value.toList())
325 } else if let value = value as? AppleProductInfo { 339 } else if let value = value as? AppleProductInfo {
326 - super.writeByte(131) 340 + super.writeByte(132)
327 super.writeValue(value.toList()) 341 super.writeValue(value.toList())
328 } else if let value = value as? AppleProductPaymentResult { 342 } else if let value = value as? AppleProductPaymentResult {
329 - super.writeByte(132) 343 + super.writeByte(133)
330 super.writeValue(value.toList()) 344 super.writeValue(value.toList())
331 } else { 345 } else {
332 super.writeValue(value) 346 super.writeValue(value)
@@ -354,6 +368,8 @@ protocol PlatformHostApi { @@ -354,6 +368,8 @@ protocol PlatformHostApi {
354 /// 返回完整的 User-Agent 字符串,由 native 侧组装: 368 /// 返回完整的 User-Agent 字符串,由 native 侧组装:
355 /// `{systemWebViewUA} {appName}/{versionCode}({versionName})({manufacturer}##{brand}##{model}; {OS}{osVersion}; {height}x{width})(huawei)` 369 /// `{systemWebViewUA} {appName}/{versionCode}({versionName})({manufacturer}##{brand}##{model}; {OS}{osVersion}; {height}x{width})(huawei)`
356 func getFullUserAgent() throws -> String 370 func getFullUserAgent() throws -> String
  371 + /// 是否是测试环境
  372 + func isDebugEnvoriment() throws -> Bool
357 /// 更新用户信息, 有登录态后调用 373 /// 更新用户信息, 有登录态后调用
358 /// jsonString: UserPreferences的序列化string 374 /// jsonString: UserPreferences的序列化string
359 /// baseUrl: 请求地址, https://api.doublefeel.cn 375 /// baseUrl: 请求地址, https://api.doublefeel.cn
@@ -362,9 +378,11 @@ protocol PlatformHostApi { @@ -362,9 +378,11 @@ protocol PlatformHostApi {
362 func logout() throws 378 func logout() throws
363 /// 刷新会员信息 379 /// 刷新会员信息
364 func refreshVip() throws 380 func refreshVip() throws
365 - /// 刷新watch app 和 表盘的所有数据: 381 + /// 刷新watch app 和 表盘的所有数据:
366 /// 任何可能导致展示watch内容变化时调用(好友变化、某些设置导致变化等) 382 /// 任何可能导致展示watch内容变化时调用(好友变化、某些设置导致变化等)
367 func refreshWatchAppAndWidgets() throws 383 func refreshWatchAppAndWidgets() throws
  384 + /// 请求评分弹窗
  385 + func requestAppReview(completion: @escaping (Result<Bool, Error>) -> Void)
368 /// 请求苹果登录 386 /// 请求苹果登录
369 func requestAppleSignIn(completion: @escaping (Result<AppleSignInModel?, Error>) -> Void) 387 func requestAppleSignIn(completion: @escaping (Result<AppleSignInModel?, Error>) -> Void)
370 /// 查询指定id的苹果商品 388 /// 查询指定id的苹果商品
@@ -375,6 +393,9 @@ protocol PlatformHostApi { @@ -375,6 +393,9 @@ protocol PlatformHostApi {
375 func performApplePayment(productId: String, uuid: String, completion: @escaping (Result<AppleProductPaymentResult?, Error>) -> Void) 393 func performApplePayment(productId: String, uuid: String, completion: @escaping (Result<AppleProductPaymentResult?, Error>) -> Void)
376 /// 恢复购买 394 /// 恢复购买
377 func performRestore(completion: @escaping (Result<Bool, Error>) -> Void) 395 func performRestore(completion: @escaping (Result<Bool, Error>) -> Void)
  396 + /// 上传文件到云端
  397 + /// 注意catch flutter error
  398 + func uploadFile(filePath: String, resourceType: HResourceType, completion: @escaping (Result<String?, Error>) -> Void)
378 } 399 }
379 400
380 /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. 401 /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
@@ -398,6 +419,20 @@ class PlatformHostApiSetup { @@ -398,6 +419,20 @@ class PlatformHostApiSetup {
398 } else { 419 } else {
399 getFullUserAgentChannel.setMessageHandler(nil) 420 getFullUserAgentChannel.setMessageHandler(nil)
400 } 421 }
  422 + /// 是否是测试环境
  423 + let isDebugEnvorimentChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.isDebugEnvoriment\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
  424 + if let api = api {
  425 + isDebugEnvorimentChannel.setMessageHandler { _, reply in
  426 + do {
  427 + let result = try api.isDebugEnvoriment()
  428 + reply(wrapResult(result))
  429 + } catch {
  430 + reply(wrapError(error))
  431 + }
  432 + }
  433 + } else {
  434 + isDebugEnvorimentChannel.setMessageHandler(nil)
  435 + }
401 /// 更新用户信息, 有登录态后调用 436 /// 更新用户信息, 有登录态后调用
402 /// jsonString: UserPreferences的序列化string 437 /// jsonString: UserPreferences的序列化string
403 /// baseUrl: 请求地址, https://api.doublefeel.cn 438 /// baseUrl: 请求地址, https://api.doublefeel.cn
@@ -445,7 +480,7 @@ class PlatformHostApiSetup { @@ -445,7 +480,7 @@ class PlatformHostApiSetup {
445 } else { 480 } else {
446 refreshVipChannel.setMessageHandler(nil) 481 refreshVipChannel.setMessageHandler(nil)
447 } 482 }
448 - /// 刷新watch app 和 表盘的所有数据: 483 + /// 刷新watch app 和 表盘的所有数据:
449 /// 任何可能导致展示watch内容变化时调用(好友变化、某些设置导致变化等) 484 /// 任何可能导致展示watch内容变化时调用(好友变化、某些设置导致变化等)
450 let refreshWatchAppAndWidgetsChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.refreshWatchAppAndWidgets\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) 485 let refreshWatchAppAndWidgetsChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.refreshWatchAppAndWidgets\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
451 if let api = api { 486 if let api = api {
@@ -460,6 +495,22 @@ class PlatformHostApiSetup { @@ -460,6 +495,22 @@ class PlatformHostApiSetup {
460 } else { 495 } else {
461 refreshWatchAppAndWidgetsChannel.setMessageHandler(nil) 496 refreshWatchAppAndWidgetsChannel.setMessageHandler(nil)
462 } 497 }
  498 + /// 请求评分弹窗
  499 + let requestAppReviewChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.requestAppReview\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
  500 + if let api = api {
  501 + requestAppReviewChannel.setMessageHandler { _, reply in
  502 + api.requestAppReview { result in
  503 + switch result {
  504 + case .success(let res):
  505 + reply(wrapResult(res))
  506 + case .failure(let error):
  507 + reply(wrapError(error))
  508 + }
  509 + }
  510 + }
  511 + } else {
  512 + requestAppReviewChannel.setMessageHandler(nil)
  513 + }
463 /// 请求苹果登录 514 /// 请求苹果登录
464 let requestAppleSignInChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.requestAppleSignIn\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) 515 let requestAppleSignInChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.requestAppleSignIn\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
465 if let api = api { 516 if let api = api {
@@ -532,5 +583,25 @@ class PlatformHostApiSetup { @@ -532,5 +583,25 @@ class PlatformHostApiSetup {
532 } else { 583 } else {
533 performRestoreChannel.setMessageHandler(nil) 584 performRestoreChannel.setMessageHandler(nil)
534 } 585 }
  586 + /// 上传文件到云端
  587 + /// 注意catch flutter error
  588 + let uploadFileChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.uploadFile\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
  589 + if let api = api {
  590 + uploadFileChannel.setMessageHandler { message, reply in
  591 + let args = message as! [Any?]
  592 + let filePathArg = args[0] as! String
  593 + let resourceTypeArg = args[1] as! HResourceType
  594 + api.uploadFile(filePath: filePathArg, resourceType: resourceTypeArg) { result in
  595 + switch result {
  596 + case .success(let res):
  597 + reply(wrapResult(res))
  598 + case .failure(let error):
  599 + reply(wrapError(error))
  600 + }
  601 + }
  602 + }
  603 + } else {
  604 + uploadFileChannel.setMessageHandler(nil)
  605 + }
535 } 606 }
536 } 607 }
@@ -37,6 +37,18 @@ class PriceFormatter{ @@ -37,6 +37,18 @@ class PriceFormatter{
37 * `{systemWebViewUA} doublefeel/{versionCode}({versionName})(Apple##Apple##{model}; iOS{osVersion}; {height}x{width})(huawei)` 37 * `{systemWebViewUA} doublefeel/{versionCode}({versionName})(Apple##Apple##{model}; iOS{osVersion}; {height}x{width})(huawei)`
38 */ 38 */
39 final class PlatformHostApiImpl: PlatformHostApi { 39 final class PlatformHostApiImpl: PlatformHostApi {
  40 + func isDebugEnvoriment() throws -> Bool {
  41 + return true
  42 + }
  43 +
  44 + func requestAppReview(completion: @escaping (Result<Bool, any Error>) -> Void) {
  45 +
  46 + }
  47 +
  48 + func uploadFile(filePath: String, resourceType: HResourceType, completion: @escaping (Result<String?, any Error>) -> Void) {
  49 +
  50 + }
  51 +
40 func logout() throws { 52 func logout() throws {
41 53
42 } 54 }
@@ -192,6 +192,8 @@ protocol WearEngineHostApi { @@ -192,6 +192,8 @@ protocol WearEngineHostApi {
192 /// Opens the system photo picker and returns a local PNG file path after 192 /// Opens the system photo picker and returns a local PNG file path after
193 /// removing the image background on the host platform. 193 /// removing the image background on the host platform.
194 func removeBackground(originImagePath: String, completion: @escaping (Result<String?, Error>) -> Void) 194 func removeBackground(originImagePath: String, completion: @escaping (Result<String?, Error>) -> Void)
  195 + /// 是否有已安装的表盘
  196 + func hasInstalledWatchSurface(completion: @escaping (Result<Bool, Error>) -> Void)
195 } 197 }
196 198
197 /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. 199 /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
@@ -288,5 +290,21 @@ class WearEngineHostApiSetup { @@ -288,5 +290,21 @@ class WearEngineHostApiSetup {
288 } else { 290 } else {
289 removeBackgroundChannel.setMessageHandler(nil) 291 removeBackgroundChannel.setMessageHandler(nil)
290 } 292 }
  293 + /// 是否有已安装的表盘
  294 + let hasInstalledWatchSurfaceChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.hasInstalledWatchSurface\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
  295 + if let api = api {
  296 + hasInstalledWatchSurfaceChannel.setMessageHandler { _, reply in
  297 + api.hasInstalledWatchSurface { result in
  298 + switch result {
  299 + case .success(let res):
  300 + reply(wrapResult(res))
  301 + case .failure(let error):
  302 + reply(wrapError(error))
  303 + }
  304 + }
  305 + }
  306 + } else {
  307 + hasInstalledWatchSurfaceChannel.setMessageHandler(nil)
  308 + }
291 } 309 }
292 } 310 }
@@ -8,6 +8,10 @@ enum ImageProcessError: Error{ @@ -8,6 +8,10 @@ enum ImageProcessError: Error{
8 } 8 }
9 9
10 final class WearEngineHostApiImpl: WearEngineHostApi { 10 final class WearEngineHostApiImpl: WearEngineHostApi {
  11 + func hasInstalledWatchSurface(completion: @escaping (Result<Bool, any Error>) -> Void) {
  12 +
  13 + }
  14 +
11 private let watchService: WatchConnectivityService 15 private let watchService: WatchConnectivityService
12 16
13 init(watchService: WatchConnectivityService = .shared) { 17 init(watchService: WatchConnectivityService = .shared) {
@@ -6,6 +6,7 @@ import 'package:doublefeel_flutter/app/modules/home/widgets/my/account_setting_v @@ -6,6 +6,7 @@ import 'package:doublefeel_flutter/app/modules/home/widgets/my/account_setting_v
6 import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_theme_models.dart'; 6 import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_theme_models.dart';
7 import 'package:doublefeel_flutter/app/routes/app_pages.dart'; 7 import 'package:doublefeel_flutter/app/routes/app_pages.dart';
8 import 'package:doublefeel_flutter/app/utils/dialog_utils.dart'; 8 import 'package:doublefeel_flutter/app/utils/dialog_utils.dart';
  9 +import 'package:doublefeel_flutter/core/config/app_environment_config.dart';
9 import 'package:doublefeel_flutter/core/theme/app_theme.dart'; 10 import 'package:doublefeel_flutter/core/theme/app_theme.dart';
10 import 'package:doublefeel_flutter/core/util/size_extensions.dart'; 11 import 'package:doublefeel_flutter/core/util/size_extensions.dart';
11 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; 12 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
@@ -24,6 +25,7 @@ class MyTab extends GetView<MyController> { @@ -24,6 +25,7 @@ class MyTab extends GetView<MyController> {
24 @override 25 @override
25 Widget build(BuildContext context) { 26 Widget build(BuildContext context) {
26 final userPrefs = Get.find<UserPreferencesStorage>(); 27 final userPrefs = Get.find<UserPreferencesStorage>();
  28 + final environmentConfig = Get.find<AppEnvironmentConfig>();
27 29
28 return Container( 30 return Container(
29 color: context.colors.backgroundPage, 31 color: context.colors.backgroundPage,
@@ -74,7 +76,7 @@ class MyTab extends GetView<MyController> { @@ -74,7 +76,7 @@ class MyTab extends GetView<MyController> {
74 controller.testAppleHealthUpload(); 76 controller.testAppleHealthUpload();
75 }, 77 },
76 ), 78 ),
77 - if (kDebugMode) ...[ 79 + if (environmentConfig.isDebug) ...[
78 const SizedBox(height: 12), 80 const SizedBox(height: 12),
79 _SettingsRow( 81 _SettingsRow(
80 title: 'Route List', 82 title: 'Route List',
@@ -26,6 +26,7 @@ class LoginController extends GetxController { @@ -26,6 +26,7 @@ class LoginController extends GetxController {
26 final UserPreferencesStorage _userPrefs = Get.find<UserPreferencesStorage>(); 26 final UserPreferencesStorage _userPrefs = Get.find<UserPreferencesStorage>();
27 final UserAccountStorage _userAccount = Get.find<UserAccountStorage>(); 27 final UserAccountStorage _userAccount = Get.find<UserAccountStorage>();
28 final UserStateService _userStateService = Get.find<UserStateService>(); 28 final UserStateService _userStateService = Get.find<UserStateService>();
  29 + final environmentConfig = Get.find<AppEnvironmentConfig>();
29 30
30 final phoneController = TextEditingController(); 31 final phoneController = TextEditingController();
31 final codeController = TextEditingController(); 32 final codeController = TextEditingController();
@@ -178,7 +178,7 @@ class LoginView extends GetView<LoginController> { @@ -178,7 +178,7 @@ class LoginView extends GetView<LoginController> {
178 child: const _AgreementText(), 178 child: const _AgreementText(),
179 ), 179 ),
180 180
181 - if (kDebugMode) 181 + if (controller.environmentConfig.isDebug)
182 Positioned( 182 Positioned(
183 left: 0, 183 left: 0,
184 right: 0, 184 right: 0,
  1 +import 'dart:async';
  2 +import 'dart:io';
  3 +
1 import 'package:doublefeel_flutter/app/routes/app_pages.dart'; 4 import 'package:doublefeel_flutter/app/routes/app_pages.dart';
2 import 'package:doublefeel_flutter/core/network/api/theme_api.dart'; 5 import 'package:doublefeel_flutter/core/network/api/theme_api.dart';
3 import 'package:doublefeel_flutter/core/result/app_result.dart'; 6 import 'package:doublefeel_flutter/core/result/app_result.dart';
4 import 'package:doublefeel_flutter/core/util/app_toast.dart'; 7 import 'package:doublefeel_flutter/core/util/app_toast.dart';
  8 +import 'package:doublefeel_flutter/pigeon/platform_api.g.dart';
5 import 'package:doublefeel_flutter/pigeon/wear_engine_api.g.dart'; 9 import 'package:doublefeel_flutter/pigeon/wear_engine_api.g.dart';
6 import 'package:flutter/material.dart'; 10 import 'package:flutter/material.dart';
7 import 'package:get/get.dart'; 11 import 'package:get/get.dart';
  12 +import 'package:image_cropper/image_cropper.dart';
8 import 'package:image_picker/image_picker.dart'; 13 import 'package:image_picker/image_picker.dart';
  14 +import 'package:path_provider/path_provider.dart';
9 15
10 import '../models/watch_theme_models.dart'; 16 import '../models/watch_theme_models.dart';
11 import '../widgets/watch_theme_dialogs.dart'; 17 import '../widgets/watch_theme_dialogs.dart';
@@ -14,13 +20,18 @@ class CreateWatchThemeController extends GetxController { @@ -14,13 +20,18 @@ class CreateWatchThemeController extends GetxController {
14 CreateWatchThemeController(this._themeApi); 20 CreateWatchThemeController(this._themeApi);
15 21
16 final ThemeApi _themeApi; 22 final ThemeApi _themeApi;
  23 + final PlatformHostApi _platformHostApi = PlatformHostApi();
17 final WearEngineHostApi _wearEngineHostApi = WearEngineHostApi(); 24 final WearEngineHostApi _wearEngineHostApi = WearEngineHostApi();
  25 + final ImagePicker _imagePicker = ImagePicker();
18 26
19 final customThemeName = ''.obs; 27 final customThemeName = ''.obs;
20 final agreedToSubmission = true.obs; 28 final agreedToSubmission = true.obs;
21 final customStatusNames = ['状态优秀', '状态正常', '注意压力', '压力过载'].obs; 29 final customStatusNames = ['状态优秀', '状态正常', '注意压力', '压力过载'].obs;
22 final customImagePaths = RxList<String?>.filled(4, null); 30 final customImagePaths = RxList<String?>.filled(4, null);
  31 + final isRemovingBackground = RxList<bool>.filled(4, false);
23 final isSaving = false.obs; 32 final isSaving = false.obs;
  33 + final List<int> _imageTaskVersions = List<int>.filled(4, 0);
  34 + bool _isClosed = false;
24 late final TextEditingController themeNameController; 35 late final TextEditingController themeNameController;
25 36
26 @override 37 @override
@@ -34,6 +45,7 @@ class CreateWatchThemeController extends GetxController { @@ -34,6 +45,7 @@ class CreateWatchThemeController extends GetxController {
34 45
35 @override 46 @override
36 void onClose() { 47 void onClose() {
  48 + _isClosed = true;
37 themeNameController.dispose(); 49 themeNameController.dispose();
38 super.onClose(); 50 super.onClose();
39 } 51 }
@@ -42,15 +54,136 @@ class CreateWatchThemeController extends GetxController { @@ -42,15 +54,136 @@ class CreateWatchThemeController extends GetxController {
42 !isSaving.value && 54 !isSaving.value &&
43 customThemeName.value.isNotEmpty && 55 customThemeName.value.isNotEmpty &&
44 agreedToSubmission.value && 56 agreedToSubmission.value &&
  57 + !isRemovingBackground.any((isProcessing) => isProcessing) &&
45 customImagePaths.every((path) => path != null && path.isNotEmpty); 58 customImagePaths.every((path) => path != null && path.isNotEmpty);
46 59
47 Future<void> pickCustomImage(int index) async { 60 Future<void> pickCustomImage(int index) async {
48 - final picker = ImagePicker();  
49 - final XFile? image = await picker.pickImage(source: ImageSource.gallery);  
50 - if (image != null) {  
51 - customImagePaths[index] = image.path;  
52 - } else {  
53 - AppToast.show('图片选择失败'); 61 + if (index < 0 || index >= customImagePaths.length) {
  62 + return;
  63 + }
  64 +
  65 + try {
  66 + final image = await _imagePicker.pickImage(source: ImageSource.gallery);
  67 + if (image == null) {
  68 + return;
  69 + }
  70 +
  71 + final croppedImage = await ImageCropper().cropImage(
  72 + sourcePath: image.path,
  73 + aspectRatio: const CropAspectRatio(ratioX: 1, ratioY: 1),
  74 + maxWidth: 280,
  75 + maxHeight: 280,
  76 + compressFormat: ImageCompressFormat.png,
  77 + compressQuality: 100,
  78 + uiSettings: [
  79 + AndroidUiSettings(
  80 + toolbarTitle: '裁剪表盘图片',
  81 + lockAspectRatio: true,
  82 + ),
  83 + IOSUiSettings(
  84 + title: '裁剪表盘图片',
  85 + aspectRatioLockEnabled: true,
  86 + resetAspectRatioEnabled: false,
  87 + ),
  88 + ],
  89 + );
  90 + if (croppedImage == null) {
  91 + return;
  92 + }
  93 +
  94 + final savedPath = await _saveCroppedImage(croppedImage.path, index);
  95 + await _deleteReplacedImage(customImagePaths[index], savedPath);
  96 + customImagePaths[index] = savedPath;
  97 + final taskVersion = ++_imageTaskVersions[index];
  98 + isRemovingBackground[index] = true;
  99 +
  100 + // Pigeon 方法是异步接口,iOS 端的 Vision 请求在 Task.detached 中执行。
  101 + // 此处不等待结果,让裁切图先显示;处理完成后再原位替换。
  102 + unawaited(_removeBackgroundAndReplace(index, savedPath, taskVersion));
  103 + } catch (_) {
  104 + AppToast.show('图片处理失败,请重试');
  105 + }
  106 + }
  107 +
  108 + Future<void> _removeBackgroundAndReplace(
  109 + int index,
  110 + String croppedImagePath,
  111 + int taskVersion,
  112 + ) async {
  113 + String? savedProcessedPath;
  114 + try {
  115 + final processedPath =
  116 + await _wearEngineHostApi.removeBackground(croppedImagePath);
  117 + if (processedPath == null || processedPath.isEmpty) {
  118 + return;
  119 + }
  120 +
  121 + savedProcessedPath = await _saveProcessedImage(processedPath, index);
  122 + if (!_isCurrentImageTask(index, croppedImagePath, taskVersion)) {
  123 + await _deleteReplacedImage(savedProcessedPath, '');
  124 + return;
  125 + }
  126 +
  127 + customImagePaths[index] = savedProcessedPath;
  128 + await _deleteReplacedImage(croppedImagePath, savedProcessedPath);
  129 + } catch (error, stackTrace) {
  130 + debugPrint('Watch theme background removal failed: $error');
  131 + debugPrintStack(stackTrace: stackTrace);
  132 + } finally {
  133 + if (!_isClosed && _imageTaskVersions[index] == taskVersion) {
  134 + isRemovingBackground[index] = false;
  135 + }
  136 + }
  137 + }
  138 +
  139 + bool _isCurrentImageTask(
  140 + int index,
  141 + String croppedImagePath,
  142 + int taskVersion,
  143 + ) {
  144 + return !_isClosed &&
  145 + _imageTaskVersions[index] == taskVersion &&
  146 + customImagePaths[index] == croppedImagePath;
  147 + }
  148 +
  149 + Future<String> _saveCroppedImage(String sourcePath, int index) async {
  150 + return _saveThemeImage(sourcePath, index, 'cropped');
  151 + }
  152 +
  153 + Future<String> _saveProcessedImage(String sourcePath, int index) async {
  154 + return _saveThemeImage(sourcePath, index, 'processed');
  155 + }
  156 +
  157 + Future<String> _saveThemeImage(
  158 + String sourcePath,
  159 + int index,
  160 + String stage,
  161 + ) async {
  162 + final documentsDirectory = await getApplicationDocumentsDirectory();
  163 + final themeImageDirectory =
  164 + Directory('${documentsDirectory.path}/watch_theme');
  165 + await themeImageDirectory.create(recursive: true);
  166 +
  167 + final fileName = 'watch_theme_${index}_${stage}_'
  168 + '${DateTime.now().microsecondsSinceEpoch}.png';
  169 + final savedFile = await File(sourcePath).copy(
  170 + '${themeImageDirectory.path}/$fileName',
  171 + );
  172 + return savedFile.path;
  173 + }
  174 +
  175 + Future<void> _deleteReplacedImage(
  176 + String? previousPath,
  177 + String replacementPath,
  178 + ) async {
  179 + if (previousPath == null || previousPath == replacementPath) {
  180 + return;
  181 + }
  182 +
  183 + final previousFile = File(previousPath);
  184 + if (previousFile.parent.path.endsWith('/watch_theme') &&
  185 + await previousFile.exists()) {
  186 + await previousFile.delete();
54 } 187 }
55 } 188 }
56 189
@@ -95,73 +228,61 @@ class CreateWatchThemeController extends GetxController { @@ -95,73 +228,61 @@ class CreateWatchThemeController extends GetxController {
95 } 228 }
96 229
97 isSaving.value = true; 230 isSaving.value = true;
98 -  
99 - var imagePath1 = customImagePaths[0];  
100 - var imagePath2 = customImagePaths[1];  
101 - var imagePath3 = customImagePaths[2];  
102 - var imagePath4 = customImagePaths[3];  
103 -  
104 - if (imagePath1 != null) {  
105 - final path = await _wearEngineHostApi.removeBackground(imagePath1);  
106 - if (path != null) {  
107 - imagePath1 = path;  
108 - }  
109 - }  
110 - if (imagePath2 != null) {  
111 - final path = await _wearEngineHostApi.removeBackground(imagePath2);  
112 - if (path != null) {  
113 - imagePath2 = path;  
114 - }  
115 - }  
116 - if (imagePath3 != null) {  
117 - final path = await _wearEngineHostApi.removeBackground(imagePath3);  
118 - if (path != null) {  
119 - imagePath3 = path; 231 + try {
  232 + final uploadedImageUrls = <String>[];
  233 + for (final imagePath in customImagePaths) {
  234 + final imageUrl = await _platformHostApi.uploadFile(
  235 + imagePath!,
  236 + HResourceType.image,
  237 + );
  238 + if (imageUrl == null || imageUrl.isEmpty) {
  239 + AppToast.show('图片上传失败,请重试');
  240 + return;
  241 + }
  242 + uploadedImageUrls.add(imageUrl);
120 } 243 }
121 - }  
122 - if (imagePath4 != null) {  
123 - final path = await _wearEngineHostApi.removeBackground(imagePath4);  
124 - if (path != null) {  
125 - imagePath4 = path; 244 +
  245 + final result = await _themeApi.createTheme(
  246 + themeName: customThemeName.value,
  247 + energeticDescription: customStatusNames[0],
  248 + energeticImage: uploadedImageUrls[0],
  249 + normalDescription: customStatusNames[1],
  250 + normalImage: uploadedImageUrls[1],
  251 + slightStressfulDescription: customStatusNames[2],
  252 + slightStressfulImage: uploadedImageUrls[2],
  253 + stressfulDescription: customStatusNames[3],
  254 + stressfulImage: uploadedImageUrls[3],
  255 + );
  256 + if (result is AppFailure<void>) {
  257 + AppToast.show(result.error.displayMessage);
  258 + return;
126 } 259 }
127 - }  
128 260
129 - //TODO: - 上传图片  
130 -  
131 - // final result = await _themeApi.createTheme(  
132 - // themeName: customThemeName.value,  
133 - // energeticDescription: customStatusNames[0],  
134 - // energeticImage: customImagePaths[0] ?? '',  
135 - // normalDescription: customStatusNames[1],  
136 - // normalImage: customImagePaths[1] ?? '',  
137 - // slightStressfulDescription: customStatusNames[2],  
138 - // slightStressfulImage: customImagePaths[2] ?? '',  
139 - // stressfulDescription: customStatusNames[3],  
140 - // stressfulImage: customImagePaths[3] ?? '',  
141 - // );  
142 - isSaving.value = false;  
143 -  
144 - //Test: - 测试抠图结果  
145 - customImagePaths.value = [imagePath1, imagePath2, imagePath3, imagePath4];  
146 - customImagePaths.refresh();  
147 -  
148 - // final createdTheme = await _loadCreatedTheme();  
149 - // Get.toNamed(Routes.WATCH_THEME_CUSTOM_PREVIEW, arguments: {  
150 - // 'theme': createdTheme ?? _buildThemeItem(),  
151 - // }); 261 + final createdTheme = await _loadCreatedTheme();
  262 + await Get.toNamed(
  263 + Routes.WATCH_THEME_CUSTOM_PREVIEW,
  264 + arguments: {
  265 + 'theme': createdTheme ?? _buildThemeItem(uploadedImageUrls),
  266 + },
  267 + );
  268 + } catch (_) {
  269 + AppToast.show('创建表盘失败,请重试');
  270 + } finally {
  271 + isSaving.value = false;
  272 + }
152 } 273 }
153 274
154 - WatchThemeItem _buildThemeItem() { 275 + WatchThemeItem _buildThemeItem(List<String> imageUrls) {
155 return WatchThemeItem( 276 return WatchThemeItem(
156 themeName: customThemeName.value, 277 themeName: customThemeName.value,
157 energeticDescription: customStatusNames[0], 278 energeticDescription: customStatusNames[0],
158 - energeticImage: customImagePaths[0], 279 + energeticImage: imageUrls[0],
159 normalDescription: customStatusNames[1], 280 normalDescription: customStatusNames[1],
160 - normalImage: customImagePaths[1], 281 + normalImage: imageUrls[1],
161 slightStressfulDescription: customStatusNames[2], 282 slightStressfulDescription: customStatusNames[2],
162 - slightStressfulImage: customImagePaths[2], 283 + slightStressfulImage: imageUrls[2],
163 stressfulDescription: customStatusNames[3], 284 stressfulDescription: customStatusNames[3],
164 - stressfulImage: customImagePaths[3], 285 + stressfulImage: imageUrls[3],
165 ); 286 );
166 } 287 }
167 288
@@ -71,6 +71,10 @@ class CustomWatchThemePreviewController extends GetxController { @@ -71,6 +71,10 @@ class CustomWatchThemePreviewController extends GetxController {
71 } 71 }
72 } 72 }
73 73
  74 + _changeFriend() {
  75 + //TODO: - bottomSheet 的方式展示出 SelectFriendView
  76 + }
  77 +
74 Future<void> addWatchFace() async { 78 Future<void> addWatchFace() async {
75 WearDeviceInfo? device; 79 WearDeviceInfo? device;
76 try { 80 try {
@@ -89,7 +93,7 @@ class CustomWatchThemePreviewController extends GetxController { @@ -89,7 +93,7 @@ class CustomWatchThemePreviewController extends GetxController {
89 93
90 await Get.dialog<void>( 94 await Get.dialog<void>(
91 WatchThemeSyncDialog( 95 WatchThemeSyncDialog(
92 - faceAsset: R.assetsImagesWatchThemeCustomFacePreview, 96 + themeImageUrl: themeItem.energeticImage,
93 onSync: _applyAndSyncWatchFace, 97 onSync: _applyAndSyncWatchFace,
94 ), 98 ),
95 barrierDismissible: false, 99 barrierDismissible: false,
@@ -59,7 +59,7 @@ class WatchThemePreviewController extends GetxController { @@ -59,7 +59,7 @@ class WatchThemePreviewController extends GetxController {
59 59
60 await Get.dialog<void>( 60 await Get.dialog<void>(
61 WatchThemeSyncDialog( 61 WatchThemeSyncDialog(
62 - faceAsset: R.assetsImagesWatchThemeFaceDefault, 62 + themeImageUrl: themeItem.energeticImage,
63 onSync: _applyAndSyncWatchFace, 63 onSync: _applyAndSyncWatchFace,
64 ), 64 ),
65 barrierDismissible: false, 65 barrierDismissible: false,
@@ -67,6 +67,10 @@ class WatchThemePreviewController extends GetxController { @@ -67,6 +67,10 @@ class WatchThemePreviewController extends GetxController {
67 ); 67 );
68 } 68 }
69 69
  70 + _changeFriend() {
  71 + //TODO: - bottomSheet 的方式展示出 SelectFriendView
  72 + }
  73 +
70 Future<bool> _applyAndSyncWatchFace() async { 74 Future<bool> _applyAndSyncWatchFace() async {
71 final themeId = themeItem.id; 75 final themeId = themeItem.id;
72 if (themeId == null) { 76 if (themeId == null) {
1 import 'dart:io'; 1 import 'dart:io';
2 2
3 -import 'package:doublefeel_flutter/core/util/size_extensions.dart';  
4 -import 'package:doublefeel_flutter/r.dart';  
5 import 'package:flutter/material.dart'; 3 import 'package:flutter/material.dart';
6 import 'package:flutter/services.dart'; 4 import 'package:flutter/services.dart';
7 import 'package:get/get.dart'; 5 import 'package:get/get.dart';
@@ -64,10 +62,13 @@ class CreateWatchThemeView extends GetView<CreateWatchThemeController> { @@ -64,10 +62,13 @@ class CreateWatchThemeView extends GetView<CreateWatchThemeController> {
64 child: Column( 62 child: Column(
65 children: [ 63 children: [
66 SizedBox(height: 12), 64 SizedBox(height: 12),
67 - WatchFacePreview(  
68 - faceAsset:  
69 - R.assetsImagesWatchThemeCustomFaceCreate,  
70 - ), 65 + Obx(() {
  66 + return WatchFacePreview(
  67 + type: WatchFacePreviewType.singleMedium,
  68 + themeImageUrl:
  69 + controller.customImagePaths.first,
  70 + );
  71 + }),
71 SizedBox(height: 20), 72 SizedBox(height: 20),
72 _EditorCard(controller: controller), 73 _EditorCard(controller: controller),
73 SizedBox(height: 26), 74 SizedBox(height: 26),
1 -import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_theme_models.dart';  
2 -import 'package:doublefeel_flutter/core/util/size_extensions.dart'; 1 +import 'package:doublefeel_flutter/app/modules/watch_theme/widgets/dial_preview_card.dart';
3 import 'package:doublefeel_flutter/r.dart'; 2 import 'package:doublefeel_flutter/r.dart';
4 import 'package:flutter/material.dart'; 3 import 'package:flutter/material.dart';
5 import 'package:flutter/services.dart'; 4 import 'package:flutter/services.dart';
6 import 'package:get/get.dart'; 5 import 'package:get/get.dart';
7 6
8 import '../controllers/custom_watch_theme_preview_controller.dart'; 7 import '../controllers/custom_watch_theme_preview_controller.dart';
  8 +import '../widgets/status_preview_card.dart';
9 import '../widgets/watch_face_preview.dart'; 9 import '../widgets/watch_face_preview.dart';
10 import '../widgets/watch_theme_bottom_actions.dart'; 10 import '../widgets/watch_theme_bottom_actions.dart';
11 import '../widgets/watch_theme_colors.dart'; 11 import '../widgets/watch_theme_colors.dart';
@@ -62,7 +62,7 @@ class CustomWatchThemePreviewView @@ -62,7 +62,7 @@ class CustomWatchThemePreviewView
62 '删除', 62 '删除',
63 style: TextStyle( 63 style: TextStyle(
64 color: const Color(0xFFFC4447), 64 color: const Color(0xFFFC4447),
65 - fontSize: 14.dp, 65 + fontSize: 14,
66 fontWeight: FontWeight.w500, 66 fontWeight: FontWeight.w500,
67 ), 67 ),
68 ), 68 ),
@@ -71,18 +71,20 @@ class CustomWatchThemePreviewView @@ -71,18 +71,20 @@ class CustomWatchThemePreviewView
71 Expanded( 71 Expanded(
72 child: SingleChildScrollView( 72 child: SingleChildScrollView(
73 physics: const ClampingScrollPhysics(), 73 physics: const ClampingScrollPhysics(),
74 - padding: EdgeInsets.only(bottom: 96.dp), 74 + padding: const EdgeInsets.only(bottom: 96),
75 child: Column( 75 child: Column(
76 children: [ 76 children: [
77 WatchThemeHeader( 77 WatchThemeHeader(
78 title: controller.themeItem.title, 78 title: controller.themeItem.title,
79 - faceAsset:  
80 - R.assetsImagesWatchThemeCustomFacePreview, 79 + themeImageUrl: controller.themeItem.energeticImage,
81 ), 80 ),
82 - SizedBox(height: 26.dp),  
83 - _CustomStatusPreviewCard(controller: controller),  
84 - SizedBox(height: 12.dp),  
85 - const _CustomDialPreviewCard(), 81 + const SizedBox(height: 26),
  82 + StatusPreviewCard(themeItem: controller.themeItem),
  83 + const SizedBox(height: 12),
  84 + DialPreviewCard(
  85 + showFriend: true,
  86 + themeImageUrl: controller.themeItem.energeticImage,
  87 + )
86 ], 88 ],
87 ), 89 ),
88 ), 90 ),
@@ -102,92 +104,3 @@ class CustomWatchThemePreviewView @@ -102,92 +104,3 @@ class CustomWatchThemePreviewView
102 ); 104 );
103 } 105 }
104 } 106 }
105 -  
106 -class _CustomStatusPreviewCard extends StatelessWidget {  
107 - const _CustomStatusPreviewCard({required this.controller});  
108 -  
109 - final CustomWatchThemePreviewController controller;  
110 -  
111 - static final _assets = [  
112 - R.assetsImagesWatchThemeCustomStatusExcellent,  
113 - R.assetsImagesWatchThemeCustomStatusNormal,  
114 - R.assetsImagesWatchThemeCustomStatusStress,  
115 - R.assetsImagesWatchThemeCustomStatusOverload,  
116 - ];  
117 -  
118 -  
119 - @override  
120 - Widget build(BuildContext context) {  
121 - final infoList = controller.themeItem.infoList;  
122 - return WatchThemeSectionCard(  
123 - padding: EdgeInsets.fromLTRB(20.dp, 20.dp, 20.dp, 17.dp),  
124 - child: Column(  
125 - crossAxisAlignment: CrossAxisAlignment.start,  
126 - children: [  
127 - const WatchThemeSectionTitle('状态预览'),  
128 - SizedBox(height: 24.dp),  
129 - Row(  
130 - mainAxisAlignment: MainAxisAlignment.spaceBetween,  
131 - children: [  
132 - for (var i = 0; i < 4; i++)  
133 - Column(  
134 - children: [  
135 - ClipRRect(  
136 - borderRadius: BorderRadius.circular(16.dp),  
137 - child: Image.asset(  
138 - _assets[i],  
139 - width: 60.dp,  
140 - height: 60.dp,  
141 - fit: BoxFit.cover,  
142 - ),  
143 - ),  
144 - SizedBox(height: 4.dp),  
145 - Text(  
146 - i < infoList.length ? infoList[i].title : '',  
147 - style: TextStyle(  
148 - color: defaultThemeTemplates[i].color,  
149 - fontSize: 12.dp,  
150 - ),  
151 - ),  
152 - ],  
153 - ),  
154 - ],  
155 - ),  
156 - ],  
157 - ),  
158 - );  
159 - }  
160 -}  
161 -  
162 -class _CustomDialPreviewCard extends StatelessWidget {  
163 - const _CustomDialPreviewCard();  
164 -  
165 - @override  
166 - Widget build(BuildContext context) {  
167 - return WatchThemeSectionCard(  
168 - padding: EdgeInsets.fromLTRB(20.dp, 20.dp, 0, 27.dp),  
169 - child: Column(  
170 - crossAxisAlignment: CrossAxisAlignment.start,  
171 - children: [  
172 - const WatchThemeSectionTitle('表盘预览'),  
173 - SizedBox(height: 16.dp),  
174 - Row(  
175 - children: [  
176 - WatchFacePreview(  
177 - width: 114,  
178 - height: 136,  
179 - faceAsset: R.assetsImagesWatchThemeCustomPreviewAlt,  
180 - ),  
181 - SizedBox(width: 18.dp),  
182 - WatchFacePreview(  
183 - width: 114,  
184 - height: 136,  
185 - faceAsset: R.assetsImagesWatchThemeCustomFacePreview,  
186 - ),  
187 - ],  
188 - ),  
189 - ],  
190 - ),  
191 - );  
192 - }  
193 -}  
@@ -55,7 +55,7 @@ class WatchThemePreviewView extends GetView<WatchThemePreviewController> { @@ -55,7 +55,7 @@ class WatchThemePreviewView extends GetView<WatchThemePreviewController> {
55 ), 55 ),
56 Expanded( 56 Expanded(
57 child: SingleChildScrollView( 57 child: SingleChildScrollView(
58 - physics: const ClampingScrollPhysics(), 58 + physics: const AlwaysScrollableScrollPhysics(),
59 padding: const EdgeInsets.only(bottom: 96), 59 padding: const EdgeInsets.only(bottom: 96),
60 child: Column( 60 child: Column(
61 children: [ 61 children: [
@@ -63,7 +63,9 @@ class WatchThemePreviewView extends GetView<WatchThemePreviewController> { @@ -63,7 +63,9 @@ class WatchThemePreviewView extends GetView<WatchThemePreviewController> {
63 const SizedBox(height: 26), 63 const SizedBox(height: 26),
64 StatusPreviewCard(themeItem: controller.themeItem), 64 StatusPreviewCard(themeItem: controller.themeItem),
65 const SizedBox(height: 12), 65 const SizedBox(height: 12),
66 - const DialPreviewCard(), 66 + DialPreviewCard(
  67 + showFriend: true,
  68 + ),
67 ], 69 ],
68 ), 70 ),
69 ), 71 ),
@@ -55,7 +55,7 @@ class WatchThemeView extends GetView<WatchThemeController> { @@ -55,7 +55,7 @@ class WatchThemeView extends GetView<WatchThemeController> {
55 Expanded( 55 Expanded(
56 child: Obx( 56 child: Obx(
57 () => SingleChildScrollView( 57 () => SingleChildScrollView(
58 - physics: const BouncingScrollPhysics(), 58 + physics: const AlwaysScrollableScrollPhysics(),
59 padding: const EdgeInsets.only(bottom: 24), 59 padding: const EdgeInsets.only(bottom: 24),
60 child: Column( 60 child: Column(
61 children: [ 61 children: [
1 -import 'package:doublefeel_flutter/core/util/size_extensions.dart';  
2 -import 'package:doublefeel_flutter/r.dart'; 1 +
3 import 'package:flutter/material.dart'; 2 import 'package:flutter/material.dart';
4 3
5 import 'watch_face_preview.dart'; 4 import 'watch_face_preview.dart';
6 import 'watch_theme_section_card.dart'; 5 import 'watch_theme_section_card.dart';
7 6
8 class DialPreviewCard extends StatelessWidget { 7 class DialPreviewCard extends StatelessWidget {
9 - const DialPreviewCard({super.key}); 8 + bool showFriend;
  9 + final String? themeImageUrl;
  10 + final String? themeImageUrl2;
  11 + DialPreviewCard(
  12 + {super.key,
  13 + required this.showFriend,
  14 + this.themeImageUrl,
  15 + this.themeImageUrl2});
10 16
11 @override 17 @override
12 Widget build(BuildContext context) { 18 Widget build(BuildContext context) {
13 return WatchThemeSectionCard( 19 return WatchThemeSectionCard(
14 - padding: EdgeInsets.fromLTRB(20.dp, 20.dp, 0, 27.dp), 20 + padding: EdgeInsets.fromLTRB(20, 20, 0, 27),
15 child: Column( 21 child: Column(
16 crossAxisAlignment: CrossAxisAlignment.start, 22 crossAxisAlignment: CrossAxisAlignment.start,
17 children: [ 23 children: [
18 const WatchThemeSectionTitle('表盘预览'), 24 const WatchThemeSectionTitle('表盘预览'),
19 - SizedBox(height: 16.dp), 25 + SizedBox(height: 16),
20 SizedBox( 26 SizedBox(
21 - height: 136.dp, 27 + height: 136,
22 child: ListView.separated( 28 child: ListView.separated(
23 padding: EdgeInsets.zero, 29 padding: EdgeInsets.zero,
24 scrollDirection: Axis.horizontal, 30 scrollDirection: Axis.horizontal,
25 - physics: const ClampingScrollPhysics(), 31 + physics: const AlwaysScrollableScrollPhysics(),
26 itemBuilder: (context, index) { 32 itemBuilder: (context, index) {
  33 + if (index == 0) {
  34 + return WatchFacePreview(
  35 + type: WatchFacePreviewType.surface,
  36 + themeImageUrl: themeImageUrl,
  37 + );
  38 + } else if (showFriend && index == 1) {
  39 + return WatchFacePreview(
  40 + type: WatchFacePreviewType.coupleSmall,
  41 + themeImageUrl: themeImageUrl,
  42 + themeImageUrl2: themeImageUrl2,
  43 + );
  44 + }
27 return WatchFacePreview( 45 return WatchFacePreview(
28 - width: 114,  
29 - height: 136,  
30 - faceAsset: index == 0  
31 - ? R.assetsImagesWatchThemeFacePreviewAlt  
32 - : R.assetsImagesWatchThemeFaceDefault, 46 + type: WatchFacePreviewType.singleSmall,
  47 + themeImageUrl: themeImageUrl,
33 ); 48 );
34 }, 49 },
35 - separatorBuilder: (context, index) => SizedBox(width: 18.dp),  
36 - itemCount: 2, 50 + separatorBuilder: (context, index) => SizedBox(width: 12),
  51 + itemCount: showFriend ? 3 : 2,
37 ), 52 ),
38 ), 53 ),
39 ], 54 ],
1 import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_theme_models.dart'; 1 import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_theme_models.dart';
2 -import 'package:doublefeel_flutter/core/util/size_extensions.dart';  
3 import 'package:flutter/material.dart'; 2 import 'package:flutter/material.dart';
4 3
5 import 'watch_theme_character_avatar.dart'; 4 import 'watch_theme_character_avatar.dart';
@@ -12,12 +11,12 @@ class StatusPreviewCard extends StatelessWidget { @@ -12,12 +11,12 @@ class StatusPreviewCard extends StatelessWidget {
12 @override 11 @override
13 Widget build(BuildContext context) { 12 Widget build(BuildContext context) {
14 return WatchThemeSectionCard( 13 return WatchThemeSectionCard(
15 - padding: EdgeInsets.fromLTRB(20.dp, 20.dp, 20.dp, 17.dp), 14 + padding: const EdgeInsets.fromLTRB(20, 20, 20, 17),
16 child: Column( 15 child: Column(
17 crossAxisAlignment: CrossAxisAlignment.start, 16 crossAxisAlignment: CrossAxisAlignment.start,
18 children: [ 17 children: [
19 const WatchThemeSectionTitle('状态预览'), 18 const WatchThemeSectionTitle('状态预览'),
20 - SizedBox(height: 24.dp), 19 + const SizedBox(height: 24),
21 Row( 20 Row(
22 mainAxisAlignment: MainAxisAlignment.spaceBetween, 21 mainAxisAlignment: MainAxisAlignment.spaceBetween,
23 children: _previewItemView(), 22 children: _previewItemView(),
@@ -38,12 +37,12 @@ class StatusPreviewCard extends StatelessWidget { @@ -38,12 +37,12 @@ class StatusPreviewCard extends StatelessWidget {
38 assetPath: infoList[i].assetPath, 37 assetPath: infoList[i].assetPath,
39 imageUrl: infoList[i].imgUrl, 38 imageUrl: infoList[i].imgUrl,
40 ), 39 ),
41 - SizedBox(height: 4.dp), 40 + const SizedBox(height: 4),
42 Text( 41 Text(
43 i < infoList.length ? infoList[i].title : '', 42 i < infoList.length ? infoList[i].title : '',
44 style: TextStyle( 43 style: TextStyle(
45 color: defaultThemeTemplates[i].color, 44 color: defaultThemeTemplates[i].color,
46 - fontSize: 12.dp, 45 + fontSize: 12,
47 fontWeight: FontWeight.w400, 46 fontWeight: FontWeight.w400,
48 ), 47 ),
49 ), 48 ),
1 import 'dart:io'; 1 import 'dart:io';
2 2
3 -import 'package:doublefeel_flutter/core/util/size_extensions.dart';  
4 -import 'package:doublefeel_flutter/r.dart'; 3 +import 'package:cached_network_image/cached_network_image.dart';
5 import 'package:flutter/material.dart'; 4 import 'package:flutter/material.dart';
  5 +import 'package:get/get.dart';
6 6
7 -import 'watch_theme_colors.dart'; 7 +enum WatchFacePreviewType {
  8 + surface,
  9 + singleSmall,
  10 + singleMedium,
  11 + coupleSmall,
  12 +}
8 13
9 class WatchFacePreview extends StatelessWidget { 14 class WatchFacePreview extends StatelessWidget {
10 const WatchFacePreview({ 15 const WatchFacePreview({
11 super.key, 16 super.key,
12 - this.width = 134,  
13 - this.height = 160,  
14 - this.faceAsset,  
15 - this.faceFilePath, 17 + this.type = WatchFacePreviewType.singleMedium,
  18 + this.themeImageUrl,
  19 + this.themeImageUrl2,
16 }); 20 });
17 21
18 - final double width;  
19 - final double height;  
20 - final String? faceAsset;  
21 - final String? faceFilePath; 22 + final String? themeImageUrl;
  23 + final String? themeImageUrl2;
  24 + final WatchFacePreviewType type;
  25 +
  26 + String get _backgroundAsset {
  27 + if (type == WatchFacePreviewType.surface) {
  28 + return 'assets/images/watch_theme/watch_theme_preview_surface.png';
  29 + }
  30 + if (type == WatchFacePreviewType.coupleSmall) {
  31 + return 'assets/images/watch_theme/watch_theme_preview_couple.png';
  32 + }
  33 + return 'assets/images/watch_theme/watch_theme_preview_single.png';
  34 + }
  35 +
  36 + Size get _previewSize {
  37 + if (type == WatchFacePreviewType.singleMedium) {
  38 + return Size(134, 160);
  39 + }
  40 + return Size(120, 136);
  41 + }
  42 +
  43 + Size get _themeSize {
  44 + if (type == WatchFacePreviewType.singleMedium) {
  45 + return Size(78, 78);
  46 + }
  47 + if (type == WatchFacePreviewType.singleSmall) {
  48 + return Size(55, 55);
  49 + }
  50 + if (type == WatchFacePreviewType.surface) {
  51 + return Size(44, 44);
  52 + }
  53 + return Size(55, 55);
  54 + }
22 55
23 @override 56 @override
24 Widget build(BuildContext context) { 57 Widget build(BuildContext context) {
25 - final w = width.dp;  
26 - final h = height.dp;  
27 - final inset = 6.dp;  
28 - final knobWidth = (width * 0.067).dp;  
29 - final knobHeight = (height * 0.156).dp; 58 + final size = _previewSize;
30 59
31 return SizedBox( 60 return SizedBox(
32 - width: w + knobWidth,  
33 - height: h,  
34 child: Stack( 61 child: Stack(
35 clipBehavior: Clip.none, 62 clipBehavior: Clip.none,
36 children: [ 63 children: [
37 - Positioned(  
38 - left: 0,  
39 - top: 0,  
40 - child: Container(  
41 - width: w,  
42 - height: h,  
43 - decoration: BoxDecoration(  
44 - color: WatchThemeColors.watchShell,  
45 - borderRadius: BorderRadius.circular((width * 0.239).dp),  
46 - ),  
47 - ), 64 + Image(
  65 + image: AssetImage(_backgroundAsset),
  66 + width: size.width,
  67 + height: size.height,
  68 + fit: BoxFit.fitWidth,
48 ), 69 ),
49 - Positioned(  
50 - left: inset,  
51 - top: inset,  
52 - child: Container(  
53 - width: w - inset * 2,  
54 - height: h - inset * 2,  
55 - decoration: BoxDecoration(  
56 - color: WatchThemeColors.watchScreen,  
57 - borderRadius: BorderRadius.circular((width * 0.209).dp),  
58 - ),  
59 - clipBehavior: Clip.antiAlias,  
60 - child: _buildFaceImage(),  
61 - ),  
62 - ),  
63 - Positioned(  
64 - left: w - 4.dp,  
65 - top: (height * 0.2375).dp,  
66 - child: Container(  
67 - width: knobWidth,  
68 - height: knobHeight,  
69 - decoration: BoxDecoration(  
70 - color: WatchThemeColors.watchShell,  
71 - borderRadius: BorderRadius.horizontal(  
72 - right: Radius.circular(5.dp),  
73 - ),  
74 - ), 70 + _buildThemeImages(),
  71 + ],
  72 + ),
  73 + );
  74 + }
  75 +
  76 + Widget _buildThemeImages() {
  77 + final themeSize = _themeSize;
  78 +
  79 + if (type == WatchFacePreviewType.singleMedium) {
  80 + return Positioned(
  81 + top: 40,
  82 + width: themeSize.width,
  83 + height: themeSize.height,
  84 + child: _buildFaceImage(themeImageUrl,
  85 + 'assets/images/watch_theme/official_default_green.png'),
  86 + );
  87 + }
  88 + if (type == WatchFacePreviewType.singleSmall) {
  89 + return Positioned(
  90 + top: 36,
  91 + width: themeSize.width,
  92 + height: themeSize.height,
  93 + child: _buildFaceImage(themeImageUrl,
  94 + 'assets/images/watch_theme/official_default_green.png'),
  95 + );
  96 + }
  97 + if (type == WatchFacePreviewType.surface) {
  98 + return Positioned(
  99 + top: 90,
  100 + left: 24,
  101 + width: themeSize.width,
  102 + height: themeSize.height,
  103 + child: _buildFaceImage(themeImageUrl,
  104 + 'assets/images/watch_theme/official_default_green.png'),
  105 + );
  106 + }
  107 +
  108 + return Positioned(
  109 + top: 30,
  110 + child: Row(
  111 + children: [
  112 + SizedBox(
  113 + width: themeSize.width,
  114 + height: themeSize.height,
  115 + child: _buildFaceImage(themeImageUrl,
  116 + 'assets/images/watch_theme/official_default_green.png'),
75 ), 117 ),
  118 + SizedBox(
  119 + width: 8,
76 ), 120 ),
77 - Positioned(  
78 - left: w - 1.dp,  
79 - top: (height * 0.5125).dp,  
80 - child: Container(  
81 - width: 3.dp,  
82 - height: (height * 0.256).dp,  
83 - decoration: BoxDecoration(  
84 - color: WatchThemeColors.watchShell,  
85 - borderRadius: BorderRadius.horizontal(  
86 - right: Radius.circular(2.dp),  
87 - ),  
88 - ),  
89 - ), 121 + SizedBox(
  122 + width: themeSize.width,
  123 + height: themeSize.height,
  124 + child: _buildFaceImage(themeImageUrl2,
  125 + 'assets/images/watch_theme/official_default_blue.png'),
90 ), 126 ),
91 ], 127 ],
92 ), 128 ),
93 ); 129 );
94 } 130 }
95 131
96 - Widget _buildFaceImage() {  
97 - if (faceFilePath != null && faceFilePath!.isNotEmpty) {  
98 - return Image.file(File(faceFilePath!), fit: BoxFit.cover); 132 + Widget _buildFaceImage(String? themeUrl, String placeholder) {
  133 + if (themeUrl != null && themeUrl.isNotEmpty) {
  134 + if (themeUrl.isURL) {
  135 + return CachedNetworkImage(imageUrl: themeUrl);
  136 + }
  137 + return Image.file(File(themeUrl), fit: BoxFit.cover);
99 } 138 }
100 return Image.asset( 139 return Image.asset(
101 - faceAsset ?? R.assetsImagesWatchThemeFaceDefault, 140 + placeholder,
102 fit: BoxFit.cover, 141 fit: BoxFit.cover,
103 ); 142 );
104 } 143 }
1 -import 'package:doublefeel_flutter/core/util/size_extensions.dart';  
2 import 'package:flutter/material.dart'; 1 import 'package:flutter/material.dart';
3 2
4 import 'watch_face_preview.dart'; 3 import 'watch_face_preview.dart';
@@ -8,31 +7,29 @@ class WatchThemeHeader extends StatelessWidget { @@ -8,31 +7,29 @@ class WatchThemeHeader extends StatelessWidget {
8 const WatchThemeHeader({ 7 const WatchThemeHeader({
9 super.key, 8 super.key,
10 this.title = '默认主题', 9 this.title = '默认主题',
11 - this.faceAsset,  
12 - this.faceFilePath, 10 + this.themeImageUrl,
13 }); 11 });
14 12
15 final String title; 13 final String title;
16 - final String? faceAsset;  
17 - final String? faceFilePath; 14 + final String? themeImageUrl;
18 15
19 @override 16 @override
20 Widget build(BuildContext context) { 17 Widget build(BuildContext context) {
21 return Column( 18 return Column(
22 children: [ 19 children: [
23 - SizedBox(height: 12.dp), 20 + SizedBox(height: 12),
24 Center( 21 Center(
25 child: WatchFacePreview( 22 child: WatchFacePreview(
26 - faceAsset: faceAsset,  
27 - faceFilePath: faceFilePath, 23 + type: WatchFacePreviewType.singleMedium,
  24 + themeImageUrl: themeImageUrl,
28 ), 25 ),
29 ), 26 ),
30 - SizedBox(height: 12.dp), 27 + SizedBox(height: 12),
31 Text( 28 Text(
32 title, 29 title,
33 style: TextStyle( 30 style: TextStyle(
34 color: WatchThemeColors.textPrimary, 31 color: WatchThemeColors.textPrimary,
35 - fontSize: 20.dp, 32 + fontSize: 20,
36 fontWeight: FontWeight.w600, 33 fontWeight: FontWeight.w600,
37 height: 1.25, 34 height: 1.25,
38 ), 35 ),
@@ -9,13 +9,11 @@ class WatchThemeSyncDialog extends StatefulWidget { @@ -9,13 +9,11 @@ class WatchThemeSyncDialog extends StatefulWidget {
9 const WatchThemeSyncDialog({ 9 const WatchThemeSyncDialog({
10 super.key, 10 super.key,
11 required this.onSync, 11 required this.onSync,
12 - this.faceAsset,  
13 - this.faceFilePath, 12 + this.themeImageUrl,
14 }); 13 });
15 14
16 final Future<bool> Function() onSync; 15 final Future<bool> Function() onSync;
17 - final String? faceAsset;  
18 - final String? faceFilePath; 16 + final String? themeImageUrl;
19 17
20 @override 18 @override
21 State<WatchThemeSyncDialog> createState() => _WatchThemeSyncDialogState(); 19 State<WatchThemeSyncDialog> createState() => _WatchThemeSyncDialogState();
@@ -76,10 +74,8 @@ class _WatchThemeSyncDialogState extends State<WatchThemeSyncDialog> { @@ -76,10 +74,8 @@ class _WatchThemeSyncDialogState extends State<WatchThemeSyncDialog> {
76 right: 0, 74 right: 0,
77 child: Center( 75 child: Center(
78 child: WatchFacePreview( 76 child: WatchFacePreview(
79 - width: 134,  
80 - height: 160,  
81 - faceAsset: widget.faceAsset,  
82 - faceFilePath: widget.faceFilePath, 77 + type: WatchFacePreviewType.singleMedium,
  78 + themeImageUrl: widget.themeImageUrl,
83 ), 79 ),
84 ), 80 ),
85 ), 81 ),
1 import 'package:doublefeel_flutter/app/modules/friends/bindings/friend_home_binding.dart'; 1 import 'package:doublefeel_flutter/app/modules/friends/bindings/friend_home_binding.dart';
2 import 'package:doublefeel_flutter/app/modules/friends/views/friend_home_page.dart'; 2 import 'package:doublefeel_flutter/app/modules/friends/views/friend_home_page.dart';
  3 +import 'package:doublefeel_flutter/core/config/app_environment_config.dart';
3 import 'package:flutter/foundation.dart'; 4 import 'package:flutter/foundation.dart';
4 5
5 import 'package:get/get.dart'; 6 import 'package:get/get.dart';
@@ -65,6 +66,8 @@ abstract final class AppRoutes { @@ -65,6 +66,8 @@ abstract final class AppRoutes {
65 abstract final class AppPages { 66 abstract final class AppPages {
66 static const initialRoute = AppRoutes.splash; 67 static const initialRoute = AppRoutes.splash;
67 68
  69 + static final environmentConfig = Get.find<AppEnvironmentConfig>();
  70 +
68 static final routes = [ 71 static final routes = [
69 GetPage( 72 GetPage(
70 name: AppRoutes.splash, 73 name: AppRoutes.splash,
@@ -81,7 +84,7 @@ abstract final class AppPages { @@ -81,7 +84,7 @@ abstract final class AppPages {
81 page: () => const PhoneLoginView(), 84 page: () => const PhoneLoginView(),
82 binding: LoginBinding(), 85 binding: LoginBinding(),
83 ), 86 ),
84 - if (kDebugMode) 87 + if (environmentConfig.isDebug)
85 GetPage( 88 GetPage(
86 name: AppRoutes.debugEnvironment, 89 name: AppRoutes.debugEnvironment,
87 page: () => const DebugEnvironmentView(), 90 page: () => const DebugEnvironmentView(),
  1 +import 'package:doublefeel_flutter/pigeon/platform_api.g.dart';
  2 +import 'package:flutter/foundation.dart';
1 import 'package:get/get.dart'; 3 import 'package:get/get.dart';
2 4
3 import '../constants/app_const.dart'; 5 import '../constants/app_const.dart';
@@ -10,11 +12,13 @@ class AppEnvironmentConfig { @@ -10,11 +12,13 @@ class AppEnvironmentConfig {
10 AppEnvironmentConfig(this._storage); 12 AppEnvironmentConfig(this._storage);
11 13
12 final LocalStorage _storage; 14 final LocalStorage _storage;
  15 + final PlatformHostApi _platformHostApi = PlatformHostApi();
13 16
14 final Rx<AppEnvironment> environment = AppEnvironment.release.obs; 17 final Rx<AppEnvironment> environment = AppEnvironment.release.obs;
15 18
16 Future<AppEnvironmentConfig> init() async { 19 Future<AppEnvironmentConfig> init() async {
17 environment.value = await _storage.readEnvironment(); 20 environment.value = await _storage.readEnvironment();
  21 + _isNativeDebug = await _platformHostApi.isDebugEnvoriment();
18 return this; 22 return this;
19 } 23 }
20 24
@@ -23,6 +27,9 @@ class AppEnvironmentConfig { @@ -23,6 +27,9 @@ class AppEnvironmentConfig {
23 await _storage.writeEnvironment(value); 27 await _storage.writeEnvironment(value);
24 } 28 }
25 29
  30 + bool _isNativeDebug = false;
  31 +
  32 + bool get isDebug => kDebugMode || _isNativeDebug;
26 String get serverBaseUrl => resolveServerUrl(AppConst.serverBaseUrl); 33 String get serverBaseUrl => resolveServerUrl(AppConst.serverBaseUrl);
27 34
28 String resolveServerUrl(String url) { 35 String resolveServerUrl(String url) {
@@ -30,6 +30,11 @@ bool _deepEquals(Object? a, Object? b) { @@ -30,6 +30,11 @@ bool _deepEquals(Object? a, Object? b) {
30 } 30 }
31 31
32 32
  33 +enum HResourceType {
  34 + image,
  35 + video,
  36 +}
  37 +
33 class AppleSignInModel { 38 class AppleSignInModel {
34 AppleSignInModel({ 39 AppleSignInModel({
35 required this.userId, 40 required this.userId,
@@ -305,17 +310,20 @@ class _PigeonCodec extends StandardMessageCodec { @@ -305,17 +310,20 @@ class _PigeonCodec extends StandardMessageCodec {
305 if (value is int) { 310 if (value is int) {
306 buffer.putUint8(4); 311 buffer.putUint8(4);
307 buffer.putInt64(value); 312 buffer.putInt64(value);
308 - } else if (value is AppleSignInModel) { 313 + } else if (value is HResourceType) {
309 buffer.putUint8(129); 314 buffer.putUint8(129);
  315 + writeValue(buffer, value.index);
  316 + } else if (value is AppleSignInModel) {
  317 + buffer.putUint8(130);
310 writeValue(buffer, value.encode()); 318 writeValue(buffer, value.encode());
311 } else if (value is WatchAppOtherInfo) { 319 } else if (value is WatchAppOtherInfo) {
312 - buffer.putUint8(130); 320 + buffer.putUint8(131);
313 writeValue(buffer, value.encode()); 321 writeValue(buffer, value.encode());
314 } else if (value is AppleProductInfo) { 322 } else if (value is AppleProductInfo) {
315 - buffer.putUint8(131); 323 + buffer.putUint8(132);
316 writeValue(buffer, value.encode()); 324 writeValue(buffer, value.encode());
317 } else if (value is AppleProductPaymentResult) { 325 } else if (value is AppleProductPaymentResult) {
318 - buffer.putUint8(132); 326 + buffer.putUint8(133);
319 writeValue(buffer, value.encode()); 327 writeValue(buffer, value.encode());
320 } else { 328 } else {
321 super.writeValue(buffer, value); 329 super.writeValue(buffer, value);
@@ -326,12 +334,15 @@ class _PigeonCodec extends StandardMessageCodec { @@ -326,12 +334,15 @@ class _PigeonCodec extends StandardMessageCodec {
326 Object? readValueOfType(int type, ReadBuffer buffer) { 334 Object? readValueOfType(int type, ReadBuffer buffer) {
327 switch (type) { 335 switch (type) {
328 case 129: 336 case 129:
329 - return AppleSignInModel.decode(readValue(buffer)!); 337 + final int? value = readValue(buffer) as int?;
  338 + return value == null ? null : HResourceType.values[value];
330 case 130: 339 case 130:
331 - return WatchAppOtherInfo.decode(readValue(buffer)!); 340 + return AppleSignInModel.decode(readValue(buffer)!);
332 case 131: 341 case 131:
333 - return AppleProductInfo.decode(readValue(buffer)!); 342 + return WatchAppOtherInfo.decode(readValue(buffer)!);
334 case 132: 343 case 132:
  344 + return AppleProductInfo.decode(readValue(buffer)!);
  345 + case 133:
335 return AppleProductPaymentResult.decode(readValue(buffer)!); 346 return AppleProductPaymentResult.decode(readValue(buffer)!);
336 default: 347 default:
337 return super.readValueOfType(type, buffer); 348 return super.readValueOfType(type, buffer);
@@ -382,6 +393,35 @@ class PlatformHostApi { @@ -382,6 +393,35 @@ class PlatformHostApi {
382 } 393 }
383 } 394 }
384 395
  396 + /// 是否是测试环境
  397 + Future<bool> isDebugEnvoriment() async {
  398 + final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.isDebugEnvoriment$pigeonVar_messageChannelSuffix';
  399 + final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
  400 + pigeonVar_channelName,
  401 + pigeonChannelCodec,
  402 + binaryMessenger: pigeonVar_binaryMessenger,
  403 + );
  404 + final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
  405 + final List<Object?>? pigeonVar_replyList =
  406 + await pigeonVar_sendFuture as List<Object?>?;
  407 + if (pigeonVar_replyList == null) {
  408 + throw _createConnectionError(pigeonVar_channelName);
  409 + } else if (pigeonVar_replyList.length > 1) {
  410 + throw PlatformException(
  411 + code: pigeonVar_replyList[0]! as String,
  412 + message: pigeonVar_replyList[1] as String?,
  413 + details: pigeonVar_replyList[2],
  414 + );
  415 + } else if (pigeonVar_replyList[0] == null) {
  416 + throw PlatformException(
  417 + code: 'null-error',
  418 + message: 'Host platform returned null value for non-null return value.',
  419 + );
  420 + } else {
  421 + return (pigeonVar_replyList[0] as bool?)!;
  422 + }
  423 + }
  424 +
385 /// 更新用户信息, 有登录态后调用 425 /// 更新用户信息, 有登录态后调用
386 /// jsonString: UserPreferences的序列化string 426 /// jsonString: UserPreferences的序列化string
387 /// baseUrl: 请求地址, https://api.doublefeel.cn 427 /// baseUrl: 请求地址, https://api.doublefeel.cn
@@ -456,7 +496,7 @@ class PlatformHostApi { @@ -456,7 +496,7 @@ class PlatformHostApi {
456 } 496 }
457 } 497 }
458 498
459 - /// 刷新watch app 和 表盘的所有数据: 499 + /// 刷新watch app 和 表盘的所有数据:
460 /// 任何可能导致展示watch内容变化时调用(好友变化、某些设置导致变化等) 500 /// 任何可能导致展示watch内容变化时调用(好友变化、某些设置导致变化等)
461 Future<void> refreshWatchAppAndWidgets() async { 501 Future<void> refreshWatchAppAndWidgets() async {
462 final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.refreshWatchAppAndWidgets$pigeonVar_messageChannelSuffix'; 502 final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.refreshWatchAppAndWidgets$pigeonVar_messageChannelSuffix';
@@ -481,6 +521,35 @@ class PlatformHostApi { @@ -481,6 +521,35 @@ class PlatformHostApi {
481 } 521 }
482 } 522 }
483 523
  524 + /// 请求评分弹窗
  525 + Future<bool> requestAppReview() async {
  526 + final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.requestAppReview$pigeonVar_messageChannelSuffix';
  527 + final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
  528 + pigeonVar_channelName,
  529 + pigeonChannelCodec,
  530 + binaryMessenger: pigeonVar_binaryMessenger,
  531 + );
  532 + final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
  533 + final List<Object?>? pigeonVar_replyList =
  534 + await pigeonVar_sendFuture as List<Object?>?;
  535 + if (pigeonVar_replyList == null) {
  536 + throw _createConnectionError(pigeonVar_channelName);
  537 + } else if (pigeonVar_replyList.length > 1) {
  538 + throw PlatformException(
  539 + code: pigeonVar_replyList[0]! as String,
  540 + message: pigeonVar_replyList[1] as String?,
  541 + details: pigeonVar_replyList[2],
  542 + );
  543 + } else if (pigeonVar_replyList[0] == null) {
  544 + throw PlatformException(
  545 + code: 'null-error',
  546 + message: 'Host platform returned null value for non-null return value.',
  547 + );
  548 + } else {
  549 + return (pigeonVar_replyList[0] as bool?)!;
  550 + }
  551 + }
  552 +
484 /// 请求苹果登录 553 /// 请求苹果登录
485 Future<AppleSignInModel?> requestAppleSignIn() async { 554 Future<AppleSignInModel?> requestAppleSignIn() async {
486 final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.requestAppleSignIn$pigeonVar_messageChannelSuffix'; 555 final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.requestAppleSignIn$pigeonVar_messageChannelSuffix';
@@ -583,4 +652,29 @@ class PlatformHostApi { @@ -583,4 +652,29 @@ class PlatformHostApi {
583 return (pigeonVar_replyList[0] as bool?)!; 652 return (pigeonVar_replyList[0] as bool?)!;
584 } 653 }
585 } 654 }
  655 +
  656 + /// 上传文件到云端
  657 + /// 注意catch flutter error
  658 + Future<String?> uploadFile(String filePath, HResourceType resourceType) async {
  659 + final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.uploadFile$pigeonVar_messageChannelSuffix';
  660 + final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
  661 + pigeonVar_channelName,
  662 + pigeonChannelCodec,
  663 + binaryMessenger: pigeonVar_binaryMessenger,
  664 + );
  665 + final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[filePath, resourceType]);
  666 + final List<Object?>? pigeonVar_replyList =
  667 + await pigeonVar_sendFuture as List<Object?>?;
  668 + if (pigeonVar_replyList == null) {
  669 + throw _createConnectionError(pigeonVar_channelName);
  670 + } else if (pigeonVar_replyList.length > 1) {
  671 + throw PlatformException(
  672 + code: pigeonVar_replyList[0]! as String,
  673 + message: pigeonVar_replyList[1] as String?,
  674 + details: pigeonVar_replyList[2],
  675 + );
  676 + } else {
  677 + return (pigeonVar_replyList[0] as String?);
  678 + }
  679 + }
586 } 680 }
@@ -280,4 +280,33 @@ class WearEngineHostApi { @@ -280,4 +280,33 @@ class WearEngineHostApi {
280 return (pigeonVar_replyList[0] as String?); 280 return (pigeonVar_replyList[0] as String?);
281 } 281 }
282 } 282 }
  283 +
  284 + /// 是否有已安装的表盘
  285 + Future<bool> hasInstalledWatchSurface() async {
  286 + final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.hasInstalledWatchSurface$pigeonVar_messageChannelSuffix';
  287 + final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
  288 + pigeonVar_channelName,
  289 + pigeonChannelCodec,
  290 + binaryMessenger: pigeonVar_binaryMessenger,
  291 + );
  292 + final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
  293 + final List<Object?>? pigeonVar_replyList =
  294 + await pigeonVar_sendFuture as List<Object?>?;
  295 + if (pigeonVar_replyList == null) {
  296 + throw _createConnectionError(pigeonVar_channelName);
  297 + } else if (pigeonVar_replyList.length > 1) {
  298 + throw PlatformException(
  299 + code: pigeonVar_replyList[0]! as String,
  300 + message: pigeonVar_replyList[1] as String?,
  301 + details: pigeonVar_replyList[2],
  302 + );
  303 + } else if (pigeonVar_replyList[0] == null) {
  304 + throw PlatformException(
  305 + code: 'null-error',
  306 + message: 'Host platform returned null value for non-null return value.',
  307 + );
  308 + } else {
  309 + return (pigeonVar_replyList[0] as bool?)!;
  310 + }
  311 + }
283 } 312 }
@@ -40,36 +40,6 @@ class R { @@ -40,36 +40,6 @@ class R {
40 static final String assetsImagesMyAvatarDefault = 40 static final String assetsImagesMyAvatarDefault =
41 'assets/images/my/avatar_default.png'; 41 'assets/images/my/avatar_default.png';
42 42
43 - // watch theme  
44 - static final String assetsImagesWatchThemeFaceDefault =  
45 - 'assets/images/watch_theme/watch_face_default.png';  
46 - static final String assetsImagesWatchThemeFacePreviewAlt =  
47 - 'assets/images/watch_theme/watch_face_preview_alt.png';  
48 - static final String assetsImagesWatchThemeCustomFaceCreate =  
49 - 'assets/images/watch_theme/custom_watch_face_create.png';  
50 - static final String assetsImagesWatchThemeCustomFacePreview =  
51 - 'assets/images/watch_theme/custom_watch_face_preview.png';  
52 - static final String assetsImagesWatchThemeCustomPreviewAlt =  
53 - 'assets/images/watch_theme/custom_preview_alt.png';  
54 - static final String assetsImagesWatchThemeCustomStatusExcellent =  
55 - 'assets/images/watch_theme/custom_status_excellent.png';  
56 - static final String assetsImagesWatchThemeCustomStatusNormal =  
57 - 'assets/images/watch_theme/custom_status_normal.png';  
58 - static final String assetsImagesWatchThemeCustomStatusStress =  
59 - 'assets/images/watch_theme/custom_status_stress.png';  
60 - static final String assetsImagesWatchThemeCustomStatusOverload =  
61 - 'assets/images/watch_theme/custom_status_overload.png';  
62 - static final String assetsImagesWatchThemeOfficialDogWhite =  
63 - 'assets/images/watch_theme/official_dog_white.png';  
64 - static final String assetsImagesWatchThemeOfficialRabbitPink =  
65 - 'assets/images/watch_theme/official_rabbit_pink.png';  
66 - static final String assetsImagesWatchThemeOfficialCatOrange =  
67 - 'assets/images/watch_theme/official_cat_orange.png';  
68 - static final String assetsImagesWatchThemeOfficialElephantBlue =  
69 - 'assets/images/watch_theme/official_elephant_blue.png';  
70 - static final String assetsImagesWatchThemeCustomCatDog =  
71 - 'assets/images/watch_theme/custom_cat_dog.png';  
72 -  
73 // real-time stress 43 // real-time stress
74 static final String assetsImagesRealtimeStressAttentionIcon = 44 static final String assetsImagesRealtimeStressAttentionIcon =
75 'assets/images/common/ic_health_stress_attention.webp'; 45 'assets/images/common/ic_health_stress_attention.webp';
@@ -116,6 +116,9 @@ abstract class PlatformHostApi { @@ -116,6 +116,9 @@ abstract class PlatformHostApi {
116 /// `{systemWebViewUA} {appName}/{versionCode}({versionName})({manufacturer}##{brand}##{model}; {OS}{osVersion}; {height}x{width})(huawei)` 116 /// `{systemWebViewUA} {appName}/{versionCode}({versionName})({manufacturer}##{brand}##{model}; {OS}{osVersion}; {height}x{width})(huawei)`
117 String getFullUserAgent(); 117 String getFullUserAgent();
118 118
  119 + /// 是否是测试环境
  120 + bool isDebugEnvoriment();
  121 +
119 /// 更新用户信息, 有登录态后调用 122 /// 更新用户信息, 有登录态后调用
120 /// jsonString: UserPreferences的序列化string 123 /// jsonString: UserPreferences的序列化string
121 /// baseUrl: 请求地址, https://api.doublefeel.cn 124 /// baseUrl: 请求地址, https://api.doublefeel.cn
@@ -131,6 +134,10 @@ abstract class PlatformHostApi { @@ -131,6 +134,10 @@ abstract class PlatformHostApi {
131 /// 任何可能导致展示watch内容变化时调用(好友变化、某些设置导致变化等) 134 /// 任何可能导致展示watch内容变化时调用(好友变化、某些设置导致变化等)
132 void refreshWatchAppAndWidgets(); 135 void refreshWatchAppAndWidgets();
133 136
  137 + /// 请求评分弹窗
  138 + @async
  139 + bool requestAppReview();
  140 +
134 /// 请求苹果登录 141 /// 请求苹果登录
135 @async 142 @async
136 AppleSignInModel? requestAppleSignIn(); 143 AppleSignInModel? requestAppleSignIn();
@@ -40,9 +40,12 @@ abstract class WearEngineHostApi { @@ -40,9 +40,12 @@ abstract class WearEngineHostApi {
40 40
41 bool sendWatchSyncPayload(String jsonPayload); 41 bool sendWatchSyncPayload(String jsonPayload);
42 42
43 -  
44 /// Opens the system photo picker and returns a local PNG file path after 43 /// Opens the system photo picker and returns a local PNG file path after
45 /// removing the image background on the host platform. 44 /// removing the image background on the host platform.
46 @async 45 @async
47 String? removeBackground(String originImagePath); 46 String? removeBackground(String originImagePath);
  47 +
  48 + /// 是否有已安装的表盘
  49 + @async
  50 + bool hasInstalledWatchSurface();
48 } 51 }
@@ -430,13 +430,13 @@ packages: @@ -430,13 +430,13 @@ packages:
430 source: hosted 430 source: hosted
431 version: "6.1.0" 431 version: "6.1.0"
432 image_cropper_platform_interface: 432 image_cropper_platform_interface:
433 - dependency: transitive 433 + dependency: "direct overridden"
434 description: 434 description:
435 name: image_cropper_platform_interface 435 name: image_cropper_platform_interface
436 - sha256: "2d8db8f4b638e448fa89a1e77cd8f053b4547472bd3ae073169e86626d03afef" 436 + sha256: "6ca6b81769abff9a4dcc3bbd3d75f5dfa9de6b870ae9613c8cd237333a4283af"
437 url: "https://pub.dev" 437 url: "https://pub.dev"
438 source: hosted 438 source: hosted
439 - version: "7.2.0" 439 + version: "7.1.0"
440 image_picker: 440 image_picker:
441 dependency: "direct main" 441 dependency: "direct main"
442 description: 442 description:
@@ -647,7 +647,7 @@ packages: @@ -647,7 +647,7 @@ packages:
647 source: hosted 647 source: hosted
648 version: "1.9.1" 648 version: "1.9.1"
649 path_provider: 649 path_provider:
650 - dependency: "direct overridden" 650 + dependency: "direct main"
651 description: 651 description:
652 name: path_provider 652 name: path_provider
653 sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" 653 sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
@@ -51,6 +51,7 @@ dependencies: @@ -51,6 +51,7 @@ dependencies:
51 video_thumbnail: ^0.5.3 51 video_thumbnail: ^0.5.3
52 table_calendar: ^3.1.3 52 table_calendar: ^3.1.3
53 fluttertoast: ^8.2.2 53 fluttertoast: ^8.2.2
  54 + path_provider: ^2.1.5
54 flutter_localizations: 55 flutter_localizations:
55 sdk: flutter 56 sdk: flutter
56 intl: any 57 intl: any
@@ -60,6 +61,7 @@ dependencies: @@ -60,6 +61,7 @@ dependencies:
60 # 强制使用 hosted 版本统一来源。 61 # 强制使用 hosted 版本统一来源。
61 dependency_overrides: 62 dependency_overrides:
62 path_provider: ^2.1.5 63 path_provider: ^2.1.5
  64 + image_cropper_platform_interface: 7.1.0
63 65
64 dev_dependencies: 66 dev_dependencies:
65 flutter_test: 67 flutter_test: